Update Dialer source from latest green build.

* Refactor voicemail component
* Add new enriched calling components

Test: treehugger, manual aosp testing

Change-Id: I521a0f86327d4b42e14d93927c7d613044ed5942
diff --git a/java/com/android/contacts/common/ContactPhotoManager.java b/java/com/android/contacts/common/ContactPhotoManager.java
index 8344710..0f65a6c 100644
--- a/java/com/android/contacts/common/ContactPhotoManager.java
+++ b/java/com/android/contacts/common/ContactPhotoManager.java
@@ -40,6 +40,7 @@
   public static final int TYPE_BUSINESS = LetterTileDrawable.TYPE_BUSINESS;
   public static final int TYPE_VOICEMAIL = LetterTileDrawable.TYPE_VOICEMAIL;
   public static final int TYPE_DEFAULT = LetterTileDrawable.TYPE_DEFAULT;
+  public static final int TYPE_GENERIC_AVATAR = LetterTileDrawable.TYPE_GENERIC_AVATAR;
   /** Scale and offset default constants used for default letter images */
   public static final float SCALE_DEFAULT = 1.0f;
 
diff --git a/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java b/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java
index 7e1839c..ca12f18 100644
--- a/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java
+++ b/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java
@@ -19,6 +19,7 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
 import android.graphics.ColorFilter;
@@ -48,13 +49,18 @@
    * #TYPE_BUSINESS}, and voicemail contacts should use {@link #TYPE_VOICEMAIL}.
    */
   @Retention(RetentionPolicy.SOURCE)
-  @IntDef({TYPE_PERSON, TYPE_BUSINESS, TYPE_VOICEMAIL})
+  @IntDef({TYPE_PERSON, TYPE_BUSINESS, TYPE_VOICEMAIL, TYPE_GENERIC_AVATAR})
   public @interface ContactType {}
 
   /** Contact type constants */
   public static final int TYPE_PERSON = 1;
   public static final int TYPE_BUSINESS = 2;
   public static final int TYPE_VOICEMAIL = 3;
+  /**
+   * A generic avatar that features the default icon, default color, and no letter. Useful for
+   * situations where a contact is anonymous.
+   */
+  public static final int TYPE_GENERIC_AVATAR = 4;
   @ContactType public static final int TYPE_DEFAULT = TYPE_PERSON;
 
   /**
@@ -87,7 +93,6 @@
   private static Bitmap sDefaultPersonAvatar;
   private static Bitmap sDefaultBusinessAvatar;
   private static Bitmap sDefaultVoicemailAvatar;
-  private static final String TAG = LetterTileDrawable.class.getSimpleName();
   private final Paint mPaint;
   private int mContactType = TYPE_DEFAULT;
   private float mScale = 1.0f;
@@ -97,7 +102,7 @@
   private int mColor;
   private Character mLetter = null;
 
-  private boolean mAvatarWasVoicemailOrBusiness = false;
+  @ContactType private int mAvatarType = TYPE_DEFAULT;
   private String mDisplayName;
 
   public LetterTileDrawable(final Resources res) {
@@ -130,6 +135,7 @@
       case TYPE_VOICEMAIL:
         return sDefaultVoicemailAvatar;
       case TYPE_PERSON:
+      case TYPE_GENERIC_AVATAR:
       default:
         return sDefaultPersonAvatar;
     }
@@ -149,6 +155,14 @@
     drawLetterTile(canvas);
   }
 
+  public Bitmap getBitmap(int width, int height) {
+    Bitmap bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
+    this.setBounds(0, 0, width, height);
+    Canvas canvas = new Canvas(bitmap);
+    this.draw(canvas);
+    return bitmap;
+  }
+
   /**
    * Draw the bitmap onto the canvas at the current bounds taking into account the current scale.
    */
@@ -231,7 +245,9 @@
 
   /** Returns a deterministic color based on the provided contact identifier string. */
   private int pickColor(final String identifier) {
-    if (TextUtils.isEmpty(identifier) || mContactType == TYPE_VOICEMAIL) {
+    if (mContactType == TYPE_VOICEMAIL
+        || mContactType == TYPE_BUSINESS
+        || TextUtils.isEmpty(identifier)) {
       return sDefaultColor;
     }
     // String.hashCode() implementation is not supposed to change across java versions, so
@@ -329,6 +345,10 @@
     return this;
   }
 
+  public boolean tileIsCircular() {
+    return this.mIsCircle;
+  }
+
   /**
    * Creates a canonical letter tile for use across dialer fragments.
    *
@@ -344,39 +364,37 @@
       @Nullable final String identifierForTileColor,
       @Shape final int shape,
       final int contactType) {
-    setContactType(contactType);
-    /**
-     * During hangup, we lose the call state for special types of contacts, like voicemail. To help
-     * callers avoid extraneous LetterTileDrawable allocations, we keep track of the special case
-     * until we encounter a new display name.
-     */
-    if (contactType == TYPE_VOICEMAIL || contactType == TYPE_BUSINESS) {
-      this.mAvatarWasVoicemailOrBusiness = true;
-    } else if (displayName != null && !displayName.equals(mDisplayName)) {
-      this.mAvatarWasVoicemailOrBusiness = false;
-    }
-    this.mDisplayName = displayName;
-    if (shape == SHAPE_CIRCLE) {
-      this.setIsCircular(true);
-    } else {
-      this.setIsCircular(false);
-    }
+
+    this.setIsCircular(shape == SHAPE_CIRCLE);
 
     /**
-     * To preserve style, we don't use contactType to set the tile icon. In the future, when all
-     * callers surface this detail, we can use this to better style the tile icon.
+     * We return quickly under the following conditions: 1. We are asked to draw a default tile, and
+     * no coloring information is provided, meaning no further initialization is necessary OR 2.
+     * We've already invoked this method before, set mDisplayName, and found that it has not
+     * changed. This is useful during events like hangup, when we lose the call state for special
+     * types of contacts, like voicemail. We keep track of the special case until we encounter a new
+     * display name.
      */
-    if (mAvatarWasVoicemailOrBusiness) {
-      this.setLetterAndColorFromContactDetails(null, displayName);
+    if (contactType == TYPE_DEFAULT
+        && ((displayName == null && identifierForTileColor == null)
+            || (displayName != null && displayName.equals(mDisplayName)))) {
       return this;
+    }
+
+    this.mDisplayName = displayName;
+    this.mAvatarType = contactType;
+    setContactType(this.mAvatarType);
+
+    // Special contact types receive default color and no letter tile, but special iconography.
+    if (this.mAvatarType != TYPE_PERSON) {
+      this.setLetterAndColorFromContactDetails(null, null);
     } else {
       if (identifierForTileColor != null) {
         this.setLetterAndColorFromContactDetails(displayName, identifierForTileColor);
-        return this;
       } else {
         this.setLetterAndColorFromContactDetails(displayName, displayName);
-        return this;
       }
     }
+    return this;
   }
 }
diff --git a/java/com/android/contacts/common/list/ContactEntryListFragment.java b/java/com/android/contacts/common/list/ContactEntryListFragment.java
index a8d9b55..278175c 100644
--- a/java/com/android/contacts/common/list/ContactEntryListFragment.java
+++ b/java/com/android/contacts/common/list/ContactEntryListFragment.java
@@ -16,7 +16,6 @@
 
 package com.android.contacts.common.list;
 
-import android.app.Activity;
 import android.app.Fragment;
 import android.app.LoaderManager;
 import android.app.LoaderManager.LoaderCallbacks;
@@ -29,8 +28,8 @@
 import android.os.Message;
 import android.os.Parcelable;
 import android.provider.ContactsContract.Directory;
+import android.support.annotation.Nullable;
 import android.text.TextUtils;
-import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
@@ -42,12 +41,13 @@
 import android.widget.AbsListView.OnScrollListener;
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
-import android.widget.AdapterView.OnItemLongClickListener;
 import android.widget.ListView;
 import com.android.common.widget.CompositeCursorAdapter.Partition;
 import com.android.contacts.common.ContactPhotoManager;
 import com.android.contacts.common.preference.ContactsPreferences;
 import com.android.contacts.common.util.ContactListViewUtils;
+import com.android.dialer.common.LogUtil;
+import java.lang.ref.WeakReference;
 import java.util.Locale;
 
 /** Common base class for various contact-related list fragments. */
@@ -56,9 +56,7 @@
         OnScrollListener,
         OnFocusChangeListener,
         OnTouchListener,
-        OnItemLongClickListener,
         LoaderCallbacks<Cursor> {
-  private static final String TAG = "ContactEntryListFragment";
   private static final String KEY_LIST_STATE = "liststate";
   private static final String KEY_SECTION_HEADER_DISPLAY_ENABLED = "sectionHeaderDisplayEnabled";
   private static final String KEY_PHOTO_LOADER_ENABLED = "photoLoaderEnabled";
@@ -130,15 +128,27 @@
 
   private LoaderManager mLoaderManager;
 
-  private Handler mDelayedDirectorySearchHandler =
-      new Handler() {
-        @Override
-        public void handleMessage(Message msg) {
-          if (msg.what == DIRECTORY_SEARCH_MESSAGE) {
-            loadDirectoryPartition(msg.arg1, (DirectoryPartition) msg.obj);
-          }
-        }
-      };
+  private Handler mDelayedDirectorySearchHandler;
+
+  private static class DelayedDirectorySearchHandler extends Handler {
+    private final WeakReference<ContactEntryListFragment<?>> contactEntryListFragmentRef;
+
+    private DelayedDirectorySearchHandler(ContactEntryListFragment<?> contactEntryListFragment) {
+      this.contactEntryListFragmentRef = new WeakReference<>(contactEntryListFragment);
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+      ContactEntryListFragment<?> contactEntryListFragment = contactEntryListFragmentRef.get();
+      if (contactEntryListFragment == null) {
+        return;
+      }
+      if (msg.what == DIRECTORY_SEARCH_MESSAGE) {
+        contactEntryListFragment.loadDirectoryPartition(msg.arg1, (DirectoryPartition) msg.obj);
+      }
+    }
+  }
+
   private ContactsPreferences.ChangeListener mPreferencesChangeListener =
       new ContactsPreferences.ChangeListener() {
         @Override
@@ -148,6 +158,10 @@
         }
       };
 
+  protected ContactEntryListFragment() {
+    mDelayedDirectorySearchHandler = new DelayedDirectorySearchHandler(this);
+  }
+
   protected abstract View inflateView(LayoutInflater inflater, ViewGroup container);
 
   protected abstract T createListAdapter();
@@ -158,18 +172,10 @@
    */
   protected abstract void onItemClick(int position, long id);
 
-  /**
-   * @param position Please note that the position is already adjusted for header views, so "0"
-   *     means the first list item below header views.
-   */
-  protected boolean onItemLongClick(int position, long id) {
-    return false;
-  }
-
   @Override
-  public void onAttach(Activity activity) {
-    super.onAttach(activity);
-    setContext(activity);
+  public void onAttach(Context context) {
+    super.onAttach(context);
+    setContext(context);
     setLoaderManager(super.getLoaderManager());
   }
 
@@ -343,7 +349,9 @@
         } catch (RuntimeException e) {
           // We don't even know what the projection should be, so no point trying to
           // return an empty MatrixCursor with the correct projection here.
-          Log.w(TAG, "RuntimeException while trying to query ContactsProvider.");
+          LogUtil.w(
+              "ContactEntryListFragment.onLoadInBackground",
+              "RuntimeException while trying to query ContactsProvider.");
           return null;
         }
       }
@@ -441,6 +449,7 @@
   }
 
   public boolean isLoading() {
+    //noinspection SimplifiableIfStatement
     if (mAdapter != null && mAdapter.isLoading()) {
       return true;
     }
@@ -511,7 +520,6 @@
 
     if (mListView != null) {
       mListView.setFastScrollEnabled(hasScrollbar);
-      mListView.setFastScrollAlwaysVisible(hasScrollbar);
       mListView.setVerticalScrollbarPosition(mVerticalScrollbarPosition);
       mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
     }
@@ -573,6 +581,7 @@
     }
   }
 
+  @Nullable
   public final String getQueryString() {
     return mQueryString;
   }
@@ -694,7 +703,6 @@
     }
 
     mListView.setOnItemClickListener(this);
-    mListView.setOnItemLongClickListener(this);
     mListView.setOnFocusChangeListener(this);
     mListView.setOnTouchListener(this);
     mListView.setFastScrollEnabled(!isSearchMode());
@@ -779,16 +787,6 @@
     }
   }
 
-  @Override
-  public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
-    int adjPosition = position - mListView.getHeaderViewsCount();
-
-    if (adjPosition >= 0) {
-      return onItemLongClick(adjPosition, id);
-    }
-    return false;
-  }
-
   private void hideSoftKeyboard() {
     // Hide soft keyboard, if visible
     InputMethodManager inputMethodManager =
diff --git a/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java
index 63f8ca5..8156d97 100644
--- a/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java
+++ b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java
@@ -59,8 +59,6 @@
   private static final String ARG_LISTENER = "listener";
   private static final String ARG_CALL_ID = "call_id";
 
-  private int mTitleResId;
-  private boolean mCanSetDefault;
   private List<PhoneAccountHandle> mAccountHandles;
   private boolean mIsSelected;
   private boolean mIsDefaultChecked;
@@ -126,8 +124,8 @@
 
   @Override
   public Dialog onCreateDialog(Bundle savedInstanceState) {
-    mTitleResId = getArguments().getInt(ARG_TITLE_RES_ID);
-    mCanSetDefault = getArguments().getBoolean(ARG_CAN_SET_DEFAULT);
+    int titleResId = getArguments().getInt(ARG_TITLE_RES_ID);
+    boolean canSetDefault = getArguments().getBoolean(ARG_CAN_SET_DEFAULT);
     mAccountHandles = getArguments().getParcelableArrayList(ARG_ACCOUNT_HANDLES);
     mListener = getArguments().getParcelable(ARG_LISTENER);
     if (savedInstanceState != null) {
@@ -167,11 +165,11 @@
 
     AlertDialog dialog =
         builder
-            .setTitle(mTitleResId)
+            .setTitle(titleResId)
             .setAdapter(selectAccountListAdapter, selectionListener)
             .create();
 
-    if (mCanSetDefault) {
+    if (canSetDefault) {
       // Generate custom checkbox view, lint suppressed since no appropriate parent (is dialog)
       @SuppressLint("InflateParams")
       LinearLayout checkboxLayout =
@@ -190,13 +188,13 @@
   }
 
   @Override
-  public void onStop() {
+  public void onCancel(DialogInterface dialog) {
     if (!mIsSelected && mListener != null) {
       Bundle result = new Bundle();
       result.putString(SelectPhoneAccountListener.EXTRA_CALL_ID, getCallId());
       mListener.onReceiveResult(SelectPhoneAccountListener.RESULT_DISMISSED, result);
     }
-    super.onStop();
+    super.onCancel(dialog);
   }
 
   @Nullable
@@ -213,7 +211,7 @@
     static final String EXTRA_SET_DEFAULT = "extra_set_default";
     static final String EXTRA_CALL_ID = "extra_call_id";
 
-    public SelectPhoneAccountListener() {
+    protected SelectPhoneAccountListener() {
       super(new Handler());
     }
 
@@ -239,7 +237,7 @@
 
     private int mResId;
 
-    public SelectAccountListAdapter(
+    SelectAccountListAdapter(
         Context context, int resource, List<PhoneAccountHandle> accountHandles) {
       super(context, resource, accountHandles);
       mResId = resource;
diff --git a/java/com/android/dialer/app/AndroidManifest.xml b/java/com/android/dialer/app/AndroidManifest.xml
index 80f294a..5ce13db 100644
--- a/java/com/android/dialer/app/AndroidManifest.xml
+++ b/java/com/android/dialer/app/AndroidManifest.xml
@@ -57,11 +57,7 @@
     android:minSdkVersion="23"
     android:targetSdkVersion="25"/>
 
-  <application
-    android:backupAgent='com.android.dialer.backup.DialerBackupAgent'
-    android:fullBackupOnly="true"
-    android:restoreAnyVersion="true"
-    android:name="com.android.dialer.app.DialerApplication">
+  <application android:theme="@style/Theme.AppCompat">
 
     <activity
       android:exported="false"
@@ -75,6 +71,12 @@
       </intent-filter>
     </activity>
 
+    <activity
+      android:label="@string/call_log_activity_title"
+      android:name="com.android.dialer.app.calllog.CallLogActivity"
+      android:theme="@style/DialtactsThemeWithoutActionBarOverlay">
+    </activity>
+
     <receiver android:name="com.android.dialer.app.calllog.CallLogReceiver">
       <intent-filter>
         <action android:name="android.intent.action.NEW_VOICEMAIL"/>
diff --git a/java/com/android/dialer/app/CallDetailActivity.java b/java/com/android/dialer/app/CallDetailActivity.java
deleted file mode 100644
index cda2b2e..0000000
--- a/java/com/android/dialer/app/CallDetailActivity.java
+++ /dev/null
@@ -1,480 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.dialer.app;
-
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.support.v7.app.AppCompatActivity;
-import android.text.BidiFormatter;
-import android.text.TextDirectionHeuristics;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.ListView;
-import android.widget.QuickContactBadge;
-import android.widget.TextView;
-import android.widget.Toast;
-import com.android.contacts.common.ClipboardUtils;
-import com.android.contacts.common.ContactPhotoManager;
-import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
-import com.android.contacts.common.GeoUtil;
-import com.android.contacts.common.preference.ContactsPreferences;
-import com.android.contacts.common.util.UriUtils;
-import com.android.dialer.app.calllog.CallDetailHistoryAdapter;
-import com.android.dialer.app.calllog.CallLogAsyncTaskUtil;
-import com.android.dialer.app.calllog.CallLogAsyncTaskUtil.CallLogAsyncTaskListener;
-import com.android.dialer.app.calllog.CallTypeHelper;
-import com.android.dialer.app.calllog.PhoneAccountUtils;
-import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
-import com.android.dialer.callintent.CallIntentBuilder;
-import com.android.dialer.callintent.nano.CallInitiationType;
-import com.android.dialer.common.Assert;
-import com.android.dialer.common.AsyncTaskExecutor;
-import com.android.dialer.common.AsyncTaskExecutors;
-import com.android.dialer.compat.CompatUtils;
-import com.android.dialer.logging.Logger;
-import com.android.dialer.logging.nano.DialerImpression;
-import com.android.dialer.logging.nano.ScreenEvent;
-import com.android.dialer.phonenumbercache.ContactInfoHelper;
-import com.android.dialer.phonenumberutil.PhoneNumberHelper;
-import com.android.dialer.proguard.UsedByReflection;
-import com.android.dialer.spam.Spam;
-import com.android.dialer.telecom.TelecomUtil;
-import com.android.dialer.util.CallUtil;
-import com.android.dialer.util.DialerUtils;
-import com.android.dialer.util.TouchPointManager;
-
-/**
- * Displays the details of a specific call log entry.
- *
- * <p>This activity can be either started with the URI of a single call log entry, or with the
- * {@link #EXTRA_CALL_LOG_IDS} extra to specify a group of call log entries.
- */
-@UsedByReflection(value = "AndroidManifest-app.xml")
-public class CallDetailActivity extends AppCompatActivity
-    implements MenuItem.OnMenuItemClickListener, View.OnClickListener {
-
-  /** A long array extra containing ids of call log entries to display. */
-  public static final String EXTRA_CALL_LOG_IDS = "EXTRA_CALL_LOG_IDS";
-  /** If we are started with a voicemail, we'll find the uri to play with this extra. */
-  public static final String EXTRA_VOICEMAIL_URI = "EXTRA_VOICEMAIL_URI";
-  /** If the activity was triggered from a notification. */
-  public static final String EXTRA_FROM_NOTIFICATION = "EXTRA_FROM_NOTIFICATION";
-
-  public static final String BLOCKED_OR_SPAM_QUERY_IDENTIFIER = "blockedOrSpamIdentifier";
-
-  private final AsyncTaskExecutor executor = AsyncTaskExecutors.createAsyncTaskExecutor();
-  protected String mNumber;
-  private Context mContext;
-  private ContactInfoHelper mContactInfoHelper;
-  private ContactsPreferences mContactsPreferences;
-  private CallTypeHelper mCallTypeHelper;
-  private ContactPhotoManager mContactPhotoManager;
-  private BidiFormatter mBidiFormatter = BidiFormatter.getInstance();
-  private LayoutInflater mInflater;
-  private Resources mResources;
-  private PhoneCallDetails mDetails;
-  private Uri mVoicemailUri;
-  private String mPostDialDigits = "";
-  private ListView mHistoryList;
-  private QuickContactBadge mQuickContactBadge;
-  private TextView mCallerName;
-  private TextView mCallerNumber;
-  private TextView mAccountLabel;
-  private View mCallButton;
-  private View mEditBeforeCallActionItem;
-  private View mReportActionItem;
-  private View mCopyNumberActionItem;
-  private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
-  private CallLogAsyncTaskListener mCallLogAsyncTaskListener =
-      new CallLogAsyncTaskListener() {
-        @Override
-        public void onDeleteCall() {
-          finish();
-        }
-
-        @Override
-        public void onDeleteVoicemail() {
-          finish();
-        }
-
-        @Override
-        public void onGetCallDetails(final PhoneCallDetails[] details) {
-          if (details == null) {
-            // Somewhere went wrong: we're going to bail out and show error to users.
-            Toast.makeText(mContext, R.string.toast_call_detail_error, Toast.LENGTH_SHORT).show();
-            finish();
-            return;
-          }
-
-          // All calls are from the same number and same contact, so pick the first detail.
-          mDetails = details[0];
-          mNumber = TextUtils.isEmpty(mDetails.number) ? null : mDetails.number.toString();
-
-          if (mNumber == null) {
-            updateDataAndRender(details);
-            return;
-          }
-
-          executor.submit(
-              BLOCKED_OR_SPAM_QUERY_IDENTIFIER,
-              new AsyncTask<Void, Void, Void>() {
-                @Override
-                protected Void doInBackground(Void... params) {
-                  mDetails.isBlocked =
-                      mFilteredNumberAsyncQueryHandler.getBlockedIdSynchronousForCalllogOnly(
-                              mNumber, mDetails.countryIso)
-                          != null;
-                  if (Spam.get(mContext).isSpamEnabled()) {
-                    mDetails.isSpam =
-                        hasIncomingCalls(details)
-                            && Spam.get(mContext)
-                                .checkSpamStatusSynchronous(mNumber, mDetails.countryIso);
-                  }
-                  return null;
-                }
-
-                @Override
-                protected void onPostExecute(Void result) {
-                  updateDataAndRender(details);
-                }
-              });
-        }
-
-        private void updateDataAndRender(PhoneCallDetails[] details) {
-          mPostDialDigits =
-              TextUtils.isEmpty(mDetails.postDialDigits) ? "" : mDetails.postDialDigits;
-
-          final CharSequence callLocationOrType = getNumberTypeOrLocation(mDetails);
-
-          final CharSequence displayNumber;
-          if (!TextUtils.isEmpty(mDetails.postDialDigits)) {
-            displayNumber = mDetails.number + mDetails.postDialDigits;
-          } else {
-            displayNumber = mDetails.displayNumber;
-          }
-
-          final String displayNumberStr =
-              mBidiFormatter.unicodeWrap(displayNumber.toString(), TextDirectionHeuristics.LTR);
-
-          mDetails.nameDisplayOrder = mContactsPreferences.getDisplayOrder();
-
-          if (!TextUtils.isEmpty(mDetails.getPreferredName())) {
-            mCallerName.setText(mDetails.getPreferredName());
-            mCallerNumber.setText(callLocationOrType + " " + displayNumberStr);
-          } else {
-            mCallerName.setText(displayNumberStr);
-            if (!TextUtils.isEmpty(callLocationOrType)) {
-              mCallerNumber.setText(callLocationOrType);
-              mCallerNumber.setVisibility(View.VISIBLE);
-            } else {
-              mCallerNumber.setVisibility(View.GONE);
-            }
-          }
-
-          CharSequence accountLabel =
-              PhoneAccountUtils.getAccountLabel(mContext, mDetails.accountHandle);
-          CharSequence accountContentDescription =
-              PhoneCallDetails.createAccountLabelDescription(
-                  mResources, mDetails.viaNumber, accountLabel);
-          if (!TextUtils.isEmpty(mDetails.viaNumber)) {
-            if (!TextUtils.isEmpty(accountLabel)) {
-              accountLabel =
-                  mResources.getString(
-                      R.string.call_log_via_number_phone_account, accountLabel, mDetails.viaNumber);
-            } else {
-              accountLabel = mResources.getString(R.string.call_log_via_number, mDetails.viaNumber);
-            }
-          }
-          if (!TextUtils.isEmpty(accountLabel)) {
-            mAccountLabel.setText(accountLabel);
-            mAccountLabel.setContentDescription(accountContentDescription);
-            mAccountLabel.setVisibility(View.VISIBLE);
-          } else {
-            mAccountLabel.setVisibility(View.GONE);
-          }
-
-          final boolean canPlaceCallsTo =
-              PhoneNumberHelper.canPlaceCallsTo(mNumber, mDetails.numberPresentation);
-          mCallButton.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE);
-          mCopyNumberActionItem.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE);
-
-          final boolean isSipNumber = PhoneNumberHelper.isSipNumber(mNumber);
-          final boolean isVoicemailNumber =
-              PhoneNumberHelper.isVoicemailNumber(mContext, mDetails.accountHandle, mNumber);
-          final boolean showEditNumberBeforeCallAction =
-              canPlaceCallsTo && !isSipNumber && !isVoicemailNumber;
-          mEditBeforeCallActionItem.setVisibility(
-              showEditNumberBeforeCallAction ? View.VISIBLE : View.GONE);
-
-          final boolean showReportAction =
-              mContactInfoHelper.canReportAsInvalid(mDetails.sourceType, mDetails.objectId);
-          mReportActionItem.setVisibility(showReportAction ? View.VISIBLE : View.GONE);
-
-          invalidateOptionsMenu();
-
-          mHistoryList.setAdapter(
-              new CallDetailHistoryAdapter(mContext, mInflater, mCallTypeHelper, details));
-
-          updateContactPhoto(mDetails.isSpam);
-
-          findViewById(R.id.call_detail).setVisibility(View.VISIBLE);
-        }
-
-        /**
-         * Determines the location geocode text for a call, or the phone number type (if available).
-         *
-         * @param details The call details.
-         * @return The phone number type or location.
-         */
-        private CharSequence getNumberTypeOrLocation(PhoneCallDetails details) {
-          if (details.isSpam) {
-            return mResources.getString(R.string.spam_number_call_log_label);
-          } else if (details.isBlocked) {
-            return mResources.getString(R.string.blocked_number_call_log_label);
-          } else if (!TextUtils.isEmpty(details.namePrimary)) {
-            return Phone.getTypeLabel(mResources, details.numberType, details.numberLabel);
-          } else {
-            return details.geocode;
-          }
-        }
-      };
-
-  @Override
-  protected void onCreate(Bundle icicle) {
-    super.onCreate(icicle);
-
-    mContext = this;
-    mResources = getResources();
-    mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this));
-    mContactsPreferences = new ContactsPreferences(mContext);
-    mCallTypeHelper = new CallTypeHelper(getResources());
-    mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(mContext);
-
-    mVoicemailUri = getIntent().getParcelableExtra(EXTRA_VOICEMAIL_URI);
-
-    getSupportActionBar().setDisplayHomeAsUpEnabled(true);
-
-    setContentView(R.layout.call_detail);
-    mInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
-
-    mHistoryList = (ListView) findViewById(R.id.history);
-    mHistoryList.addHeaderView(mInflater.inflate(R.layout.call_detail_header, null));
-    mHistoryList.addFooterView(mInflater.inflate(R.layout.call_detail_footer, null), null, false);
-
-    mQuickContactBadge = (QuickContactBadge) findViewById(R.id.quick_contact_photo);
-    mQuickContactBadge.setOverlay(null);
-    if (CompatUtils.hasPrioritizedMimeType()) {
-      mQuickContactBadge.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
-    }
-    mCallerName = (TextView) findViewById(R.id.caller_name);
-    mCallerNumber = (TextView) findViewById(R.id.caller_number);
-    mAccountLabel = (TextView) findViewById(R.id.phone_account_label);
-    mContactPhotoManager = ContactPhotoManager.getInstance(this);
-
-    mCallButton = findViewById(R.id.call_back_button);
-    mCallButton.setOnClickListener(
-        new View.OnClickListener() {
-          @Override
-          public void onClick(View view) {
-            if (TextUtils.isEmpty(mNumber)) {
-              return;
-            }
-            DialerUtils.startActivityWithErrorToast(
-                CallDetailActivity.this,
-                new CallIntentBuilder(getDialableNumber(), CallInitiationType.Type.CALL_DETAILS)
-                    .build());
-          }
-        });
-
-    mEditBeforeCallActionItem = findViewById(R.id.call_detail_action_edit_before_call);
-    mEditBeforeCallActionItem.setOnClickListener(this);
-    mReportActionItem = findViewById(R.id.call_detail_action_report);
-    mReportActionItem.setOnClickListener(this);
-
-    mCopyNumberActionItem = findViewById(R.id.call_detail_action_copy);
-    mCopyNumberActionItem.setOnClickListener(this);
-
-    if (getIntent().getBooleanExtra(EXTRA_FROM_NOTIFICATION, false)) {
-      closeSystemDialogs();
-    }
-
-    Logger.get(this).logScreenView(ScreenEvent.Type.CALL_DETAILS, this);
-  }
-
-  @Override
-  public void onResume() {
-    super.onResume();
-    mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
-    getCallDetails();
-  }
-
-  @Override
-  public boolean dispatchTouchEvent(MotionEvent ev) {
-    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-      TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY());
-    }
-    return super.dispatchTouchEvent(ev);
-  }
-
-  public void getCallDetails() {
-    CallLogAsyncTaskUtil.getCallDetails(this, mCallLogAsyncTaskListener, getCallLogEntryUris());
-  }
-
-  /**
-   * Returns the list of URIs to show.
-   *
-   * <p>There are two ways the URIs can be provided to the activity: as the data on the intent, or
-   * as a list of ids in the call log added as an extra on the URI.
-   *
-   * <p>If both are available, the data on the intent takes precedence.
-   */
-  private Uri[] getCallLogEntryUris() {
-    final Uri uri = getIntent().getData();
-    if (uri != null) {
-      // If there is a data on the intent, it takes precedence over the extra.
-      return new Uri[] {uri};
-    }
-    final long[] ids = getIntent().getLongArrayExtra(EXTRA_CALL_LOG_IDS);
-    final int numIds = ids == null ? 0 : ids.length;
-    final Uri[] uris = new Uri[numIds];
-    for (int index = 0; index < numIds; ++index) {
-      uris[index] =
-          ContentUris.withAppendedId(
-              TelecomUtil.getCallLogUri(CallDetailActivity.this), ids[index]);
-    }
-    return uris;
-  }
-
-  @Override
-  public boolean onCreateOptionsMenu(Menu menu) {
-    final MenuItem deleteMenuItem =
-        menu.add(
-            Menu.NONE, R.id.call_detail_delete_menu_item, Menu.NONE, R.string.call_details_delete);
-    deleteMenuItem.setIcon(R.drawable.ic_delete_24dp);
-    deleteMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
-    deleteMenuItem.setOnMenuItemClickListener(this);
-
-    return super.onCreateOptionsMenu(menu);
-  }
-
-  @Override
-  public boolean onMenuItemClick(MenuItem item) {
-    if (item.getItemId() == R.id.call_detail_delete_menu_item) {
-      Logger.get(mContext).logImpression(DialerImpression.Type.USER_DELETED_CALL_LOG_ITEM);
-      if (hasVoicemail()) {
-        CallLogAsyncTaskUtil.deleteVoicemail(this, mVoicemailUri, mCallLogAsyncTaskListener);
-      } else {
-        final StringBuilder callIds = new StringBuilder();
-        for (Uri callUri : getCallLogEntryUris()) {
-          if (callIds.length() != 0) {
-            callIds.append(",");
-          }
-          callIds.append(ContentUris.parseId(callUri));
-        }
-        CallLogAsyncTaskUtil.deleteCalls(this, callIds.toString(), mCallLogAsyncTaskListener);
-      }
-    }
-    return true;
-  }
-
-  @Override
-  public void onClick(View view) {
-    int resId = view.getId();
-    if (resId == R.id.call_detail_action_copy) {
-      ClipboardUtils.copyText(mContext, null, mNumber, true);
-    } else if (resId == R.id.call_detail_action_edit_before_call) {
-      Intent dialIntent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(getDialableNumber()));
-      DialerUtils.startActivityWithErrorToast(mContext, dialIntent);
-    } else {
-      Assert.fail("Unexpected onClick event from " + view);
-    }
-  }
-
-  // Loads and displays the contact photo.
-  private void updateContactPhoto(boolean isSpam) {
-    if (mDetails == null) {
-      return;
-    }
-
-    mQuickContactBadge.assignContactUri(mDetails.contactUri);
-    final String displayName =
-        TextUtils.isEmpty(mDetails.namePrimary)
-            ? mDetails.displayNumber
-            : mDetails.namePrimary.toString();
-    mQuickContactBadge.setContentDescription(
-        mResources.getString(R.string.description_contact_details, displayName));
-
-    final boolean isVoicemailNumber =
-        PhoneNumberHelper.isVoicemailNumber(mContext, mDetails.accountHandle, mNumber);
-    if (isSpam) {
-      mQuickContactBadge.setImageDrawable(mContext.getDrawable(R.drawable.blocked_contact));
-      return;
-    }
-
-    final boolean isBusiness = mContactInfoHelper.isBusiness(mDetails.sourceType);
-    int contactType = ContactPhotoManager.TYPE_DEFAULT;
-    if (isVoicemailNumber) {
-      contactType = ContactPhotoManager.TYPE_VOICEMAIL;
-    } else if (isBusiness) {
-      contactType = ContactPhotoManager.TYPE_BUSINESS;
-    }
-
-    final String lookupKey =
-        mDetails.contactUri == null ? null : UriUtils.getLookupKeyFromUri(mDetails.contactUri);
-
-    final DefaultImageRequest request =
-        new DefaultImageRequest(displayName, lookupKey, contactType, true /* isCircular */);
-
-    mContactPhotoManager.loadDirectoryPhoto(
-        mQuickContactBadge,
-        mDetails.photoUri,
-        false /* darkTheme */,
-        true /* isCircular */,
-        request);
-  }
-
-  private void closeSystemDialogs() {
-    sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
-  }
-
-  private String getDialableNumber() {
-    return mNumber + mPostDialDigits;
-  }
-
-  public boolean hasVoicemail() {
-    return mVoicemailUri != null;
-  }
-
-  private static boolean hasIncomingCalls(PhoneCallDetails[] details) {
-    for (int i = 0; i < details.length; i++) {
-      if (details[i].hasIncomingCalls()) {
-        return true;
-      }
-    }
-    return false;
-  }
-}
diff --git a/java/com/android/dialer/app/DialerApplication.java b/java/com/android/dialer/app/DialerApplication.java
deleted file mode 100644
index 3b97921..0000000
--- a/java/com/android/dialer/app/DialerApplication.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.app;
-
-import android.app.Application;
-import android.os.Trace;
-import android.preference.PreferenceManager;
-import com.android.dialer.blocking.BlockedNumbersAutoMigrator;
-import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
-import com.android.dialer.enrichedcall.EnrichedCallManager;
-import com.android.dialer.inject.ApplicationModule;
-import com.android.dialer.inject.DaggerDialerAppComponent;
-import com.android.dialer.inject.DialerAppComponent;
-
-public class DialerApplication extends Application implements EnrichedCallManager.Factory {
-
-  private static final String TAG = "DialerApplication";
-
-  private volatile DialerAppComponent component;
-
-  @Override
-  public void onCreate() {
-    Trace.beginSection(TAG + " onCreate");
-    super.onCreate();
-    new BlockedNumbersAutoMigrator(
-            this,
-            PreferenceManager.getDefaultSharedPreferences(this),
-            new FilteredNumberAsyncQueryHandler(this))
-        .autoMigrate();
-    Trace.endSection();
-  }
-
-  @Override
-  public EnrichedCallManager getEnrichedCallManager() {
-    return component().enrichedCallManager();
-  }
-
-  protected DialerAppComponent buildApplicationComponent() {
-    return DaggerDialerAppComponent.builder()
-        .applicationModule(new ApplicationModule(this))
-        .build();
-  }
-
-  /**
-   * Returns the application component.
-   *
-   * <p>A single Component is created per application instance. Note that it won't be instantiated
-   * until it's first requested, but guarantees that only one will ever be created.
-   */
-  private final DialerAppComponent component() {
-    // Double-check idiom for lazy initialization
-    DialerAppComponent result = component;
-    if (result == null) {
-      synchronized (this) {
-        result = component;
-        if (result == null) {
-          component = result = buildApplicationComponent();
-        }
-      }
-    }
-    return result;
-  }
-}
diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java
index 4c57cda..b283776 100644
--- a/java/com/android/dialer/app/DialtactsActivity.java
+++ b/java/com/android/dialer/app/DialtactsActivity.java
@@ -63,15 +63,14 @@
 import com.android.contacts.common.dialog.ClearFrequentsDialog;
 import com.android.contacts.common.list.OnPhoneNumberPickerActionListener;
 import com.android.contacts.common.list.PhoneNumberListAdapter;
-import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery;
 import com.android.contacts.common.list.PhoneNumberPickerFragment.CursorReranker;
 import com.android.contacts.common.list.PhoneNumberPickerFragment.OnLoadFinishedListener;
 import com.android.contacts.common.widget.FloatingActionButtonController;
 import com.android.dialer.animation.AnimUtils;
 import com.android.dialer.animation.AnimationListenerAdapter;
+import com.android.dialer.app.calllog.CallLogActivity;
 import com.android.dialer.app.calllog.CallLogFragment;
 import com.android.dialer.app.calllog.CallLogNotificationsService;
-import com.android.dialer.app.calllog.ClearCallLogDialog;
 import com.android.dialer.app.dialpad.DialpadFragment;
 import com.android.dialer.app.list.DragDropController;
 import com.android.dialer.app.list.ListsFragment;
@@ -85,6 +84,7 @@
 import com.android.dialer.app.settings.DialerSettingsActivity;
 import com.android.dialer.app.widget.ActionBarController;
 import com.android.dialer.app.widget.SearchEditTextLayout;
+import com.android.dialer.callcomposer.CallComposerActivity;
 import com.android.dialer.callintent.CallIntentBuilder;
 import com.android.dialer.callintent.nano.CallSpecificAppData;
 import com.android.dialer.common.Assert;
@@ -101,7 +101,10 @@
 import com.android.dialer.p13n.inference.protocol.P13nRanker.P13nRefreshCompleteListener;
 import com.android.dialer.p13n.logging.P13nLogger;
 import com.android.dialer.p13n.logging.P13nLogging;
+import com.android.dialer.postcall.PostCall;
 import com.android.dialer.proguard.UsedByReflection;
+import com.android.dialer.simulator.Simulator;
+import com.android.dialer.simulator.SimulatorComponent;
 import com.android.dialer.smartdial.SmartDialNameMatcher;
 import com.android.dialer.smartdial.SmartDialPrefix;
 import com.android.dialer.telecom.TelecomUtil;
@@ -124,7 +127,6 @@
         OnListFragmentScrolledListener,
         CallLogFragment.HostInterface,
         DialpadFragment.HostInterface,
-        ListsFragment.HostInterface,
         SpeedDialFragment.HostInterface,
         SearchFragment.HostInterface,
         OnDragDropListener,
@@ -478,6 +480,7 @@
 
   @Override
   protected void onResume() {
+    LogUtil.d("DialtactsActivity.onResume", "");
     Trace.beginSection(TAG + " onResume");
     super.onResume();
 
@@ -490,6 +493,8 @@
     } else if (mShowDialpadOnResume) {
       showDialpadFragment(false);
       mShowDialpadOnResume = false;
+    } else {
+      PostCall.promptUserForMessageIfNecessary(this, mParentLayout);
     }
 
     // If there was a voice query result returned in the {@link #onActivityResult} callback, it
@@ -539,7 +544,7 @@
     }
 
     if (getIntent().getBooleanExtra(EXTRA_CLEAR_NEW_VOICEMAILS, false)) {
-      CallLogNotificationsService.markNewVoicemailsAsOld(this);
+      CallLogNotificationsService.markNewVoicemailsAsOld(this, null);
     }
 
     setSearchBoxHint();
@@ -588,6 +593,7 @@
 
   @Override
   public void onAttachFragment(final Fragment fragment) {
+    LogUtil.d("DialtactsActivity.onAttachFragment", "fragment: %s", fragment);
     if (fragment instanceof DialpadFragment) {
       mDialpadFragment = (DialpadFragment) fragment;
       if (!mIsDialpadShown && !mShowDialpadOnResume) {
@@ -616,7 +622,8 @@
             @MainThread
             public Cursor rerankCursor(Cursor data) {
               Assert.isMainThread();
-              return mP13nRanker.rankCursor(data, PhoneQuery.PHONE_NUMBER);
+              String queryString = searchFragment.getQueryString();
+              return mP13nRanker.rankCursor(data, queryString == null ? 0 : queryString.length());
             }
           });
       searchFragment.addOnLoadFinishedListener(
@@ -674,9 +681,9 @@
     }
 
     int resId = item.getItemId();
-    if (item.getItemId() == R.id.menu_delete_all) {
-      ClearCallLogDialog.show(getFragmentManager());
-      return true;
+    if (resId == R.id.menu_history) {
+      final Intent intent = new Intent(this, CallLogActivity.class);
+      startActivity(intent);
     } else if (resId == R.id.menu_clear_frequents) {
       ClearFrequentsDialog.show(getFragmentManager());
       Logger.get(this).logScreenView(ScreenEvent.Type.CLEAR_FREQUENTS, this);
@@ -691,6 +698,11 @@
 
   @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+    LogUtil.i(
+        "DialtactsActivity.onActivityResult",
+        "requestCode:%d, resultCode:%d",
+        requestCode,
+        resultCode);
     if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) {
       if (resultCode == RESULT_OK) {
         final ArrayList<String> matches =
@@ -701,15 +713,16 @@
           LogUtil.i("DialtactsActivity.onActivityResult", "voice search - nothing heard");
         }
       } else {
-        LogUtil.e("DialtactsActivity.onActivityResult", "voice search failed: " + resultCode);
+        LogUtil.e("DialtactsActivity.onActivityResult", "voice search failed");
       }
     } else if (requestCode == ACTIVITY_REQUEST_CODE_CALL_COMPOSE) {
-      if (resultCode != RESULT_OK) {
+      if (resultCode == RESULT_FIRST_USER) {
         LogUtil.i(
-            "DialtactsActivity.onActivityResult",
-            "returned from call composer, error occurred (resultCode=" + resultCode + ")");
+            "DialtactsActivity.onActivityResult", "returned from call composer, error occurred");
         String message =
-            getString(R.string.call_composer_connection_failed, getString(R.string.share_and_call));
+            getString(
+                R.string.call_composer_connection_failed,
+                data.getStringExtra(CallComposerActivity.KEY_CONTACT_NAME));
         Snackbar.make(mParentLayout, message, Snackbar.LENGTH_LONG).show();
       } else {
         LogUtil.i("DialtactsActivity.onActivityResult", "returned from call composer, no error");
@@ -732,6 +745,7 @@
    * @see #onDialpadShown
    */
   private void showDialpadFragment(boolean animate) {
+    LogUtil.d("DialtactActivity.showDialpadFragment", "animate: %b", animate);
     if (mIsDialpadShown || mStateSaved) {
       return;
     }
@@ -767,6 +781,7 @@
 
   /** Callback from child DialpadFragment when the dialpad is shown. */
   public void onDialpadShown() {
+    LogUtil.d("DialtactsActivity.onDialpadShown", "");
     Assert.isNotNull(mDialpadFragment);
     if (mDialpadFragment.getAnimate()) {
       Assert.isNotNull(mDialpadFragment.getView()).startAnimation(mSlideIn);
@@ -838,12 +853,21 @@
 
   private void updateSearchFragmentPosition() {
     SearchFragment fragment = null;
-    if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) {
+    if (mSmartDialSearchFragment != null) {
       fragment = mSmartDialSearchFragment;
-    } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) {
+    } else if (mRegularSearchFragment != null) {
       fragment = mRegularSearchFragment;
     }
-    if (fragment != null && fragment.isVisible()) {
+    LogUtil.d(
+        "DialtactsActivity.updateSearchFragmentPosition",
+        "fragment: %s, isVisible: %b",
+        fragment,
+        fragment != null && fragment.isVisible());
+    if (fragment != null) {
+      // We need to force animation here even when fragment is not visible since it might not be
+      // visible immediately after screen orientation change and dialpad height would not be
+      // available immediately which is required to update position. By forcing an animation,
+      // position will be updated after a delay by when the dialpad height would be available.
       fragment.updatePosition(true /* animate */);
     }
   }
@@ -858,11 +882,6 @@
     return !TextUtils.isEmpty(mSearchQuery);
   }
 
-  @Override
-  public boolean shouldShowActionBar() {
-    return mListsFragment.shouldShowActionBar();
-  }
-
   private void setNotInSearchUi() {
     mInDialpadSearch = false;
     mInRegularSearch = false;
@@ -1056,7 +1075,8 @@
     }
     // DialtactsActivity will provide the options menu
     fragment.setHasOptionsMenu(false);
-    fragment.setShowEmptyListForNullQuery(true);
+    // Will show empty list if P13nRanker is not enabled. Else, re-ranked list by the ranker.
+    fragment.setShowEmptyListForNullQuery(mP13nRanker.shouldShowEmptyListForNullQuery());
     if (!smartDialSearch) {
       fragment.setQueryString(query);
     }
@@ -1361,11 +1381,6 @@
   }
 
   @Override
-  public ActionBarController getActionBarController() {
-    return mActionBarController;
-  }
-
-  @Override
   public boolean isDialpadShown() {
     return mIsDialpadShown;
   }
@@ -1379,11 +1394,6 @@
   }
 
   @Override
-  public int getActionBarHideOffset() {
-    return getActionBarSafely().getHideOffset();
-  }
-
-  @Override
   public void setActionBarHideOffset(int offset) {
     getActionBarSafely().setHideOffset(offset);
   }
@@ -1461,8 +1471,19 @@
               && mListsFragment.getSpeedDialFragment().hasFrequents()
               && hasContactsPermission);
 
-      menu.findItem(R.id.menu_delete_all)
+      menu.findItem(R.id.menu_history)
           .setVisible(PermissionsUtil.hasPhonePermissions(DialtactsActivity.this));
+
+      Context context = DialtactsActivity.this.getApplicationContext();
+      MenuItem simulatorMenuItem = menu.findItem(R.id.menu_simulator_submenu);
+      Simulator simulator = SimulatorComponent.get(context).getSimulator();
+      if (simulator.shouldShow()) {
+        simulatorMenuItem.setVisible(true);
+        simulatorMenuItem.setActionProvider(simulator.getActionProvider(context));
+      } else {
+        simulatorMenuItem.setVisible(false);
+      }
+
       super.show();
     }
   }
diff --git a/java/com/android/dialer/app/SpecialCharSequenceMgr.java b/java/com/android/dialer/app/SpecialCharSequenceMgr.java
index 2ae1970..712659c 100644
--- a/java/com/android/dialer/app/SpecialCharSequenceMgr.java
+++ b/java/com/android/dialer/app/SpecialCharSequenceMgr.java
@@ -28,15 +28,14 @@
 import android.content.Intent;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.Looper;
 import android.provider.Settings;
 import android.support.annotation.Nullable;
+import android.support.v4.os.BuildCompat;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
-import android.util.Log;
 import android.view.WindowManager;
 import android.widget.EditText;
 import android.widget.Toast;
@@ -46,8 +45,11 @@
 import com.android.contacts.common.util.ContactDisplayUtils;
 import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment;
 import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener;
-import com.android.dialer.app.calllog.PhoneAccountUtils;
+import com.android.dialer.calllogutils.PhoneAccountUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
 import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.oem.MotorolaUtils;
 import com.android.dialer.telecom.TelecomUtil;
 import java.util.ArrayList;
 import java.util.List;
@@ -100,12 +102,19 @@
     //get rid of the separators so that the string gets parsed correctly
     String dialString = PhoneNumberUtils.stripSeparators(input);
 
-    return handleDeviceIdDisplay(context, dialString)
+    if (handleDeviceIdDisplay(context, dialString)
         || handleRegulatoryInfoDisplay(context, dialString)
         || handlePinEntry(context, dialString)
         || handleAdnEntry(context, dialString, textField)
-        || handleSecretCode(context, dialString);
+        || handleSecretCode(context, dialString)) {
+      return true;
+    }
 
+    if (MotorolaUtils.handleSpecialCharSequence(context, input)) {
+      return true;
+    }
+
+    return false;
   }
 
   /**
@@ -114,10 +123,7 @@
    * <p>This should be called when the screen becomes background.
    */
   public static void cleanup() {
-    if (Looper.myLooper() != Looper.getMainLooper()) {
-      Log.wtf(TAG, "cleanup() is called outside the main thread");
-      return;
-    }
+    Assert.isMainThread();
 
     if (sPreviousAdnQueryHandler != null) {
       sPreviousAdnQueryHandler.cancel();
@@ -126,14 +132,21 @@
   }
 
   /**
-   * Handles secret codes to launch arbitrary activities in the form of *#*#<code>#*#*. If a secret
-   * code is encountered an Intent is started with the android_secret_code://<code> URI.
+   * Handles secret codes to launch arbitrary activities in the form of *#*#<code>#*#*.
+   * If a secret code is encountered, an Intent is started with the android_secret_code://<code>
+   * URI.
    *
    * @param context the context to use
    * @param input the text to check for a secret code in
-   * @return true if a secret code was encountered
+   * @return true if a secret code was encountered and intent is sent out
    */
   static boolean handleSecretCode(Context context, String input) {
+    // Must use system service on O+ to avoid using broadcasts, which are not allowed on O+.
+    if (BuildCompat.isAtLeastO()) {
+      return context.getSystemService(TelephonyManager.class).sendDialerCode(input);
+    }
+
+    // System service call is not supported pre-O, so must use a broadcast for N-.
     // Secret codes are in the form *#*#<code>#*#*
     int len = input.length();
     if (len > 8 && input.startsWith("*#*#") && input.endsWith("#*#*")) {
@@ -144,7 +157,6 @@
       context.sendBroadcast(intent);
       return true;
     }
-
     return false;
   }
 
@@ -237,7 +249,7 @@
 
   private static void handleAdnQuery(QueryHandler handler, SimContactQueryCookie cookie, Uri uri) {
     if (handler == null || cookie == null || uri == null) {
-      Log.w(TAG, "queryAdn parameters incorrect");
+      LogUtil.w("SpecialCharSequenceMgr.handleAdnQuery", "queryAdn parameters incorrect");
       return;
     }
 
@@ -325,12 +337,14 @@
 
   private static boolean handleRegulatoryInfoDisplay(Context context, String input) {
     if (input.equals(MMI_REGULATORY_INFO_DISPLAY)) {
-      Log.d(TAG, "handleRegulatoryInfoDisplay() sending intent to settings app");
+      LogUtil.i(
+          "SpecialCharSequenceMgr.handleRegulatoryInfoDisplay", "sending intent to settings app");
       Intent showRegInfoIntent = new Intent(Settings.ACTION_SHOW_REGULATORY_INFO);
       try {
         context.startActivity(showRegInfoIntent);
       } catch (ActivityNotFoundException e) {
-        Log.e(TAG, "startActivity() failed: " + e);
+        LogUtil.e(
+            "SpecialCharSequenceMgr.handleRegulatoryInfoDisplay", "startActivity() failed: ", e);
       }
       return true;
     }
diff --git a/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java b/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java
deleted file mode 100644
index ab6ef73..0000000
--- a/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java
+++ /dev/null
@@ -1,214 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.dialer.app.calllog;
-
-import android.content.Context;
-import android.icu.lang.UCharacter;
-import android.icu.text.BreakIterator;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
-import android.provider.CallLog.Calls;
-import android.text.format.DateUtils;
-import android.text.format.Formatter;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-import android.widget.TextView;
-import com.android.dialer.app.PhoneCallDetails;
-import com.android.dialer.app.R;
-import com.android.dialer.util.CallUtil;
-import com.android.dialer.util.DialerUtils;
-import java.util.ArrayList;
-import java.util.Locale;
-
-/** Adapter for a ListView containing history items from the details of a call. */
-public class CallDetailHistoryAdapter extends BaseAdapter {
-
-  /** Each history item shows the detail of a call. */
-  private static final int VIEW_TYPE_HISTORY_ITEM = 1;
-
-  private final Context mContext;
-  private final LayoutInflater mLayoutInflater;
-  private final CallTypeHelper mCallTypeHelper;
-  private final PhoneCallDetails[] mPhoneCallDetails;
-
-  /** List of items to be concatenated together for duration strings. */
-  private ArrayList<CharSequence> mDurationItems = new ArrayList<>();
-
-  public CallDetailHistoryAdapter(
-      Context context,
-      LayoutInflater layoutInflater,
-      CallTypeHelper callTypeHelper,
-      PhoneCallDetails[] phoneCallDetails) {
-    mContext = context;
-    mLayoutInflater = layoutInflater;
-    mCallTypeHelper = callTypeHelper;
-    mPhoneCallDetails = phoneCallDetails;
-  }
-
-  @Override
-  public boolean isEnabled(int position) {
-    // None of history will be clickable.
-    return false;
-  }
-
-  @Override
-  public int getCount() {
-    return mPhoneCallDetails.length;
-  }
-
-  @Override
-  public Object getItem(int position) {
-    return mPhoneCallDetails[position];
-  }
-
-  @Override
-  public long getItemId(int position) {
-    return position;
-  }
-
-  @Override
-  public int getViewTypeCount() {
-    return 1;
-  }
-
-  @Override
-  public int getItemViewType(int position) {
-    return VIEW_TYPE_HISTORY_ITEM;
-  }
-
-  @Override
-  public View getView(int position, View convertView, ViewGroup parent) {
-    // Make sure we have a valid convertView to start with
-    final View result =
-        convertView == null
-            ? mLayoutInflater.inflate(R.layout.call_detail_history_item, parent, false)
-            : convertView;
-
-    PhoneCallDetails details = mPhoneCallDetails[position];
-    CallTypeIconsView callTypeIconView =
-        (CallTypeIconsView) result.findViewById(R.id.call_type_icon);
-    TextView callTypeTextView = (TextView) result.findViewById(R.id.call_type_text);
-    TextView dateView = (TextView) result.findViewById(R.id.date);
-    TextView durationView = (TextView) result.findViewById(R.id.duration);
-
-    int callType = details.callTypes[0];
-    boolean isVideoCall =
-        (details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO
-            && CallUtil.isVideoEnabled(mContext);
-    boolean isPulledCall =
-        (details.features & Calls.FEATURES_PULLED_EXTERNALLY) == Calls.FEATURES_PULLED_EXTERNALLY;
-
-    callTypeIconView.clear();
-    callTypeIconView.add(callType);
-    callTypeIconView.setShowVideo(isVideoCall);
-    callTypeTextView.setText(mCallTypeHelper.getCallTypeText(callType, isVideoCall, isPulledCall));
-    // Set the date.
-    dateView.setText(formatDate(details.date));
-    // Set the duration
-    if (Calls.VOICEMAIL_TYPE == callType || CallTypeHelper.isMissedCallType(callType)) {
-      durationView.setVisibility(View.GONE);
-    } else {
-      durationView.setVisibility(View.VISIBLE);
-      durationView.setText(formatDurationAndDataUsage(details.duration, details.dataUsage));
-    }
-
-    return result;
-  }
-
-  /**
-   * Formats the provided date into a value suitable for display in the current locale.
-   *
-   * <p>For example, returns a string like "Wednesday, May 25, 2016, 8:02PM" or "Chorshanba, 2016
-   * may 25,20:02".
-   *
-   * <p>For pre-N devices, the returned value may not start with a capital if the local convention
-   * is to not capitalize day names. On N+ devices, the returned value is always capitalized.
-   */
-  private CharSequence formatDate(long callDateMillis) {
-    CharSequence dateValue =
-        DateUtils.formatDateRange(
-            mContext,
-            callDateMillis /* startDate */,
-            callDateMillis /* endDate */,
-            DateUtils.FORMAT_SHOW_TIME
-                | DateUtils.FORMAT_SHOW_DATE
-                | DateUtils.FORMAT_SHOW_WEEKDAY
-                | DateUtils.FORMAT_SHOW_YEAR);
-
-    // We want the beginning of the date string to be capitalized, even if the word at the beginning
-    // of the string is not usually capitalized. For example, "Wednesdsay" in Uzbek is "chorshanba”
-    // (not capitalized). To handle this issue we apply title casing to the start of the sentence so
-    // that "chorshanba, 2016 may 25,20:02" becomes "Chorshanba, 2016 may 25,20:02".
-    //
-    // The ICU library was not available in Android until N, so we can only do this in N+ devices.
-    // Pre-N devices will still see incorrect capitalization in some languages.
-    if (VERSION.SDK_INT < VERSION_CODES.N) {
-      return dateValue;
-    }
-
-    // Using the ICU library is safer than just applying toUpperCase() on the first letter of the
-    // word because in some languages, there can be multiple starting characters which should be
-    // upper-cased together. For example in Dutch "ij" is a digraph in which both letters should be
-    // capitalized together.
-
-    // TITLECASE_NO_LOWERCASE is necessary so that things that are already capitalized like the
-    // month ("May") are not lower-cased as part of the conversion.
-    return UCharacter.toTitleCase(
-        Locale.getDefault(),
-        dateValue.toString(),
-        BreakIterator.getSentenceInstance(),
-        UCharacter.TITLECASE_NO_LOWERCASE);
-  }
-
-  private CharSequence formatDuration(long elapsedSeconds) {
-    long minutes = 0;
-    long seconds = 0;
-
-    if (elapsedSeconds >= 60) {
-      minutes = elapsedSeconds / 60;
-      elapsedSeconds -= minutes * 60;
-      seconds = elapsedSeconds;
-      return mContext.getString(R.string.callDetailsDurationFormat, minutes, seconds);
-    } else {
-      seconds = elapsedSeconds;
-      return mContext.getString(R.string.callDetailsShortDurationFormat, seconds);
-    }
-  }
-
-  /**
-   * Formats a string containing the call duration and the data usage (if specified).
-   *
-   * @param elapsedSeconds Total elapsed seconds.
-   * @param dataUsage Data usage in bytes, or null if not specified.
-   * @return String containing call duration and data usage.
-   */
-  private CharSequence formatDurationAndDataUsage(long elapsedSeconds, Long dataUsage) {
-    CharSequence duration = formatDuration(elapsedSeconds);
-
-    if (dataUsage != null) {
-      mDurationItems.clear();
-      mDurationItems.add(duration);
-      mDurationItems.add(Formatter.formatShortFileSize(mContext, dataUsage));
-
-      return DialerUtils.join(mDurationItems);
-    } else {
-      return duration;
-    }
-  }
-}
diff --git a/java/com/android/dialer/app/calllog/CallLogActivity.java b/java/com/android/dialer/app/calllog/CallLogActivity.java
new file mode 100644
index 0000000..719ab43
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/CallLogActivity.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dialer.app.calllog;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.ActionBar;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.ViewGroup;
+import com.android.contacts.common.list.ViewPagerTabs;
+import com.android.dialer.app.DialtactsActivity;
+import com.android.dialer.app.R;
+import com.android.dialer.database.CallLogQueryHandler;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.ScreenEvent;
+import com.android.dialer.util.TransactionSafeActivity;
+import com.android.dialer.util.ViewUtil;
+
+/** Activity for viewing call history. */
+public class CallLogActivity extends TransactionSafeActivity
+    implements ViewPager.OnPageChangeListener {
+
+  private static final int TAB_INDEX_ALL = 0;
+  private static final int TAB_INDEX_MISSED = 1;
+  private static final int TAB_INDEX_COUNT = 2;
+  private ViewPager mViewPager;
+  private ViewPagerTabs mViewPagerTabs;
+  private ViewPagerAdapter mViewPagerAdapter;
+  private CallLogFragment mAllCallsFragment;
+  private CallLogFragment mMissedCallsFragment;
+  private String[] mTabTitles;
+  private boolean mIsResumed;
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+
+    setContentView(R.layout.call_log_activity);
+    getWindow().setBackgroundDrawable(null);
+
+    final ActionBar actionBar = getSupportActionBar();
+    actionBar.setDisplayShowHomeEnabled(true);
+    actionBar.setDisplayHomeAsUpEnabled(true);
+    actionBar.setDisplayShowTitleEnabled(true);
+    actionBar.setElevation(0);
+
+    int startingTab = TAB_INDEX_ALL;
+    final Intent intent = getIntent();
+    if (intent != null) {
+      final int callType = intent.getIntExtra(CallLog.Calls.EXTRA_CALL_TYPE_FILTER, -1);
+      if (callType == CallLog.Calls.MISSED_TYPE) {
+        startingTab = TAB_INDEX_MISSED;
+      }
+    }
+
+    mTabTitles = new String[TAB_INDEX_COUNT];
+    mTabTitles[0] = getString(R.string.call_log_all_title);
+    mTabTitles[1] = getString(R.string.call_log_missed_title);
+
+    mViewPager = (ViewPager) findViewById(R.id.call_log_pager);
+
+    mViewPagerAdapter = new ViewPagerAdapter(getFragmentManager());
+    mViewPager.setAdapter(mViewPagerAdapter);
+    mViewPager.setOffscreenPageLimit(1);
+    mViewPager.setOnPageChangeListener(this);
+
+    mViewPagerTabs = (ViewPagerTabs) findViewById(R.id.viewpager_header);
+
+    mViewPagerTabs.setViewPager(mViewPager);
+    mViewPager.setCurrentItem(startingTab);
+  }
+
+  @Override
+  protected void onResume() {
+    mIsResumed = true;
+    super.onResume();
+    sendScreenViewForChildFragment(mViewPager.getCurrentItem());
+  }
+
+  @Override
+  protected void onPause() {
+    mIsResumed = false;
+    super.onPause();
+  }
+
+  @Override
+  public boolean onCreateOptionsMenu(Menu menu) {
+    final MenuInflater inflater = getMenuInflater();
+    inflater.inflate(R.menu.call_log_options, menu);
+    return true;
+  }
+
+  @Override
+  public boolean onPrepareOptionsMenu(Menu menu) {
+    final MenuItem itemDeleteAll = menu.findItem(R.id.delete_all);
+    if (mAllCallsFragment != null && itemDeleteAll != null) {
+      // If onPrepareOptionsMenu is called before fragments are loaded, don't do anything.
+      final CallLogAdapter adapter = mAllCallsFragment.getAdapter();
+      itemDeleteAll.setVisible(adapter != null && !adapter.isEmpty());
+    }
+    return true;
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    if (!isSafeToCommitTransactions()) {
+      return true;
+    }
+
+    if (item.getItemId() == android.R.id.home) {
+      final Intent intent = new Intent(this, DialtactsActivity.class);
+      intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+      startActivity(intent);
+      return true;
+    } else if (item.getItemId() == R.id.delete_all) {
+      ClearCallLogDialog.show(getFragmentManager());
+      return true;
+    }
+    return super.onOptionsItemSelected(item);
+  }
+
+  @Override
+  public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+    mViewPagerTabs.onPageScrolled(position, positionOffset, positionOffsetPixels);
+  }
+
+  @Override
+  public void onPageSelected(int position) {
+    if (mIsResumed) {
+      sendScreenViewForChildFragment(position);
+    }
+    mViewPagerTabs.onPageSelected(position);
+  }
+
+  @Override
+  public void onPageScrollStateChanged(int state) {
+    mViewPagerTabs.onPageScrollStateChanged(state);
+  }
+
+  private void sendScreenViewForChildFragment(int position) {
+    Logger.get(this).logScreenView(ScreenEvent.Type.CALL_LOG_FILTER, this);
+  }
+
+  private int getRtlPosition(int position) {
+    if (ViewUtil.isRtl()) {
+      return mViewPagerAdapter.getCount() - 1 - position;
+    }
+    return position;
+  }
+
+  /** Adapter for the view pager. */
+  public class ViewPagerAdapter extends FragmentPagerAdapter {
+
+    public ViewPagerAdapter(FragmentManager fm) {
+      super(fm);
+    }
+
+    @Override
+    public long getItemId(int position) {
+      return getRtlPosition(position);
+    }
+
+    @Override
+    public Fragment getItem(int position) {
+      switch (getRtlPosition(position)) {
+        case TAB_INDEX_ALL:
+          return new CallLogFragment(
+              CallLogQueryHandler.CALL_TYPE_ALL, true /* isCallLogActivity */);
+        case TAB_INDEX_MISSED:
+          return new CallLogFragment(Calls.MISSED_TYPE, true /* isCallLogActivity */);
+      }
+      throw new IllegalStateException("No fragment at position " + position);
+    }
+
+    @Override
+    public Object instantiateItem(ViewGroup container, int position) {
+      final CallLogFragment fragment = (CallLogFragment) super.instantiateItem(container, position);
+      switch (position) {
+        case TAB_INDEX_ALL:
+          mAllCallsFragment = fragment;
+          break;
+        case TAB_INDEX_MISSED:
+          mMissedCallsFragment = fragment;
+          break;
+      }
+      return fragment;
+    }
+
+    @Override
+    public CharSequence getPageTitle(int position) {
+      return mTabTitles[position];
+    }
+
+    @Override
+    public int getCount() {
+      return TAB_INDEX_COUNT;
+    }
+  }
+}
diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java
index ea09a8c..fc5ffbb 100644
--- a/java/com/android/dialer/app/calllog/CallLogAdapter.java
+++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java
@@ -47,7 +47,6 @@
 import com.android.contacts.common.preference.ContactsPreferences;
 import com.android.dialer.app.Bindings;
 import com.android.dialer.app.DialtactsActivity;
-import com.android.dialer.app.PhoneCallDetails;
 import com.android.dialer.app.R;
 import com.android.dialer.app.calllog.CallLogGroupBuilder.GroupCreator;
 import com.android.dialer.app.calllog.calllogcache.CallLogCache;
@@ -55,13 +54,19 @@
 import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
 import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter.OnVoicemailDeletedListener;
 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.calldetails.nano.CallDetailsEntries;
+import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry;
+import com.android.dialer.calllogutils.PhoneAccountUtils;
+import com.android.dialer.calllogutils.PhoneCallDetails;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.AsyncTaskExecutor;
 import com.android.dialer.common.AsyncTaskExecutors;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
 import com.android.dialer.enrichedcall.EnrichedCallManager;
 import com.android.dialer.enrichedcall.EnrichedCallManager.CapabilitiesListener;
+import com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.logging.nano.DialerImpression;
 import com.android.dialer.phonenumbercache.CallLogQuery;
@@ -70,6 +75,7 @@
 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
 import com.android.dialer.spam.Spam;
 import com.android.dialer.util.PermissionsUtil;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -96,7 +102,7 @@
   protected final CallLogCache mCallLogCache;
 
   private final CallFetcher mCallFetcher;
-  private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
+  @NonNull private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
   private final int mActivityType;
 
   /** Instance of helper class for managing views. */
@@ -182,8 +188,6 @@
 
   private boolean mIsSpamEnabled;
 
-  @NonNull private final EnrichedCallManager mEnrichedCallManager;
-
   public CallLogAdapter(
       Activity activity,
       ViewGroup alertContainer,
@@ -191,6 +195,7 @@
       CallLogCache callLogCache,
       ContactInfoCache contactInfoCache,
       VoicemailPlaybackPresenter voicemailPlaybackPresenter,
+      @NonNull FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler,
       int activityType) {
     super();
 
@@ -218,7 +223,7 @@
     mCallLogListItemHelper =
         new CallLogListItemHelper(phoneCallDetailsHelper, resources, mCallLogCache);
     mCallLogGroupBuilder = new CallLogGroupBuilder(this);
-    mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(mActivity);
+    mFilteredNumberAsyncQueryHandler = Assert.isNotNull(filteredNumberAsyncQueryHandler);
 
     mContactsPreferences = new ContactsPreferences(mActivity);
 
@@ -232,7 +237,6 @@
 
     mCallLogAlertManager =
         new CallLogAlertManager(this, LayoutInflater.from(mActivity), alertContainer);
-    mEnrichedCallManager = EnrichedCallManager.Accessor.getInstance(activity.getApplication());
   }
 
   private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) {
@@ -296,7 +300,7 @@
     }
     mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
     mIsSpamEnabled = Spam.get(mActivity).isSpamEnabled();
-    mEnrichedCallManager.registerCapabilitiesListener(this);
+    getEnrichedCallManager().registerCapabilitiesListener(this);
     notifyDataSetChanged();
   }
 
@@ -305,11 +309,11 @@
     for (Uri uri : mHiddenItemUris) {
       CallLogAsyncTaskUtil.deleteVoicemail(mActivity, uri, null);
     }
-    mEnrichedCallManager.unregisterCapabilitiesListener(this);
+    getEnrichedCallManager().unregisterCapabilitiesListener(this);
   }
 
   public void onStop() {
-    mEnrichedCallManager.clearCachedData();
+    getEnrichedCallManager().clearCachedData();
   }
 
   public CallLogAlertManager getAlertManager() {
@@ -420,7 +424,9 @@
     }
     CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder;
     views.isLoaded = false;
-    PhoneCallDetails details = createPhoneCallDetails(c, getGroupSize(position), views);
+    int groupSize = getGroupSize(position);
+    CallDetailsEntries callDetailsEntries = createCallDetailsEntries(c, groupSize);
+    PhoneCallDetails details = createPhoneCallDetails(c, groupSize, views);
     if (mHiddenRowIds.contains(c.getLong(CallLogQuery.ID))) {
       views.callLogEntryView.setVisibility(View.GONE);
       views.dayGroupHeader.setVisibility(View.GONE);
@@ -432,11 +438,14 @@
     if (mCurrentlyExpandedRowId == views.rowId) {
       views.inflateActionViewStub();
     }
-    loadAndRender(views, views.rowId, details);
+    loadAndRender(views, views.rowId, details, callDetailsEntries);
   }
 
   private void loadAndRender(
-      final CallLogListItemViewHolder views, final long rowId, final PhoneCallDetails details) {
+      final CallLogListItemViewHolder views,
+      final long rowId,
+      final PhoneCallDetails details,
+      final CallDetailsEntries callDetailsEntries) {
     // Reset block and spam information since this view could be reused which may contain
     // outdated data.
     views.isSpam = false;
@@ -464,12 +473,33 @@
                       && Spam.get(mActivity)
                           .checkSpamStatusSynchronous(views.number, views.countryIso);
               details.isSpam = views.isSpam;
-              if (isCancelled()) {
-                return false;
+            }
+            if (isCancelled()) {
+              return false;
+            }
+            setCallDetailsEntriesHistoryResults(
+                PhoneNumberUtils.formatNumberToE164(views.number, views.countryIso),
+                callDetailsEntries);
+            views.setDetailedPhoneDetails(callDetailsEntries);
+            return !isCancelled() && loadData(views, rowId, details);
+          }
+
+          private void setCallDetailsEntriesHistoryResults(
+              @Nullable String number, CallDetailsEntries callDetailsEntries) {
+            if (number == null) {
+              return;
+            }
+            Map<CallDetailsEntry, List<HistoryResult>> mappedResults =
+                getEnrichedCallManager().getAllHistoricalData(number, callDetailsEntries);
+            for (CallDetailsEntry entry : callDetailsEntries.entries) {
+              List<HistoryResult> results = mappedResults.get(entry);
+              if (results != null) {
+                entry.historyResults = mappedResults.get(entry).toArray(new HistoryResult[0]);
+                LogUtil.v(
+                    "CallLogAdapter.setCallDetailsEntriesHistoryResults",
+                    "mapped %d results",
+                    entry.historyResults.length);
               }
-              return loadData(views, rowId, details);
-            } else {
-              return loadData(views, rowId, details);
             }
           }
 
@@ -499,9 +529,9 @@
       return false;
     }
 
-    EnrichedCallCapabilities capabilities = mEnrichedCallManager.getCapabilities(e164Number);
+    EnrichedCallCapabilities capabilities = getEnrichedCallManager().getCapabilities(e164Number);
     if (capabilities == null) {
-      mEnrichedCallManager.requestCapabilities(e164Number);
+      getEnrichedCallManager().requestCapabilities(e164Number);
       return false;
     }
     return capabilities.supportsCallComposer();
@@ -562,6 +592,27 @@
     return details;
   }
 
+  @MainThread
+  private static CallDetailsEntries createCallDetailsEntries(Cursor cursor, int count) {
+    Assert.isMainThread();
+    int position = cursor.getPosition();
+    CallDetailsEntries entries = new CallDetailsEntries();
+    entries.entries = new CallDetailsEntry[count];
+    for (int i = 0; i < count; i++) {
+      CallDetailsEntry entry = new CallDetailsEntry();
+      entry.callId = cursor.getLong(CallLogQuery.ID);
+      entry.callType = cursor.getInt(CallLogQuery.CALL_TYPE);
+      entry.dataUsage = cursor.getLong(CallLogQuery.DATA_USAGE);
+      entry.date = cursor.getLong(CallLogQuery.DATE);
+      entry.duration = cursor.getLong(CallLogQuery.DURATION);
+      entry.features |= cursor.getInt(CallLogQuery.FEATURES);
+      entries.entries[i] = entry;
+      cursor.moveToNext();
+    }
+    cursor.moveToPosition(position);
+    return entries;
+  }
+
   /**
    * Load data for call log. Any expensive operation should be put here to avoid blocking main
    * thread. Do NOT put any cursor operation here since it's not thread safe.
@@ -907,6 +958,11 @@
     notifyDataSetChanged();
   }
 
+  @NonNull
+  private EnrichedCallManager getEnrichedCallManager() {
+    return EnrichedCallComponent.get(mActivity).getEnrichedCallManager();
+  }
+
   /** Interface used to initiate a refresh of the content. */
   public interface CallFetcher {
 
diff --git a/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java
index b4e6fc5..2198626 100644
--- a/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java
+++ b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java
@@ -16,37 +16,22 @@
 
 package com.android.dialer.app.calllog;
 
-import android.Manifest.permission;
 import android.annotation.TargetApi;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.database.Cursor;
 import android.net.Uri;
 import android.os.AsyncTask;
-import android.os.Build.VERSION;
 import android.os.Build.VERSION_CODES;
 import android.provider.CallLog;
 import android.provider.VoicemailContract.Voicemails;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.support.v4.content.ContextCompat;
-import android.telecom.PhoneAccountHandle;
 import android.text.TextUtils;
-import com.android.contacts.common.GeoUtil;
-import com.android.dialer.app.PhoneCallDetails;
 import com.android.dialer.common.AsyncTaskExecutor;
 import com.android.dialer.common.AsyncTaskExecutors;
-import com.android.dialer.common.LogUtil;
-import com.android.dialer.phonenumbercache.ContactInfo;
-import com.android.dialer.phonenumbercache.ContactInfoHelper;
-import com.android.dialer.phonenumberutil.PhoneNumberHelper;
-import com.android.dialer.telecom.TelecomUtil;
 import com.android.dialer.util.PermissionsUtil;
-import java.util.ArrayList;
-import java.util.Arrays;
+import com.android.voicemail.VoicemailClient;
 
 @TargetApi(VERSION_CODES.M)
 public class CallLogAsyncTaskUtil {
@@ -58,166 +43,6 @@
     sAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor();
   }
 
-  public static void getCallDetails(
-      @NonNull final Context context,
-      @Nullable final CallLogAsyncTaskListener callLogAsyncTaskListener,
-      @NonNull final Uri... callUris) {
-    if (sAsyncTaskExecutor == null) {
-      initTaskExecutor();
-    }
-
-    sAsyncTaskExecutor.submit(
-        Tasks.GET_CALL_DETAILS,
-        new AsyncTask<Void, Void, PhoneCallDetails[]>() {
-          @Override
-          public PhoneCallDetails[] doInBackground(Void... params) {
-            if (ContextCompat.checkSelfPermission(context, permission.READ_CALL_LOG)
-                != PackageManager.PERMISSION_GRANTED) {
-              LogUtil.w("CallLogAsyncTaskUtil.getCallDetails", "missing READ_CALL_LOG permission");
-              return null;
-            }
-            // TODO: All calls correspond to the same person, so make a single lookup.
-            final int numCalls = callUris.length;
-            PhoneCallDetails[] details = new PhoneCallDetails[numCalls];
-            try {
-              for (int index = 0; index < numCalls; ++index) {
-                details[index] = getPhoneCallDetailsForUri(context, callUris[index]);
-              }
-              return details;
-            } catch (IllegalArgumentException e) {
-              // Something went wrong reading in our primary data.
-              LogUtil.e(
-                  "CallLogAsyncTaskUtil.getCallDetails", "invalid URI starting call details", e);
-              return null;
-            }
-          }
-
-          @Override
-          public void onPostExecute(PhoneCallDetails[] phoneCallDetails) {
-            if (callLogAsyncTaskListener != null) {
-              callLogAsyncTaskListener.onGetCallDetails(phoneCallDetails);
-            }
-          }
-        });
-  }
-
-  /** Return the phone call details for a given call log URI. */
-  private static PhoneCallDetails getPhoneCallDetailsForUri(
-      @NonNull Context context, @NonNull Uri callUri) {
-    Cursor cursor =
-        context
-            .getContentResolver()
-            .query(callUri, CallDetailQuery.CALL_LOG_PROJECTION, null, null, null);
-
-    try {
-      if (cursor == null || !cursor.moveToFirst()) {
-        throw new IllegalArgumentException("Cannot find content: " + callUri);
-      }
-
-      // Read call log.
-      final String countryIso = cursor.getString(CallDetailQuery.COUNTRY_ISO_COLUMN_INDEX);
-      final String number = cursor.getString(CallDetailQuery.NUMBER_COLUMN_INDEX);
-      final String postDialDigits =
-          (VERSION.SDK_INT >= VERSION_CODES.N)
-              ? cursor.getString(CallDetailQuery.POST_DIAL_DIGITS)
-              : "";
-      final String viaNumber =
-          (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallDetailQuery.VIA_NUMBER) : "";
-      final int numberPresentation =
-          cursor.getInt(CallDetailQuery.NUMBER_PRESENTATION_COLUMN_INDEX);
-
-      final PhoneAccountHandle accountHandle =
-          PhoneAccountUtils.getAccount(
-              cursor.getString(CallDetailQuery.ACCOUNT_COMPONENT_NAME),
-              cursor.getString(CallDetailQuery.ACCOUNT_ID));
-
-      // If this is not a regular number, there is no point in looking it up in the contacts.
-      ContactInfoHelper contactInfoHelper =
-          new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context));
-      boolean isVoicemail = PhoneNumberHelper.isVoicemailNumber(context, accountHandle, number);
-      boolean shouldLookupNumber =
-          PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation) && !isVoicemail;
-      ContactInfo info = ContactInfo.EMPTY;
-
-      if (shouldLookupNumber) {
-        ContactInfo lookupInfo = contactInfoHelper.lookupNumber(number, countryIso);
-        info = lookupInfo != null ? lookupInfo : ContactInfo.EMPTY;
-      }
-
-      PhoneCallDetails details = new PhoneCallDetails(number, numberPresentation, postDialDigits);
-      details.updateDisplayNumber(context, info.formattedNumber, isVoicemail);
-
-      details.viaNumber = viaNumber;
-      details.accountHandle = accountHandle;
-      details.contactUri = info.lookupUri;
-      details.namePrimary = info.name;
-      details.nameAlternative = info.nameAlternative;
-      details.numberType = info.type;
-      details.numberLabel = info.label;
-      details.photoUri = info.photoUri;
-      details.sourceType = info.sourceType;
-      details.objectId = info.objectId;
-
-      details.callTypes = new int[] {cursor.getInt(CallDetailQuery.CALL_TYPE_COLUMN_INDEX)};
-      details.date = cursor.getLong(CallDetailQuery.DATE_COLUMN_INDEX);
-      details.duration = cursor.getLong(CallDetailQuery.DURATION_COLUMN_INDEX);
-      details.features = cursor.getInt(CallDetailQuery.FEATURES);
-      details.geocode = cursor.getString(CallDetailQuery.GEOCODED_LOCATION_COLUMN_INDEX);
-      details.transcription = cursor.getString(CallDetailQuery.TRANSCRIPTION_COLUMN_INDEX);
-
-      details.countryIso =
-          !TextUtils.isEmpty(countryIso) ? countryIso : GeoUtil.getCurrentCountryIso(context);
-
-      if (!cursor.isNull(CallDetailQuery.DATA_USAGE)) {
-        details.dataUsage = cursor.getLong(CallDetailQuery.DATA_USAGE);
-      }
-
-      return details;
-    } finally {
-      if (cursor != null) {
-        cursor.close();
-      }
-    }
-  }
-
-  /**
-   * Delete specified calls from the call log.
-   *
-   * @param context The context.
-   * @param callIds String of the callIds to delete from the call log, delimited by commas (",").
-   * @param callLogAsyncTaskListener The listener to invoke after the entries have been deleted.
-   */
-  public static void deleteCalls(
-      @NonNull final Context context,
-      final String callIds,
-      @Nullable final CallLogAsyncTaskListener callLogAsyncTaskListener) {
-    if (sAsyncTaskExecutor == null) {
-      initTaskExecutor();
-    }
-
-    sAsyncTaskExecutor.submit(
-        Tasks.DELETE_CALL,
-        new AsyncTask<Void, Void, Void>() {
-          @Override
-          public Void doInBackground(Void... params) {
-            context
-                .getContentResolver()
-                .delete(
-                    TelecomUtil.getCallLogUri(context),
-                    CallLog.Calls._ID + " IN (" + callIds + ")",
-                    null);
-            return null;
-          }
-
-          @Override
-          public void onPostExecute(Void result) {
-            if (callLogAsyncTaskListener != null) {
-              callLogAsyncTaskListener.onDeleteCall();
-            }
-          }
-        });
-  }
-
   public static void markVoicemailAsRead(
       @NonNull final Context context, @NonNull final Uri voicemailUri) {
     if (sAsyncTaskExecutor == null) {
@@ -235,6 +60,8 @@
                 .getContentResolver()
                 .update(voicemailUri, values, Voicemails.IS_READ + " = 0", null);
 
+            uploadVoicemailLocalChangesToServer(context);
+
             Intent intent = new Intent(context, CallLogNotificationsService.class);
             intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
             context.startService(intent);
@@ -256,7 +83,12 @@
         new AsyncTask<Void, Void, Void>() {
           @Override
           public Void doInBackground(Void... params) {
-            context.getContentResolver().delete(voicemailUri, null, null);
+            ContentValues values = new ContentValues();
+            values.put(Voicemails.DELETED, "1");
+            context.getContentResolver().update(voicemailUri, values, null, null);
+            // TODO(b/35440541): check which source package is changed. Don't need
+            // to upload changes on foreign voicemails, they will get a PROVIDER_CHANGED
+            uploadVoicemailLocalChangesToServer(context);
             return null;
           }
 
@@ -305,11 +137,6 @@
         });
   }
 
-  @VisibleForTesting
-  public static void resetForTest() {
-    sAsyncTaskExecutor = null;
-  }
-
   /** The enumeration of {@link AsyncTask} objects used in this class. */
   public enum Tasks {
     DELETE_VOICEMAIL,
@@ -321,56 +148,12 @@
   }
 
   public interface CallLogAsyncTaskListener {
-
-    void onDeleteCall();
-
     void onDeleteVoicemail();
-
-    void onGetCallDetails(PhoneCallDetails[] details);
   }
 
-  private static final class CallDetailQuery {
-
-    public static final String[] CALL_LOG_PROJECTION;
-    static final int DATE_COLUMN_INDEX = 0;
-    static final int DURATION_COLUMN_INDEX = 1;
-    static final int NUMBER_COLUMN_INDEX = 2;
-    static final int CALL_TYPE_COLUMN_INDEX = 3;
-    static final int COUNTRY_ISO_COLUMN_INDEX = 4;
-    static final int GEOCODED_LOCATION_COLUMN_INDEX = 5;
-    static final int NUMBER_PRESENTATION_COLUMN_INDEX = 6;
-    static final int ACCOUNT_COMPONENT_NAME = 7;
-    static final int ACCOUNT_ID = 8;
-    static final int FEATURES = 9;
-    static final int DATA_USAGE = 10;
-    static final int TRANSCRIPTION_COLUMN_INDEX = 11;
-    static final int POST_DIAL_DIGITS = 12;
-    static final int VIA_NUMBER = 13;
-    private static final String[] CALL_LOG_PROJECTION_INTERNAL =
-        new String[] {
-          CallLog.Calls.DATE,
-          CallLog.Calls.DURATION,
-          CallLog.Calls.NUMBER,
-          CallLog.Calls.TYPE,
-          CallLog.Calls.COUNTRY_ISO,
-          CallLog.Calls.GEOCODED_LOCATION,
-          CallLog.Calls.NUMBER_PRESENTATION,
-          CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME,
-          CallLog.Calls.PHONE_ACCOUNT_ID,
-          CallLog.Calls.FEATURES,
-          CallLog.Calls.DATA_USAGE,
-          CallLog.Calls.TRANSCRIPTION
-        };
-
-    static {
-      ArrayList<String> projectionList = new ArrayList<>();
-      projectionList.addAll(Arrays.asList(CALL_LOG_PROJECTION_INTERNAL));
-      if (VERSION.SDK_INT >= VERSION_CODES.N) {
-        projectionList.add(CallLog.Calls.POST_DIAL_DIGITS);
-        projectionList.add(CallLog.Calls.VIA_NUMBER);
-      }
-      projectionList.trimToSize();
-      CALL_LOG_PROJECTION = projectionList.toArray(new String[projectionList.size()]);
-    }
+  private static void uploadVoicemailLocalChangesToServer(Context context) {
+    Intent intent = new Intent(VoicemailClient.ACTION_UPLOAD);
+    intent.setPackage(context.getPackageName());
+    context.sendBroadcast(intent);
   }
 }
diff --git a/java/com/android/dialer/app/calllog/CallLogFragment.java b/java/com/android/dialer/app/calllog/CallLogFragment.java
index 1ae68cd..4abef34 100644
--- a/java/com/android/dialer/app/calllog/CallLogFragment.java
+++ b/java/com/android/dialer/app/calllog/CallLogFragment.java
@@ -53,6 +53,7 @@
 import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
 import com.android.dialer.app.widget.EmptyContentView;
 import com.android.dialer.app.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.database.CallLogQueryHandler;
 import com.android.dialer.phonenumbercache.ContactInfoHelper;
@@ -70,9 +71,17 @@
         FragmentCompat.OnRequestPermissionsResultCallback,
         CallLogModalAlertManager.Listener {
   private static final String KEY_FILTER_TYPE = "filter_type";
+  private static final String KEY_LOG_LIMIT = "log_limit";
+  private static final String KEY_DATE_LIMIT = "date_limit";
+  private static final String KEY_IS_CALL_LOG_ACTIVITY = "is_call_log_activity";
   private static final String KEY_HAS_READ_CALL_LOG_PERMISSION = "has_read_call_log_permission";
   private static final String KEY_REFRESH_DATA_REQUIRED = "refresh_data_required";
 
+  // No limit specified for the number of logs to show; use the CallLogQueryHandler's default.
+  private static final int NO_LOG_LIMIT = -1;
+  // No date-based filtering.
+  private static final int NO_DATE_LIMIT = 0;
+
   private static final int READ_CALL_LOG_PERMISSION_REQUEST_CODE = 1;
 
   private static final int EVENT_UPDATE_DISPLAY = 1;
@@ -104,8 +113,17 @@
   // Exactly same variable is in Fragment as a package private.
   private boolean mMenuVisible = true;
   // Default to all calls.
-  protected int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
-
+  private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
+  // Log limit - if no limit is specified, then the default in {@link CallLogQueryHandler}
+  // will be used.
+  private int mLogLimit = NO_LOG_LIMIT;
+  // Date limit (in millis since epoch) - when non-zero, only calls which occurred on or after
+  // the date filter are included.  If zero, no date-based filtering occurs.
+  private long mDateLimit = NO_DATE_LIMIT;
+  /*
+   * True if this instance of the CallLogFragment shown in the CallLogActivity.
+   */
+  private boolean mIsCallLogActivity = false;
   private final Handler mDisplayUpdateHandler =
       new Handler() {
         @Override
@@ -121,6 +139,48 @@
   protected CallLogModalAlertManager mModalAlertManager;
   private ViewGroup mModalAlertView;
 
+  public CallLogFragment() {
+    this(CallLogQueryHandler.CALL_TYPE_ALL, NO_LOG_LIMIT);
+  }
+
+  public CallLogFragment(int filterType) {
+    this(filterType, NO_LOG_LIMIT);
+  }
+
+  public CallLogFragment(int filterType, boolean isCallLogActivity) {
+    this(filterType, NO_LOG_LIMIT);
+    mIsCallLogActivity = isCallLogActivity;
+  }
+
+  public CallLogFragment(int filterType, int logLimit) {
+    this(filterType, logLimit, NO_DATE_LIMIT);
+  }
+
+  /**
+   * Creates a call log fragment, filtering to include only calls of the desired type, occurring
+   * after the specified date.
+   *
+   * @param filterType type of calls to include.
+   * @param dateLimit limits results to calls occurring on or after the specified date.
+   */
+  public CallLogFragment(int filterType, long dateLimit) {
+    this(filterType, NO_LOG_LIMIT, dateLimit);
+  }
+
+  /**
+   * Creates a call log fragment, filtering to include only calls of the desired type, occurring
+   * after the specified date. Also provides a means to limit the number of results returned.
+   *
+   * @param filterType type of calls to include.
+   * @param logLimit limits the number of results to return.
+   * @param dateLimit limits results to calls occurring on or after the specified date.
+   */
+  public CallLogFragment(int filterType, int logLimit, long dateLimit) {
+    mCallTypeFilter = filterType;
+    mLogLimit = logLimit;
+    mDateLimit = dateLimit;
+  }
+
   @Override
   public void onCreate(Bundle state) {
     LogUtil.d("CallLogFragment.onCreate", toString());
@@ -128,13 +188,16 @@
     mRefreshDataRequired = true;
     if (state != null) {
       mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter);
+      mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit);
+      mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit);
+      mIsCallLogActivity = state.getBoolean(KEY_IS_CALL_LOG_ACTIVITY, mIsCallLogActivity);
       mHasReadCallLogPermission = state.getBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, false);
       mRefreshDataRequired = state.getBoolean(KEY_REFRESH_DATA_REQUIRED, mRefreshDataRequired);
     }
 
     final Activity activity = getActivity();
     final ContentResolver resolver = activity.getContentResolver();
-    mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this);
+    mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this, mLogLimit);
     mKeyguardManager = (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE);
     resolver.registerContentObserver(CallLog.CONTENT_URI, true, mCallLogObserver);
     resolver.registerContentObserver(
@@ -226,7 +289,10 @@
   }
 
   protected void setupData() {
-    int activityType = CallLogAdapter.ACTIVITY_TYPE_DIALTACTS;
+    int activityType =
+        mIsCallLogActivity
+            ? CallLogAdapter.ACTIVITY_TYPE_CALL_LOG
+            : CallLogAdapter.ACTIVITY_TYPE_DIALTACTS;
     String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
 
     mContactInfoCache =
@@ -244,6 +310,7 @@
                 CallLogCache.getCallLogCache(getActivity()),
                 mContactInfoCache,
                 getVoicemailPlaybackPresenter(),
+                new FilteredNumberAsyncQueryHandler(getActivity()),
                 activityType);
     mRecyclerView.setAdapter(mAdapter);
     fetchCalls();
@@ -324,6 +391,9 @@
   public void onSaveInstanceState(Bundle outState) {
     super.onSaveInstanceState(outState);
     outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter);
+    outState.putInt(KEY_LOG_LIMIT, mLogLimit);
+    outState.putLong(KEY_DATE_LIMIT, mDateLimit);
+    outState.putBoolean(KEY_IS_CALL_LOG_ACTIVITY, mIsCallLogActivity);
     outState.putBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, mHasReadCallLogPermission);
     outState.putBoolean(KEY_REFRESH_DATA_REQUIRED, mRefreshDataRequired);
 
@@ -334,8 +404,10 @@
 
   @Override
   public void fetchCalls() {
-    mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
-    ((ListsFragment) getParentFragment()).updateTabUnreadCounts();
+    mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
+    if (!mIsCallLogActivity) {
+      ((ListsFragment) getParentFragment()).updateTabUnreadCounts();
+    }
   }
 
   private void updateEmptyMessage(int filterType) {
@@ -366,7 +438,9 @@
             "Unexpected filter type in CallLogFragment: " + filterType);
     }
     mEmptyListView.setDescription(messageId);
-    if (filterType == CallLogQueryHandler.CALL_TYPE_ALL) {
+    if (mIsCallLogActivity) {
+      mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL);
+    } else if (filterType == CallLogQueryHandler.CALL_TYPE_ALL) {
       mEmptyListView.setActionLabel(R.string.call_log_all_empty_action);
     }
   }
@@ -420,7 +494,7 @@
     if (mKeyguardManager != null
         && !mKeyguardManager.inKeyguardRestrictedInputMode()
         && mCallTypeFilter == Calls.VOICEMAIL_TYPE) {
-      CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
+      CallLogNotificationsQueryHelper.updateVoicemailNotifications(getActivity());
     }
   }
 
@@ -434,7 +508,8 @@
     if (!PermissionsUtil.hasPermission(activity, READ_CALL_LOG)) {
       FragmentCompat.requestPermissions(
           this, new String[] {READ_CALL_LOG}, READ_CALL_LOG_PERMISSION_REQUEST_CODE);
-    } else {
+    } else if (!mIsCallLogActivity) {
+      // Show dialpad if we are not in the call log activity.
       ((HostInterface) activity).showDialpad();
     }
   }
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemHelper.java b/java/com/android/dialer/app/calllog/CallLogListItemHelper.java
index ea2119c..a5df8cc 100644
--- a/java/com/android/dialer/app/calllog/CallLogListItemHelper.java
+++ b/java/com/android/dialer/app/calllog/CallLogListItemHelper.java
@@ -21,18 +21,16 @@
 import android.support.annotation.WorkerThread;
 import android.text.SpannableStringBuilder;
 import android.text.TextUtils;
-import android.util.Log;
-import com.android.dialer.app.PhoneCallDetails;
 import com.android.dialer.app.R;
 import com.android.dialer.app.calllog.calllogcache.CallLogCache;
+import com.android.dialer.calllogutils.PhoneCallDetails;
 import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
 import com.android.dialer.compat.AppCompatConstants;
 
 /** Helper class to fill in the views of a call log entry. */
 /* package */ class CallLogListItemHelper {
 
-  private static final String TAG = "CallLogListItemHelper";
-
   /** Helper for populating the details of a phone call. */
   private final PhoneCallDetailsHelper mPhoneCallDetailsHelper;
   /** Resources to look up strings. */
@@ -105,7 +103,9 @@
    */
   public void setActionContentDescriptions(CallLogListItemViewHolder views) {
     if (views.nameOrNumber == null) {
-      Log.e(TAG, "setActionContentDescriptions; name or number is null.");
+      LogUtil.e(
+          "CallLogListItemHelper.setActionContentDescriptions",
+          "setActionContentDescriptions; name or number is null.");
     }
 
     // Calling expandTemplate with a null parameter will cause a NullPointerException.
@@ -170,7 +170,6 @@
    *
    * <p>2 calls. Answered call from John Doe mobile 1 hour ago.
    *
-   * @param context The application context.
    * @param details Details of call.
    * @return Return call action description.
    */
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
index 6abd360..8a2d944 100644
--- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
+++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
@@ -25,9 +25,11 @@
 import android.provider.CallLog;
 import android.provider.CallLog.Calls;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.annotation.VisibleForTesting;
 import android.support.v7.widget.CardView;
 import android.support.v7.widget.RecyclerView;
 import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
 import android.telephony.PhoneNumberUtils;
 import android.text.BidiFormatter;
 import android.text.TextDirectionHeuristics;
@@ -56,7 +58,7 @@
 import com.android.dialer.blocking.FilteredNumbersUtil;
 import com.android.dialer.callcomposer.CallComposerActivity;
 import com.android.dialer.callcomposer.nano.CallComposerContact;
-import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.calldetails.nano.CallDetailsEntries;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.compat.CompatUtils;
 import com.android.dialer.logging.Logger;
@@ -78,8 +80,6 @@
     implements View.OnClickListener,
         MenuItem.OnMenuItemClickListener,
         View.OnCreateContextMenuListener {
-  private static final String CONFIG_SHARE_VOICEMAIL_ALLOWED = "share_voicemail_allowed";
-
   /** The root view of the call log list item */
   public final View rootView;
   /** The quick contact badge for the contact. */
@@ -201,6 +201,7 @@
   public boolean isAttachedToWindow;
 
   public AsyncTask<Void, Void, ?> asyncTask;
+  private CallDetailsEntries callDetailsEntries;
 
   private CallLogListItemViewHolder(
       Context context,
@@ -549,10 +550,6 @@
     }
   }
 
-  private static boolean isShareVoicemailAllowed(Context context) {
-    return ConfigProviderBindings.get(context).getBoolean(CONFIG_SHARE_VOICEMAIL_ALLOWED, true);
-  }
-
   /**
    * Binds text titles, click handlers and intents to the voicemail, details and callback action
    * buttons.
@@ -577,13 +574,14 @@
       unblockView.setVisibility(View.GONE);
       reportNotSpamView.setVisibility(View.GONE);
 
-      if (isShareVoicemailAllowed(mContext)) {
-        sendVoicemailButtonView.setVisibility(View.VISIBLE);
-      }
       voicemailPlaybackView.setVisibility(View.VISIBLE);
       Uri uri = Uri.parse(voicemailUri);
       mVoicemailPlaybackPresenter.setPlaybackView(
-          voicemailPlaybackView, rowId, uri, mVoicemailPrimaryActionButtonClicked);
+          voicemailPlaybackView,
+          rowId,
+          uri,
+          mVoicemailPrimaryActionButtonClicked,
+          sendVoicemailButtonView);
       mVoicemailPrimaryActionButtonClicked = false;
       CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri);
       return;
@@ -621,14 +619,14 @@
         && mVoicemailPlaybackPresenter != null
         && !TextUtils.isEmpty(voicemailUri)) {
       voicemailPlaybackView.setVisibility(View.VISIBLE);
-      if (isShareVoicemailAllowed(mContext)) {
-        Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_VISIBLE);
-        sendVoicemailButtonView.setVisibility(View.VISIBLE);
-      }
 
       Uri uri = Uri.parse(voicemailUri);
       mVoicemailPlaybackPresenter.setPlaybackView(
-          voicemailPlaybackView, rowId, uri, mVoicemailPrimaryActionButtonClicked);
+          voicemailPlaybackView,
+          rowId,
+          uri,
+          mVoicemailPrimaryActionButtonClicked,
+          sendVoicemailButtonView);
       mVoicemailPrimaryActionButtonClicked = false;
       CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri);
     } else {
@@ -640,7 +638,8 @@
       detailsButtonView.setVisibility(View.GONE);
     } else {
       detailsButtonView.setVisibility(View.VISIBLE);
-      detailsButtonView.setTag(IntentProvider.getCallDetailIntentProvider(rowId, callIds, null));
+      detailsButtonView.setTag(
+          IntentProvider.getCallDetailIntentProvider(callDetailsEntries, buildContact()));
     }
 
     boolean isBlockedOrSpam = blockId != null || (isSpamFeatureEnabled && isSpam);
@@ -776,6 +775,8 @@
       contactType = ContactPhotoManager.TYPE_VOICEMAIL;
     } else if (isBusiness) {
       contactType = ContactPhotoManager.TYPE_BUSINESS;
+    } else if (numberPresentation == TelecomManager.PRESENTATION_RESTRICTED) {
+      contactType = ContactPhotoManager.TYPE_GENERIC_AVATAR;
     }
 
     final String lookupKey =
@@ -854,20 +855,9 @@
     } else if (view.getId() == R.id.call_compose_action) {
       LogUtil.i("CallLogListItemViewHolder.onClick", "share and call pressed");
       Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SHARE_AND_CALL);
-      CallComposerContact contact = new CallComposerContact();
-      contact.photoId = info.photoId;
-      contact.photoUri = info.photoUri == null ? null : info.photoUri.toString();
-      contact.contactUri = info.lookupUri == null ? null : info.lookupUri.toString();
-      contact.nameOrNumber = (String) nameOrNumber;
-      contact.isBusiness = isBusiness;
-      contact.number = number;
-      /* second line of contact view. */
-      contact.displayNumber = TextUtils.isEmpty(info.name) ? null : displayNumber;
-      /* phone number type (e.g. mobile) in second line of contact view */
-      contact.numberLabel = numberType;
       Activity activity = (Activity) mContext;
       activity.startActivityForResult(
-          CallComposerActivity.newIntent(activity, contact),
+          CallComposerActivity.newIntent(activity, buildContact()),
           DialtactsActivity.ACTIVITY_REQUEST_CODE_CALL_COMPOSE);
     } else if (view.getId() == R.id.share_voicemail) {
       Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_PRESSED);
@@ -885,6 +875,21 @@
     }
   }
 
+  private CallComposerContact buildContact() {
+    CallComposerContact contact = new CallComposerContact();
+    contact.photoId = info.photoId;
+    contact.photoUri = info.photoUri == null ? null : info.photoUri.toString();
+    contact.contactUri = info.lookupUri == null ? null : info.lookupUri.toString();
+    contact.nameOrNumber = (String) nameOrNumber;
+    contact.isBusiness = isBusiness;
+    contact.number = number;
+    /* second line of contact view. */
+    contact.displayNumber = TextUtils.isEmpty(info.name) ? null : displayNumber;
+    /* phone number type (e.g. mobile) in second line of contact view */
+    contact.numberLabel = numberType;
+    return contact;
+  }
+
   private void logCallLogAction(int id) {
     if (id == R.id.send_message_action) {
       Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SEND_MESSAGE);
@@ -931,6 +936,15 @@
     }
   }
 
+  public void setDetailedPhoneDetails(CallDetailsEntries callDetailsEntries) {
+    this.callDetailsEntries = callDetailsEntries;
+  }
+
+  @VisibleForTesting
+  public CallDetailsEntries getDetailedPhoneDetails() {
+    return callDetailsEntries;
+  }
+
   public interface OnClickListener {
 
     void onBlockReportSpam(
diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java b/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java
similarity index 79%
rename from java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java
rename to java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java
index 8f664d1..f12837e 100644
--- a/java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java
+++ b/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java
@@ -18,37 +18,41 @@
 
 import android.Manifest;
 import android.annotation.TargetApi;
+import android.app.NotificationManager;
 import android.content.ContentResolver;
 import android.content.ContentUris;
+import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Build.VERSION_CODES;
 import android.provider.CallLog.Calls;
 import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.support.v4.os.UserManagerCompat;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
-import android.util.Log;
 import com.android.contacts.common.GeoUtil;
 import com.android.dialer.app.R;
+import com.android.dialer.calllogutils.PhoneNumberDisplayUtil;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.notification.GroupedNotificationUtil;
 import com.android.dialer.phonenumbercache.ContactInfo;
 import com.android.dialer.phonenumbercache.ContactInfoHelper;
-import com.android.dialer.telecom.TelecomUtil;
 import com.android.dialer.util.PermissionsUtil;
 import java.util.ArrayList;
 import java.util.List;
 
 /** Helper class operating on call log notifications. */
-public class CallLogNotificationsHelper {
+public class CallLogNotificationsQueryHelper {
 
   private static final String TAG = "CallLogNotifHelper";
-  private static CallLogNotificationsHelper sInstance;
   private final Context mContext;
   private final NewCallsQuery mNewCallsQuery;
   private final ContactInfoHelper mContactInfoHelper;
   private final String mCurrentCountryIso;
 
-  CallLogNotificationsHelper(
+  CallLogNotificationsQueryHelper(
       Context context,
       NewCallsQuery newCallsQuery,
       ContactInfoHelper contactInfoHelper,
@@ -59,29 +63,60 @@
     mCurrentCountryIso = countryIso;
   }
 
-  /** Returns the singleton instance of the {@link CallLogNotificationsHelper}. */
-  public static CallLogNotificationsHelper getInstance(Context context) {
-    if (sInstance == null) {
-      ContentResolver contentResolver = context.getContentResolver();
-      String countryIso = GeoUtil.getCurrentCountryIso(context);
-      sInstance =
-          new CallLogNotificationsHelper(
-              context,
-              createNewCallsQuery(context, contentResolver),
-              new ContactInfoHelper(context, countryIso),
-              countryIso);
-    }
-    return sInstance;
+  /** Returns an instance of {@link CallLogNotificationsQueryHelper}. */
+  public static CallLogNotificationsQueryHelper getInstance(Context context) {
+    ContentResolver contentResolver = context.getContentResolver();
+    String countryIso = GeoUtil.getCurrentCountryIso(context);
+    return new CallLogNotificationsQueryHelper(
+        context,
+        createNewCallsQuery(context, contentResolver),
+        new ContactInfoHelper(context, countryIso),
+        countryIso);
   }
 
-  /** Removes the missed call notifications. */
-  public static void removeMissedCallNotifications(Context context) {
-    TelecomUtil.cancelMissedCallsNotification(context);
+  /**
+   * Removes the missed call notifications and marks calls as read. If a callUri is provided, only
+   * that call is marked as read.
+   */
+  @WorkerThread
+  public static void removeMissedCallNotifications(Context context, @Nullable Uri callUri) {
+    // Call log is only accessible when unlocked. If that's the case, clear the list of
+    // new missed calls from the call log.
+    if (UserManagerCompat.isUserUnlocked(context) && PermissionsUtil.hasPhonePermissions(context)) {
+      ContentValues values = new ContentValues();
+      values.put(Calls.NEW, 0);
+      values.put(Calls.IS_READ, 1);
+      StringBuilder where = new StringBuilder();
+      where.append(Calls.NEW);
+      where.append(" = 1 AND ");
+      where.append(Calls.TYPE);
+      where.append(" = ?");
+      try {
+        context
+            .getContentResolver()
+            .update(
+                callUri == null ? Calls.CONTENT_URI : callUri,
+                values,
+                where.toString(),
+                new String[] {Integer.toString(Calls.MISSED_TYPE)});
+      } catch (IllegalArgumentException e) {
+        LogUtil.e(
+            "CallLogNotificationsQueryHelper.removeMissedCallNotifications",
+            "contacts provider update command failed",
+            e);
+      }
+    }
+
+    GroupedNotificationUtil.removeNotification(
+        context.getSystemService(NotificationManager.class),
+        callUri != null ? callUri.toString() : null,
+        R.id.notification_missed_call,
+        MissedCallNotifier.NOTIFICATION_TAG);
   }
 
   /** Update the voice mail notifications. */
   public static void updateVoicemailNotifications(Context context) {
-    CallLogNotificationsService.updateVoicemailNotifications(context, null);
+    CallLogNotificationsService.updateVoicemailNotifications(context);
   }
 
   /** Create a new instance of {@link NewCallsQuery}. */
@@ -251,7 +286,7 @@
     @TargetApi(VERSION_CODES.M)
     public List<NewCall> query(int type) {
       if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CALL_LOG)) {
-        Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup.");
+        LogUtil.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup.");
         return null;
       }
       final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
@@ -272,7 +307,7 @@
         }
         return newCalls;
       } catch (RuntimeException e) {
-        Log.w(TAG, "Exception when querying Contacts Provider for calls lookup");
+        LogUtil.w(TAG, "Exception when querying Contacts Provider for calls lookup");
         return null;
       }
     }
diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java
index 8205281..b0d48ee 100644
--- a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java
+++ b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
+import android.support.annotation.Nullable;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.telecom.TelecomUtil;
 import com.android.dialer.util.PermissionsUtil;
@@ -44,21 +45,10 @@
   /** Action to mark all the new voicemails as old. */
   public static final String ACTION_MARK_NEW_VOICEMAILS_AS_OLD =
       "com.android.dialer.calllog.ACTION_MARK_NEW_VOICEMAILS_AS_OLD";
-  /**
-   * Action to update voicemail notifications.
-   *
-   * <p>May include an optional extra {@link #EXTRA_NEW_VOICEMAIL_URI}.
-   */
+  /** Action to update voicemail notifications. */
   public static final String ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS =
       "com.android.dialer.calllog.UPDATE_VOICEMAIL_NOTIFICATIONS";
   /**
-   * Extra to included with {@link #ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS} to identify the new
-   * voicemail that triggered an update.
-   *
-   * <p>It must be a {@link Uri}.
-   */
-  public static final String EXTRA_NEW_VOICEMAIL_URI = "NEW_VOICEMAIL_URI";
-  /**
    * Action to update the missed call notifications.
    *
    * <p>Includes optional extras {@link #EXTRA_MISSED_CALL_NUMBER} and {@link
@@ -66,9 +56,15 @@
    */
   public static final String ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS =
       "com.android.dialer.calllog.UPDATE_MISSED_CALL_NOTIFICATIONS";
+
   /** Action to mark all the new missed calls as old. */
   public static final String ACTION_MARK_NEW_MISSED_CALLS_AS_OLD =
       "com.android.dialer.calllog.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD";
+
+  /** Action to update missed call notifications with a post call note. */
+  public static final String ACTION_INCOMING_POST_CALL =
+      "com.android.dialer.calllog.INCOMING_POST_CALL";
+
   /** Action to call back a missed call. */
   public static final String ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION =
       "com.android.dialer.calllog.CALL_BACK_FROM_MISSED_CALL_NOTIFICATION";
@@ -92,6 +88,21 @@
    */
   public static final String EXTRA_MISSED_CALL_COUNT = "MISSED_CALL_COUNT";
 
+  /**
+   * Extra to be included with {@link #ACTION_INCOMING_POST_CALL} to represent a post call note.
+   *
+   * <p>It must be a {@link String}
+   */
+  public static final String EXTRA_POST_CALL_NOTE = "POST_CALL_NOTE";
+
+  /**
+   * Extra to be included with {@link #ACTION_INCOMING_POST_CALL} to represent the phone number the
+   * post call note came from.
+   *
+   * <p>It must be a {@link String}
+   */
+  public static final String EXTRA_POST_CALL_NUMBER = "POST_CALL_NUMBER";
+
   public static final int UNKNOWN_MISSED_CALL_COUNT = -1;
   private VoicemailQueryHandler mVoicemailQueryHandler;
 
@@ -103,10 +114,8 @@
    * Updates notifications for any new voicemails.
    *
    * @param context a valid context.
-   * @param voicemailUri The uri pointing to the voicemail to update the notification for. If {@code
-   *     null}, then notifications for all new voicemails will be updated.
    */
-  public static void updateVoicemailNotifications(Context context, Uri voicemailUri) {
+  public static void updateVoicemailNotifications(Context context) {
     if (!TelecomUtil.isDefaultDialer(context)) {
       LogUtil.i(
           "CallLogNotificationsService.updateVoicemailNotifications",
@@ -116,10 +125,6 @@
     if (TelecomUtil.hasReadWriteVoicemailPermissions(context)) {
       Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
       serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS);
-      // If voicemailUri is null, then notifications for all voicemails will be updated.
-      if (voicemailUri != null) {
-        serviceIntent.putExtra(CallLogNotificationsService.EXTRA_NEW_VOICEMAIL_URI, voicemailUri);
-      }
       context.startService(serviceIntent);
     }
   }
@@ -139,9 +144,25 @@
     context.startService(serviceIntent);
   }
 
-  public static void markNewVoicemailsAsOld(Context context) {
+  public static void insertPostCallNote(Context context, String number, String postCallNote) {
+    Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
+    serviceIntent.setAction(ACTION_INCOMING_POST_CALL);
+    serviceIntent.putExtra(EXTRA_POST_CALL_NUMBER, number);
+    serviceIntent.putExtra(EXTRA_POST_CALL_NOTE, postCallNote);
+    context.startService(serviceIntent);
+  }
+
+  public static void markNewVoicemailsAsOld(Context context, @Nullable Uri voicemailUri) {
     Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
     serviceIntent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
+    serviceIntent.setData(voicemailUri);
+    context.startService(serviceIntent);
+  }
+
+  public static void markNewMissedCallsAsOld(Context context, @Nullable Uri callUri) {
+    Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
+    serviceIntent.setAction(ACTION_MARK_NEW_MISSED_CALLS_AS_OLD);
+    serviceIntent.setData(callUri);
     context.startService(serviceIntent);
   }
 
@@ -172,11 +193,10 @@
         if (mVoicemailQueryHandler == null) {
           mVoicemailQueryHandler = new VoicemailQueryHandler(this, getContentResolver());
         }
-        mVoicemailQueryHandler.markNewVoicemailsAsOld();
+        mVoicemailQueryHandler.markNewVoicemailsAsOld(intent.getData());
         break;
       case ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS:
-        Uri voicemailUri = intent.getParcelableExtra(EXTRA_NEW_VOICEMAIL_URI);
-        DefaultVoicemailNotifier.getInstance(this).updateNotification(voicemailUri);
+        DefaultVoicemailNotifier.getInstance(this).updateNotification();
         break;
       case ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS:
         int count = intent.getIntExtra(EXTRA_MISSED_CALL_COUNT, UNKNOWN_MISSED_CALL_COUNT);
@@ -184,16 +204,24 @@
         MissedCallNotifier.getInstance(this).updateMissedCallNotification(count, number);
         updateBadgeCount(this, count);
         break;
+      case ACTION_INCOMING_POST_CALL:
+        String note = intent.getStringExtra(EXTRA_POST_CALL_NOTE);
+        String phoneNumber = intent.getStringExtra(EXTRA_POST_CALL_NUMBER);
+        MissedCallNotifier.getInstance(this).insertPostCallNotification(phoneNumber, note);
+        break;
       case ACTION_MARK_NEW_MISSED_CALLS_AS_OLD:
-        CallLogNotificationsHelper.removeMissedCallNotifications(this);
+        CallLogNotificationsQueryHelper.removeMissedCallNotifications(this, intent.getData());
+        TelecomUtil.cancelMissedCallsNotification(this);
         break;
       case ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION:
         MissedCallNotifier.getInstance(this)
-            .callBackFromMissedCall(intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER));
+            .callBackFromMissedCall(
+                intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER), intent.getData());
         break;
       case ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION:
         MissedCallNotifier.getInstance(this)
-            .sendSmsFromMissedCall(intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER));
+            .sendSmsFromMissedCall(
+                intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER), intent.getData());
         break;
       default:
         LogUtil.d("CallLogNotificationsService.onHandleIntent", "could not handle: " + intent);
diff --git a/java/com/android/dialer/app/calllog/CallLogReceiver.java b/java/com/android/dialer/app/calllog/CallLogReceiver.java
index a781b08..8fd1502 100644
--- a/java/com/android/dialer/app/calllog/CallLogReceiver.java
+++ b/java/com/android/dialer/app/calllog/CallLogReceiver.java
@@ -38,9 +38,9 @@
   public void onReceive(Context context, Intent intent) {
     if (VoicemailContract.ACTION_NEW_VOICEMAIL.equals(intent.getAction())) {
       checkVoicemailStatus(context);
-      CallLogNotificationsService.updateVoicemailNotifications(context, intent.getData());
+      CallLogNotificationsService.updateVoicemailNotifications(context);
     } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
-      CallLogNotificationsService.updateVoicemailNotifications(context, null);
+      CallLogNotificationsService.updateVoicemailNotifications(context);
     } else {
       LogUtil.w("CallLogReceiver.onReceive", "could not handle: " + intent);
     }
diff --git a/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java b/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java
index 651a0cc..cc1dc4f 100644
--- a/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java
+++ b/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java
@@ -19,31 +19,33 @@
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.ContentUris;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
+import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Build.VERSION;
 import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
 import android.support.v4.util.Pair;
-import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.ArrayMap;
-import android.util.Log;
 import com.android.contacts.common.compat.TelephonyManagerCompat;
 import com.android.contacts.common.util.ContactDisplayUtils;
 import com.android.dialer.app.DialtactsActivity;
 import com.android.dialer.app.R;
-import com.android.dialer.app.calllog.CallLogNotificationsHelper.NewCall;
+import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall;
+import com.android.dialer.app.contactinfo.ContactPhotoLoader;
 import com.android.dialer.app.list.ListsFragment;
 import com.android.dialer.blocking.FilteredNumbersUtil;
-import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
+import com.android.dialer.phonenumbercache.ContactInfo;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -54,26 +56,23 @@
   public static final String TAG = "VoicemailNotifier";
 
   /** The tag used to identify notifications from this class. */
-  private static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier";
+  static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier";
   /** The identifier of the notification of new voicemails. */
-  private static final int NOTIFICATION_ID = 1;
+  private static final int NOTIFICATION_ID = R.id.notification_voicemail;
 
-  /** The singleton instance of {@link DefaultVoicemailNotifier}. */
-  private static DefaultVoicemailNotifier sInstance;
+  private final Context context;
+  private final CallLogNotificationsQueryHelper queryHelper;
 
-  private final Context mContext;
-
-  private DefaultVoicemailNotifier(Context context) {
-    mContext = context;
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  DefaultVoicemailNotifier(Context context, CallLogNotificationsQueryHelper queryHelper) {
+    this.context = context;
+    this.queryHelper = queryHelper;
   }
 
-  /** Returns the singleton instance of the {@link DefaultVoicemailNotifier}. */
+  /** Returns an instance of {@link DefaultVoicemailNotifier}. */
   public static DefaultVoicemailNotifier getInstance(Context context) {
-    if (sInstance == null) {
-      ContentResolver contentResolver = context.getContentResolver();
-      sInstance = new DefaultVoicemailNotifier(context);
-    }
-    return sInstance;
+    return new DefaultVoicemailNotifier(
+        context, CallLogNotificationsQueryHelper.getInstance(context));
   }
 
   /**
@@ -84,34 +83,23 @@
    *
    * <p>It is not safe to call this method from the main thread.
    */
-  public void updateNotification(Uri newCallUri) {
+  public void updateNotification() {
     // Lookup the list of new voicemails to include in the notification.
-    // TODO: Move this into a service, to avoid holding the receiver up.
-    final List<NewCall> newCalls =
-        CallLogNotificationsHelper.getInstance(mContext).getNewVoicemails();
+    final List<NewCall> newCalls = queryHelper.getNewVoicemails();
 
     if (newCalls == null) {
       // Query failed, just return.
       return;
     }
 
-    if (newCalls.isEmpty()) {
-      // No voicemails to notify about: clear the notification.
-      getNotificationManager().cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
-      return;
-    }
-
-    Resources resources = mContext.getResources();
+    Resources resources = context.getResources();
 
     // This represents a list of names to include in the notification.
     String callers = null;
 
     // Maps each number into a name: if a number is in the map, it has already left a more
     // recent voicemail.
-    final Map<String, String> names = new ArrayMap<>();
-
-    // Determine the call corresponding to the new voicemail we have to notify about.
-    NewCall callToNotify = null;
+    final Map<String, ContactInfo> contactInfos = new ArrayMap<>();
 
     // Iterate over the new voicemails to determine all the information above.
     Iterator<NewCall> itr = newCalls.iterator();
@@ -120,95 +108,64 @@
 
       // Skip notifying for numbers which are blocked.
       if (FilteredNumbersUtil.shouldBlockVoicemail(
-          mContext, newCall.number, newCall.countryIso, newCall.dateMs)) {
+          context, newCall.number, newCall.countryIso, newCall.dateMs)) {
         itr.remove();
 
         // Delete the voicemail.
-        mContext.getContentResolver().delete(newCall.voicemailUri, null, null);
+        context.getContentResolver().delete(newCall.voicemailUri, null, null);
         continue;
       }
 
       // Check if we already know the name associated with this number.
-      String name = names.get(newCall.number);
-      if (name == null) {
-        name =
-            CallLogNotificationsHelper.getInstance(mContext)
-                .getName(newCall.number, newCall.numberPresentation, newCall.countryIso);
-        names.put(newCall.number, name);
+      ContactInfo contactInfo = contactInfos.get(newCall.number);
+      if (contactInfo == null) {
+        contactInfo =
+            queryHelper.getContactInfo(
+                newCall.number, newCall.numberPresentation, newCall.countryIso);
+        contactInfos.put(newCall.number, contactInfo);
         // This is a new caller. Add it to the back of the list of callers.
         if (TextUtils.isEmpty(callers)) {
-          callers = name;
+          callers = contactInfo.name;
         } else {
           callers =
-              resources.getString(R.string.notification_voicemail_callers_list, callers, name);
+              resources.getString(
+                  R.string.notification_voicemail_callers_list, callers, contactInfo.name);
         }
       }
-      // Check if this is the new call we need to notify about.
-      if (newCallUri != null
-          && newCall.voicemailUri != null
-          && ContentUris.parseId(newCallUri) == ContentUris.parseId(newCall.voicemailUri)) {
-        callToNotify = newCall;
-      }
     }
 
-    // All the potential new voicemails have been removed, e.g. if they were spam.
     if (newCalls.isEmpty()) {
+      // No voicemails to notify about: clear the notification.
+      CallLogNotificationsService.markNewVoicemailsAsOld(context, null);
       return;
     }
 
-    // If there is only one voicemail, set its transcription as the "long text".
-    String transcription = null;
-    if (newCalls.size() == 1) {
-      transcription = newCalls.get(0).transcription;
-    }
-
-    if (newCallUri != null && callToNotify == null) {
-      Log.e(TAG, "The new call could not be found in the call log: " + newCallUri);
-    }
-
-    // Determine the title of the notification and the icon for it.
-    final String title =
-        resources.getQuantityString(
-            R.plurals.notification_voicemail_title, newCalls.size(), newCalls.size());
-    // TODO: Use the photo of contact if all calls are from the same person.
-    final int icon = android.R.drawable.stat_notify_voicemail;
-
-    Pair<Uri, Integer> info = getNotificationInfo(callToNotify);
-
-    Notification.Builder notificationBuilder =
-        new Notification.Builder(mContext)
-            .setSmallIcon(icon)
-            .setContentTitle(title)
+    Notification.Builder groupSummary =
+        createNotificationBuilder()
+            .setContentTitle(
+                resources.getQuantityString(
+                    R.plurals.notification_voicemail_title, newCalls.size(), newCalls.size()))
             .setContentText(callers)
-            .setColor(resources.getColor(R.color.dialer_theme_color))
-            .setSound(info.first)
-            .setDefaults(info.second)
-            .setDeleteIntent(createMarkNewVoicemailsAsOldIntent())
-            .setAutoCancel(true);
+            .setDeleteIntent(createMarkNewVoicemailsAsOldIntent(null))
+            .setGroupSummary(true)
+            .setContentIntent(newVoicemailIntent(null));
 
-    if (!TextUtils.isEmpty(transcription)) {
-      notificationBuilder.setStyle(new Notification.BigTextStyle().bigText(transcription));
+    NotificationChannelManager.applyChannel(
+        groupSummary,
+        context,
+        Channel.VOICEMAIL,
+        PhoneAccountHandles.getAccount(context, newCalls.get(0)));
+
+    LogUtil.i(TAG, "Creating voicemail notification");
+    getNotificationManager().notify(NOTIFICATION_TAG, NOTIFICATION_ID, groupSummary.build());
+
+    for (NewCall voicemail : newCalls) {
+      getNotificationManager()
+          .notify(
+              voicemail.voicemailUri.toString(),
+              NOTIFICATION_ID,
+              createNotificationForVoicemail(voicemail, contactInfos));
     }
-
-    // Determine the intent to fire when the notification is clicked on.
-    final Intent contentIntent;
-    // Open the call log.
-    contentIntent = DialtactsActivity.getShowTabIntent(mContext, ListsFragment.TAB_INDEX_VOICEMAIL);
-    contentIntent.putExtra(DialtactsActivity.EXTRA_CLEAR_NEW_VOICEMAILS, true);
-    notificationBuilder.setContentIntent(
-        PendingIntent.getActivity(mContext, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
-    // The text to show in the ticker, describing the new event.
-    if (callToNotify != null) {
-      CharSequence msg =
-          ContactDisplayUtils.getTtsSpannedPhoneNumber(
-              resources,
-              R.string.notification_new_voicemail_ticker,
-              names.get(callToNotify.number));
-      notificationBuilder.setTicker(msg);
-    }
-    Log.i(TAG, "Creating voicemail notification");
-    getNotificationManager().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build());
   }
 
   /**
@@ -216,30 +173,15 @@
    * for the given call.
    */
   private Pair<Uri, Integer> getNotificationInfo(@Nullable NewCall callToNotify) {
-    Log.v(TAG, "getNotificationInfo");
+    LogUtil.v(TAG, "getNotificationInfo");
     if (callToNotify == null) {
-      Log.i(TAG, "callToNotify == null");
+      LogUtil.i(TAG, "callToNotify == null");
       return new Pair<>(null, 0);
     }
-    PhoneAccountHandle accountHandle;
-    if (callToNotify.accountComponentName == null || callToNotify.accountId == null) {
-      Log.v(TAG, "accountComponentName == null || callToNotify.accountId == null");
-      accountHandle = TelecomUtil.getDefaultOutgoingPhoneAccount(mContext, PhoneAccount.SCHEME_TEL);
-      if (accountHandle == null) {
-        Log.i(TAG, "No default phone account found, using default notification ringtone");
-        return new Pair<>(null, Notification.DEFAULT_ALL);
-      }
-
-    } else {
-      accountHandle =
-          new PhoneAccountHandle(
-              ComponentName.unflattenFromString(callToNotify.accountComponentName),
-              callToNotify.accountId);
-    }
-    if (accountHandle.getComponentName() != null) {
-      Log.v(TAG, "PhoneAccountHandle.ComponentInfo:" + accountHandle.getComponentName());
-    } else {
-      Log.i(TAG, "PhoneAccountHandle.ComponentInfo: null");
+    PhoneAccountHandle accountHandle = PhoneAccountHandles.getAccount(context, callToNotify);
+    if (accountHandle == null) {
+      LogUtil.i(TAG, "No default phone account found, using default notification ringtone");
+      return new Pair<>(null, Notification.DEFAULT_ALL);
     }
     return new Pair<>(
         TelephonyManagerCompat.getVoicemailRingtoneUri(getTelephonyManager(), accountHandle),
@@ -257,17 +199,79 @@
   }
 
   /** Creates a pending intent that marks all new voicemails as old. */
-  private PendingIntent createMarkNewVoicemailsAsOldIntent() {
-    Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+  private PendingIntent createMarkNewVoicemailsAsOldIntent(@Nullable Uri voicemailUri) {
+    Intent intent = new Intent(context, CallLogNotificationsService.class);
     intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
-    return PendingIntent.getService(mContext, 0, intent, 0);
+    intent.setData(voicemailUri);
+    return PendingIntent.getService(context, 0, intent, 0);
   }
 
   private NotificationManager getNotificationManager() {
-    return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+    return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
   }
 
   private TelephonyManager getTelephonyManager() {
-    return (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+    return (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+  }
+
+  private Notification createNotificationForVoicemail(
+      @NonNull NewCall voicemail, @NonNull Map<String, ContactInfo> contactInfos) {
+    Pair<Uri, Integer> notificationInfo = getNotificationInfo(voicemail);
+    ContactInfo contactInfo = contactInfos.get(voicemail.number);
+
+    Notification.Builder notificationBuilder =
+        createNotificationBuilder()
+            .setContentTitle(
+                context
+                    .getResources()
+                    .getQuantityString(R.plurals.notification_voicemail_title, 1, 1))
+            .setContentText(
+                ContactDisplayUtils.getTtsSpannedPhoneNumber(
+                    context.getResources(),
+                    R.string.notification_new_voicemail_ticker,
+                    contactInfo.name))
+            .setWhen(voicemail.dateMs)
+            .setSound(notificationInfo.first)
+            .setDefaults(notificationInfo.second)
+            .setDeleteIntent(createMarkNewVoicemailsAsOldIntent(voicemail.voicemailUri));
+
+    NotificationChannelManager.applyChannel(
+        notificationBuilder,
+        context,
+        Channel.VOICEMAIL,
+        PhoneAccountHandles.getAccount(context, voicemail));
+
+    ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo);
+    Bitmap photoIcon = loader.loadPhotoIcon();
+    if (photoIcon != null) {
+      notificationBuilder.setLargeIcon(photoIcon);
+    }
+
+    if (!TextUtils.isEmpty(voicemail.transcription)) {
+      notificationBuilder.setStyle(
+          new Notification.BigTextStyle().bigText(voicemail.transcription));
+    }
+    notificationBuilder.setContentIntent(newVoicemailIntent(voicemail));
+
+    return notificationBuilder.build();
+  }
+
+  private Notification.Builder createNotificationBuilder() {
+    return new Notification.Builder(context)
+        .setSmallIcon(android.R.drawable.stat_notify_voicemail)
+        .setColor(context.getColor(R.color.dialer_theme_color))
+        .setGroup(NOTIFICATION_TAG)
+        .setOnlyAlertOnce(true)
+        .setAutoCancel(true);
+  }
+
+  private PendingIntent newVoicemailIntent(@Nullable NewCall voicemail) {
+    Intent intent = DialtactsActivity.getShowTabIntent(context, ListsFragment.TAB_INDEX_VOICEMAIL);
+    // TODO (b/35486204): scroll to this voicemail
+    if (voicemail != null) {
+      intent.setData(voicemail.voicemailUri);
+    }
+    intent.putExtra(DialtactsActivity.EXTRA_CLEAR_NEW_VOICEMAILS, true);
+    return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
   }
 }
diff --git a/java/com/android/dialer/app/calllog/IntentProvider.java b/java/com/android/dialer/app/calllog/IntentProvider.java
index 879ac35..c53e3ec 100644
--- a/java/com/android/dialer/app/calllog/IntentProvider.java
+++ b/java/com/android/dialer/app/calllog/IntentProvider.java
@@ -16,7 +16,6 @@
 
 package com.android.dialer.app.calllog;
 
-import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
@@ -25,10 +24,11 @@
 import android.telecom.PhoneAccountHandle;
 import com.android.contacts.common.model.Contact;
 import com.android.contacts.common.model.ContactLoader;
-import com.android.dialer.app.CallDetailActivity;
+import com.android.dialer.callcomposer.nano.CallComposerContact;
+import com.android.dialer.calldetails.CallDetailsActivity;
+import com.android.dialer.calldetails.nano.CallDetailsEntries;
 import com.android.dialer.callintent.CallIntentBuilder;
 import com.android.dialer.callintent.nano.CallInitiationType;
-import com.android.dialer.telecom.TelecomUtil;
 import com.android.dialer.util.CallUtil;
 import com.android.dialer.util.IntentUtil;
 import java.util.ArrayList;
@@ -97,29 +97,16 @@
   /**
    * Retrieves the call details intent provider for an entry in the call log.
    *
-   * @param id The call ID of the first call in the call group.
-   * @param extraIds The call ID of the other calls grouped together with the call.
-   * @param voicemailUri If call log entry is for a voicemail, the voicemail URI.
+   * @param callDetailsEntries The call details of the other calls grouped together with the call.
+   * @param contact The contact with which this call details intent pertains to.
    * @return The call details intent provider.
    */
   public static IntentProvider getCallDetailIntentProvider(
-      final long id, final long[] extraIds, final String voicemailUri) {
+      CallDetailsEntries callDetailsEntries, CallComposerContact contact) {
     return new IntentProvider() {
       @Override
       public Intent getIntent(Context context) {
-        Intent intent = new Intent(context, CallDetailActivity.class);
-        // Check if the first item is a voicemail.
-        if (voicemailUri != null) {
-          intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, Uri.parse(voicemailUri));
-        }
-
-        if (extraIds != null && extraIds.length > 0) {
-          intent.putExtra(CallDetailActivity.EXTRA_CALL_LOG_IDS, extraIds);
-        } else {
-          // If there is a single item, use the direct URI for it.
-          intent.setData(ContentUris.withAppendedId(TelecomUtil.getCallLogUri(context), id));
-        }
-        return intent;
+        return CallDetailsActivity.newInstance(context, callDetailsEntries, contact);
       }
     };
   }
diff --git a/java/com/android/dialer/app/calllog/MissedCallNotifier.java b/java/com/android/dialer/app/calllog/MissedCallNotifier.java
index 2fa3dae..5b56616 100644
--- a/java/com/android/dialer/app/calllog/MissedCallNotifier.java
+++ b/java/com/android/dialer/app/calllog/MissedCallNotifier.java
@@ -16,16 +16,20 @@
 package com.android.dialer.app.calllog;
 
 import android.app.Notification;
+import android.app.Notification.Builder;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
-import android.os.AsyncTask;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
 import android.provider.CallLog.Calls;
+import android.service.notification.StatusBarNotification;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
 import android.support.v4.os.UserManagerCompat;
 import android.text.BidiFormatter;
 import android.text.TextDirectionHeuristics;
@@ -34,109 +38,117 @@
 import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
 import com.android.dialer.app.DialtactsActivity;
 import com.android.dialer.app.R;
-import com.android.dialer.app.calllog.CallLogNotificationsHelper.NewCall;
+import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall;
 import com.android.dialer.app.contactinfo.ContactPhotoLoader;
 import com.android.dialer.app.list.ListsFragment;
 import com.android.dialer.callintent.CallIntentBuilder;
 import com.android.dialer.callintent.nano.CallInitiationType;
-import com.android.dialer.common.ConfigProviderBindings;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
 import com.android.dialer.phonenumbercache.ContactInfo;
 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.telecom.TelecomUtil;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.IntentUtil;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 /** Creates a notification for calls that the user missed (neither answered nor rejected). */
 public class MissedCallNotifier {
 
   /** The tag used to identify notifications from this class. */
-  private static final String NOTIFICATION_TAG = "MissedCallNotifier";
+  static final String NOTIFICATION_TAG = "MissedCallNotifier";
   /** The identifier of the notification of new missed calls. */
-  private static final int NOTIFICATION_ID = 1;
+  private static final int NOTIFICATION_ID = R.id.notification_missed_call;
 
-  private static MissedCallNotifier sInstance;
-  private Context mContext;
-  private CallLogNotificationsHelper mCalllogNotificationsHelper;
+  private final Context context;
+  private final CallLogNotificationsQueryHelper callLogNotificationsQueryHelper;
 
   @VisibleForTesting
-  MissedCallNotifier(Context context, CallLogNotificationsHelper callLogNotificationsHelper) {
-    mContext = context;
-    mCalllogNotificationsHelper = callLogNotificationsHelper;
+  MissedCallNotifier(
+      Context context, CallLogNotificationsQueryHelper callLogNotificationsQueryHelper) {
+    this.context = context;
+    this.callLogNotificationsQueryHelper = callLogNotificationsQueryHelper;
   }
 
-  /** Returns the singleton instance of the {@link MissedCallNotifier}. */
+  /** Returns an instance of {@link MissedCallNotifier}. */
   public static MissedCallNotifier getInstance(Context context) {
-    if (sInstance == null) {
-      CallLogNotificationsHelper callLogNotificationsHelper =
-          CallLogNotificationsHelper.getInstance(context);
-      sInstance = new MissedCallNotifier(context, callLogNotificationsHelper);
-    }
-    return sInstance;
+    CallLogNotificationsQueryHelper callLogNotificationsQueryHelper =
+        CallLogNotificationsQueryHelper.getInstance(context);
+    return new MissedCallNotifier(context, callLogNotificationsQueryHelper);
   }
 
   /**
-   * Creates a missed call notification with a post call message if there are no existing missed
-   * calls.
+   * Update missed call notifications from the call log. Accepts default information in case call
+   * log cannot be accessed.
+   *
+   * @param count the number of missed calls to display if call log cannot be accessed. May be
+   *     {@link CallLogNotificationsService#UNKNOWN_MISSED_CALL_COUNT} if unknown.
+   * @param number the phone number of the most recent call to display if the call log cannot be
+   *     accessed. May be null if unknown.
    */
-  public void createPostCallMessageNotification(String number, String message) {
-    int count = CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT;
-    if (ConfigProviderBindings.get(mContext).getBoolean("enable_call_compose", false)) {
-      updateMissedCallNotification(count, number, message);
-    } else {
-      updateMissedCallNotification(count, number, null);
-    }
-  }
-
-  /** Creates a missed call notification. */
-  public void updateMissedCallNotification(int count, String number) {
-    updateMissedCallNotification(count, number, null);
-  }
-
-  private void updateMissedCallNotification(
-      int count, String number, @Nullable String postCallMessage) {
+  @WorkerThread
+  public void updateMissedCallNotification(int count, @Nullable String number) {
     final int titleResId;
     CharSequence expandedText; // The text in the notification's line 1 and 2.
 
-    final List<NewCall> newCalls = mCalllogNotificationsHelper.getNewMissedCalls();
+    List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls();
 
-    if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) {
-      if (newCalls == null) {
-        // If the intent did not contain a count, and we are unable to get a count from the
-        // call log, then no notification can be shown.
-        return;
+    if ((newCalls != null && newCalls.isEmpty()) || count == 0) {
+      // No calls to notify about: clear the notification.
+      CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, null);
+      return;
+    }
+
+    if (newCalls != null) {
+      if (count != CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT
+          && count != newCalls.size()) {
+        LogUtil.w(
+            "MissedCallNotifier.updateMissedCallNotification",
+            "Call count does not match call log count."
+                + " count: "
+                + count
+                + " newCalls.size(): "
+                + newCalls.size());
       }
       count = newCalls.size();
     }
 
-    if (count == 0) {
-      // No voicemails to notify about: clear the notification.
-      clearMissedCalls();
+    if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) {
+      // If the intent did not contain a count, and we are unable to get a count from the
+      // call log, then no notification can be shown.
       return;
     }
 
-    // The call log has been updated, use that information preferentially.
-    boolean useCallLog = newCalls != null && newCalls.size() == count;
-    NewCall newestCall = useCallLog ? newCalls.get(0) : null;
-    long timeMs = useCallLog ? newestCall.dateMs : System.currentTimeMillis();
-    String missedNumber = useCallLog ? newestCall.number : number;
+    Notification.Builder groupSummary = createNotificationBuilder();
+    boolean useCallList = newCalls != null;
 
-    Notification.Builder builder = new Notification.Builder(mContext);
-    // Display the first line of the notification:
-    // 1 missed call: <caller name || handle>
-    // More than 1 missed call: <number of calls> + "missed calls"
     if (count == 1) {
+      NewCall call =
+          useCallList
+              ? newCalls.get(0)
+              : new NewCall(
+                  null,
+                  null,
+                  number,
+                  Calls.PRESENTATION_ALLOWED,
+                  null,
+                  null,
+                  null,
+                  null,
+                  System.currentTimeMillis());
+
       //TODO: look up caller ID that is not in contacts.
       ContactInfo contactInfo =
-          mCalllogNotificationsHelper.getContactInfo(
-              missedNumber,
-              useCallLog ? newestCall.numberPresentation : Calls.PRESENTATION_ALLOWED,
-              useCallLog ? newestCall.countryIso : null);
-
+          callLogNotificationsQueryHelper.getContactInfo(
+              call.number, call.numberPresentation, call.countryIso);
       titleResId =
           contactInfo.userType == ContactsUtils.USER_TYPE_WORK
               ? R.string.notification_missedWorkCallTitle
               : R.string.notification_missedCallTitle;
+
       if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber)
           || TextUtils.equals(contactInfo.name, contactInfo.number)) {
         expandedText =
@@ -147,134 +159,195 @@
         expandedText = contactInfo.name;
       }
 
-      if (!TextUtils.isEmpty(postCallMessage)) {
-        // Ex. "John Doe: Hey dude"
-        expandedText =
-            mContext.getString(
-                R.string.post_call_notification_message, expandedText, postCallMessage);
-      }
-      ContactPhotoLoader loader = new ContactPhotoLoader(mContext, contactInfo);
+      ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo);
       Bitmap photoIcon = loader.loadPhotoIcon();
       if (photoIcon != null) {
-        builder.setLargeIcon(photoIcon);
+        groupSummary.setLargeIcon(photoIcon);
       }
     } else {
       titleResId = R.string.notification_missedCallsTitle;
-      expandedText = mContext.getString(R.string.notification_missedCallsMsg, count);
+      expandedText = context.getString(R.string.notification_missedCallsMsg, count);
     }
 
     // Create a public viewable version of the notification, suitable for display when sensitive
     // notification content is hidden.
-    Notification.Builder publicBuilder = new Notification.Builder(mContext);
-    publicBuilder
-        .setSmallIcon(android.R.drawable.stat_notify_missed_call)
-        .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
-        // Show "Phone" for notification title.
-        .setContentTitle(mContext.getText(R.string.userCallActivityLabel))
-        // Notification details shows that there are missed call(s), but does not reveal
-        // the missed caller information.
-        .setContentText(mContext.getText(titleResId))
+    Notification.Builder publicSummaryBuilder = createNotificationBuilder();
+    publicSummaryBuilder
+        .setContentTitle(context.getText(titleResId))
         .setContentIntent(createCallLogPendingIntent())
-        .setAutoCancel(true)
-        .setWhen(timeMs)
-        .setShowWhen(true)
-        .setDeleteIntent(createClearMissedCallsPendingIntent());
+        .setDeleteIntent(createClearMissedCallsPendingIntent(null));
 
-    // Create the notification suitable for display when sensitive information is showing.
-    builder
-        .setSmallIcon(android.R.drawable.stat_notify_missed_call)
-        .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
-        .setContentTitle(mContext.getText(titleResId))
+    // Create the notification summary suitable for display when sensitive information is showing.
+    groupSummary
+        .setContentTitle(context.getText(titleResId))
         .setContentText(expandedText)
         .setContentIntent(createCallLogPendingIntent())
-        .setAutoCancel(true)
-        .setWhen(timeMs)
-        .setShowWhen(true)
-        .setDefaults(Notification.DEFAULT_VIBRATE)
-        .setDeleteIntent(createClearMissedCallsPendingIntent())
+        .setDeleteIntent(createClearMissedCallsPendingIntent(null))
+        .setGroupSummary(useCallList)
+        .setOnlyAlertOnce(useCallList)
+        .setPublicVersion(publicSummaryBuilder.build());
+
+    NotificationChannelManager.applyChannel(
+        groupSummary,
+        context,
+        Channel.MISSED_CALL,
+        PhoneAccountHandles.getAccount(context, useCallList ? newCalls.get(0) : null));
+
+    Notification notification = groupSummary.build();
+    configureLedOnNotification(notification);
+
+    LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification");
+    getNotificationMgr().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification);
+
+    if (useCallList) {
+      // Do not repost active notifications to prevent erasing post call notes.
+      NotificationManager manager = getNotificationMgr();
+      Set<String> activeTags = new HashSet<>();
+      for (StatusBarNotification activeNotification : manager.getActiveNotifications()) {
+        activeTags.add(activeNotification.getTag());
+      }
+
+      for (NewCall call : newCalls) {
+        String callTag = call.callsUri.toString();
+        if (!activeTags.contains(callTag)) {
+          manager.notify(callTag, NOTIFICATION_ID, getNotificationForCall(call, null));
+        }
+      }
+    }
+  }
+
+  public void insertPostCallNotification(@NonNull String number, @NonNull String note) {
+    List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls();
+    if (newCalls != null && !newCalls.isEmpty()) {
+      for (NewCall call : newCalls) {
+        if (call.number.equals(number.replace("tel:", ""))) {
+          // Update the first notification that matches our post call note sender.
+          getNotificationMgr()
+              .notify(
+                  call.callsUri.toString(), NOTIFICATION_ID, getNotificationForCall(call, note));
+          break;
+        }
+      }
+    }
+  }
+
+  private Notification getNotificationForCall(
+      @NonNull NewCall call, @Nullable String postCallMessage) {
+    ContactInfo contactInfo =
+        callLogNotificationsQueryHelper.getContactInfo(
+            call.number, call.numberPresentation, call.countryIso);
+
+    // Create a public viewable version of the notification, suitable for display when sensitive
+    // notification content is hidden.
+    int titleResId =
+        contactInfo.userType == ContactsUtils.USER_TYPE_WORK
+            ? R.string.notification_missedWorkCallTitle
+            : R.string.notification_missedCallTitle;
+    Notification.Builder publicBuilder =
+        createNotificationBuilder(call).setContentTitle(context.getText(titleResId));
+
+    Notification.Builder builder = createNotificationBuilder(call);
+    CharSequence expandedText;
+    if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber)
+        || TextUtils.equals(contactInfo.name, contactInfo.number)) {
+      expandedText =
+          PhoneNumberUtilsCompat.createTtsSpannable(
+              BidiFormatter.getInstance()
+                  .unicodeWrap(contactInfo.name, TextDirectionHeuristics.LTR));
+    } else {
+      expandedText = contactInfo.name;
+    }
+
+    if (postCallMessage != null) {
+      expandedText =
+          context.getString(R.string.post_call_notification_message, expandedText, postCallMessage);
+    }
+
+    ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo);
+    Bitmap photoIcon = loader.loadPhotoIcon();
+    if (photoIcon != null) {
+      builder.setLargeIcon(photoIcon);
+    }
+    // Create the notification suitable for display when sensitive information is showing.
+    builder
+        .setContentTitle(context.getText(titleResId))
+        .setContentText(expandedText)
         // Include a public version of the notification to be shown when the missed call
         // notification is shown on the user's lock screen and they have chosen to hide
         // sensitive notification information.
         .setPublicVersion(publicBuilder.build());
 
-    // Add additional actions when there is only 1 missed call and the user isn't locked
-    if (UserManagerCompat.isUserUnlocked(mContext) && count == 1) {
-      if (!TextUtils.isEmpty(missedNumber)
-          && !TextUtils.equals(missedNumber, mContext.getString(R.string.handle_restricted))) {
+    // Add additional actions when the user isn't locked
+    if (UserManagerCompat.isUserUnlocked(context)) {
+      if (!TextUtils.isEmpty(call.number)
+          && !TextUtils.equals(call.number, context.getString(R.string.handle_restricted))) {
         builder.addAction(
-            R.drawable.ic_phone_24dp,
-            mContext.getString(R.string.notification_missedCall_call_back),
-            createCallBackPendingIntent(missedNumber));
+            new Notification.Action.Builder(
+                    Icon.createWithResource(context, R.drawable.ic_phone_24dp),
+                    context.getString(R.string.notification_missedCall_call_back),
+                    createCallBackPendingIntent(call.number, call.callsUri))
+                .build());
 
-        if (!PhoneNumberHelper.isUriNumber(missedNumber)) {
+        if (!PhoneNumberHelper.isUriNumber(call.number)) {
           builder.addAction(
-              R.drawable.ic_message_24dp,
-              mContext.getString(R.string.notification_missedCall_message),
-              createSendSmsFromNotificationPendingIntent(missedNumber));
+              new Notification.Action.Builder(
+                      Icon.createWithResource(context, R.drawable.ic_message_24dp),
+                      context.getString(R.string.notification_missedCall_message),
+                      createSendSmsFromNotificationPendingIntent(call.number, call.callsUri))
+                  .build());
         }
       }
     }
 
     Notification notification = builder.build();
     configureLedOnNotification(notification);
-
-    LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification");
-    getNotificationMgr().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification);
+    return notification;
   }
 
-  private void clearMissedCalls() {
-    AsyncTask.execute(
-        new Runnable() {
-          @Override
-          public void run() {
-            // Call log is only accessible when unlocked. If that's the case, clear the list of
-            // new missed calls from the call log.
-            if (UserManagerCompat.isUserUnlocked(mContext)) {
-              ContentValues values = new ContentValues();
-              values.put(Calls.NEW, 0);
-              values.put(Calls.IS_READ, 1);
-              StringBuilder where = new StringBuilder();
-              where.append(Calls.NEW);
-              where.append(" = 1 AND ");
-              where.append(Calls.TYPE);
-              where.append(" = ?");
-              try {
-                mContext
-                    .getContentResolver()
-                    .update(
-                        Calls.CONTENT_URI,
-                        values,
-                        where.toString(),
-                        new String[] {Integer.toString(Calls.MISSED_TYPE)});
-              } catch (IllegalArgumentException e) {
-                LogUtil.e(
-                    "MissedCallNotifier.clearMissedCalls",
-                    "contacts provider update command failed",
-                    e);
-              }
-            }
-            getNotificationMgr().cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
-          }
-        });
+  private Notification.Builder createNotificationBuilder() {
+    return new Notification.Builder(context)
+        .setGroup(NOTIFICATION_TAG)
+        .setSmallIcon(android.R.drawable.stat_notify_missed_call)
+        .setColor(context.getResources().getColor(R.color.dialer_theme_color, null))
+        .setAutoCancel(true)
+        .setOnlyAlertOnce(true)
+        .setShowWhen(true)
+        .setDefaults(Notification.DEFAULT_VIBRATE);
+  }
+
+  private Notification.Builder createNotificationBuilder(@NonNull NewCall call) {
+    Builder builder =
+        createNotificationBuilder()
+            .setWhen(call.dateMs)
+            .setDeleteIntent(createClearMissedCallsPendingIntent(call.callsUri))
+            .setContentIntent(createCallLogPendingIntent(call.callsUri));
+
+    NotificationChannelManager.applyChannel(
+        builder, context, Channel.MISSED_CALL, PhoneAccountHandles.getAccount(context, call));
+    return builder;
   }
 
   /** Trigger an intent to make a call from a missed call number. */
-  public void callBackFromMissedCall(String number) {
-    closeSystemDialogs(mContext);
-    CallLogNotificationsHelper.removeMissedCallNotifications(mContext);
+  @WorkerThread
+  public void callBackFromMissedCall(String number, Uri callUri) {
+    closeSystemDialogs(context);
+    CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, callUri);
+    TelecomUtil.cancelMissedCallsNotification(context);
     DialerUtils.startActivityWithErrorToast(
-        mContext,
+        context,
         new CallIntentBuilder(number, CallInitiationType.Type.MISSED_CALL_NOTIFICATION)
             .build()
             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
   }
 
   /** Trigger an intent to send an sms from a missed call number. */
-  public void sendSmsFromMissedCall(String number) {
-    closeSystemDialogs(mContext);
-    CallLogNotificationsHelper.removeMissedCallNotifications(mContext);
+  @WorkerThread
+  public void sendSmsFromMissedCall(String number, Uri callUri) {
+    closeSystemDialogs(context);
+    CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, callUri);
+    TelecomUtil.cancelMissedCallsNotification(context);
     DialerUtils.startActivityWithErrorToast(
-        mContext, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+        context, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
   }
 
   /**
@@ -283,34 +356,50 @@
    * @return The pending intent.
    */
   private PendingIntent createCallLogPendingIntent() {
+    return createCallLogPendingIntent(null);
+  }
+
+  /**
+   * Creates a new pending intent that sends the user to the call log.
+   *
+   * @return The pending intent.
+   * @param callUri Uri of the call to jump to. May be null
+   */
+  private PendingIntent createCallLogPendingIntent(@Nullable Uri callUri) {
     Intent contentIntent =
-        DialtactsActivity.getShowTabIntent(mContext, ListsFragment.TAB_INDEX_HISTORY);
-    return PendingIntent.getActivity(mContext, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+        DialtactsActivity.getShowTabIntent(context, ListsFragment.TAB_INDEX_HISTORY);
+    // TODO (b/35486204): scroll to call
+    contentIntent.setData(callUri);
+    return PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
   }
 
   /** Creates a pending intent that marks all new missed calls as old. */
-  private PendingIntent createClearMissedCallsPendingIntent() {
-    Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+  private PendingIntent createClearMissedCallsPendingIntent(@Nullable Uri callUri) {
+    Intent intent = new Intent(context, CallLogNotificationsService.class);
     intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD);
-    return PendingIntent.getService(mContext, 0, intent, 0);
+    intent.setData(callUri);
+    return PendingIntent.getService(context, 0, intent, 0);
   }
 
-  private PendingIntent createCallBackPendingIntent(String number) {
-    Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+  private PendingIntent createCallBackPendingIntent(String number, @NonNull Uri callUri) {
+    Intent intent = new Intent(context, CallLogNotificationsService.class);
     intent.setAction(CallLogNotificationsService.ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION);
     intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number);
+    intent.setData(callUri);
     // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new
     // extra.
-    return PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+    return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
   }
 
-  private PendingIntent createSendSmsFromNotificationPendingIntent(String number) {
-    Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+  private PendingIntent createSendSmsFromNotificationPendingIntent(
+      String number, @NonNull Uri callUri) {
+    Intent intent = new Intent(context, CallLogNotificationsService.class);
     intent.setAction(CallLogNotificationsService.ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION);
     intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number);
+    intent.setData(callUri);
     // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new
     // extra.
-    return PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+    return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
   }
 
   /** Configures a notification to emit the blinky notification light. */
@@ -325,6 +414,6 @@
   }
 
   private NotificationManager getNotificationMgr() {
-    return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+    return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
   }
 }
diff --git a/java/com/android/dialer/app/calllog/PhoneAccountHandles.java b/java/com/android/dialer/app/calllog/PhoneAccountHandles.java
new file mode 100644
index 0000000..6d51b85
--- /dev/null
+++ b/java/com/android/dialer/app/calllog/PhoneAccountHandles.java
@@ -0,0 +1,41 @@
+package com.android.dialer.app.calllog;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.telecom.TelecomUtil;
+
+/** Methods to help extract {@link PhoneAccount} information from database and Telecomm sources. */
+class PhoneAccountHandles {
+
+  @Nullable
+  public static PhoneAccountHandle getAccount(@NonNull Context context, @Nullable NewCall call) {
+    PhoneAccountHandle handle;
+    if (call == null || call.accountComponentName == null || call.accountId == null) {
+      LogUtil.v(
+          "PhoneAccountUtils.getAccount",
+          "accountComponentName == null || callToNotify.accountId == null");
+      handle = TelecomUtil.getDefaultOutgoingPhoneAccount(context, PhoneAccount.SCHEME_TEL);
+      if (handle == null) {
+        return null;
+      }
+    } else {
+      handle =
+          new PhoneAccountHandle(
+              ComponentName.unflattenFromString(call.accountComponentName), call.accountId);
+    }
+    if (handle.getComponentName() != null) {
+      LogUtil.v(
+          "PhoneAccountUtils.getAccount",
+          "PhoneAccountHandle.ComponentInfo:" + handle.getComponentName());
+    } else {
+      LogUtil.i("PhoneAccountUtils.getAccount", "PhoneAccountHandle.ComponentInfo: null");
+    }
+    return handle;
+  }
+}
diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java
index b18270b..acbccb3 100644
--- a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java
+++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java
@@ -27,9 +27,10 @@
 import android.text.format.DateUtils;
 import android.view.View;
 import android.widget.TextView;
-import com.android.dialer.app.PhoneCallDetails;
 import com.android.dialer.app.R;
 import com.android.dialer.app.calllog.calllogcache.CallLogCache;
+import com.android.dialer.calllogutils.PhoneCallDetails;
+import com.android.dialer.oem.MotorolaUtils;
 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
 import com.android.dialer.util.DialerUtils;
 import java.util.ArrayList;
@@ -84,6 +85,8 @@
     // Show the video icon if the call had video enabled.
     views.callTypeIcons.setShowVideo(
         (details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO);
+    views.callTypeIcons.setShowHd(
+        MotorolaUtils.shouldShowHdIconInCallLog(mContext, details.features));
     views.callTypeIcons.requestLayout();
     views.callTypeIcons.setVisibility(View.VISIBLE);
 
diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java
index 4769968..e2e27a1 100644
--- a/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java
+++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java
@@ -20,6 +20,7 @@
 import android.view.View;
 import android.widget.TextView;
 import com.android.dialer.app.R;
+import com.android.dialer.calllogutils.CallTypeIconsView;
 
 /** Encapsulates the views that are used to display the details of a phone call in the call log. */
 public final class PhoneCallDetailsViews {
diff --git a/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java b/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java
index e539cee..6f101f5 100644
--- a/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java
+++ b/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java
@@ -40,10 +40,13 @@
 
   private VoicemailErrorManager mVoicemailAlertManager;
 
+  public VisualVoicemailCallLogFragment() {
+    super(CallLog.Calls.VOICEMAIL_TYPE);
+  }
+
   @Override
   public void onCreate(Bundle state) {
     super.onCreate(state);
-    mCallTypeFilter = CallLog.Calls.VOICEMAIL_TYPE;
     mVoicemailPlaybackPresenter = VoicemailPlaybackPresenter.getInstance(getActivity(), state);
     getActivity()
         .getContentResolver()
diff --git a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java
index d6d8354..e73684e 100644
--- a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java
+++ b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java
@@ -15,13 +15,18 @@
  */
 package com.android.dialer.app.calllog;
 
+import android.app.NotificationManager;
 import android.content.AsyncQueryHandler;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
+import android.net.Uri;
 import android.provider.CallLog.Calls;
-import android.util.Log;
+import android.support.annotation.Nullable;
+import com.android.dialer.app.R;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.notification.GroupedNotificationUtil;
 
 /** Handles asynchronous queries to the call log for voicemail. */
 public class VoicemailQueryHandler extends AsyncQueryHandler {
@@ -39,7 +44,7 @@
   }
 
   /** Updates all new voicemails to mark them as old. */
-  public void markNewVoicemailsAsOld() {
+  public void markNewVoicemailsAsOld(@Nullable Uri voicemailUri) {
     // Mark all "new" voicemails as not new anymore.
     StringBuilder where = new StringBuilder();
     where.append(Calls.NEW);
@@ -47,6 +52,10 @@
     where.append(Calls.TYPE);
     where.append(" = ?");
 
+    if (voicemailUri != null) {
+      where.append(" AND ").append(Calls.VOICEMAIL_URI).append(" = ?");
+    }
+
     ContentValues values = new ContentValues(1);
     values.put(Calls.NEW, "0");
 
@@ -56,7 +65,15 @@
         Calls.CONTENT_URI_WITH_VOICEMAIL,
         values,
         where.toString(),
-        new String[] {Integer.toString(Calls.VOICEMAIL_TYPE)});
+        voicemailUri == null
+            ? new String[] {Integer.toString(Calls.VOICEMAIL_TYPE)}
+            : new String[] {Integer.toString(Calls.VOICEMAIL_TYPE), voicemailUri.toString()});
+
+    GroupedNotificationUtil.removeNotification(
+        mContext.getSystemService(NotificationManager.class),
+        voicemailUri != null ? voicemailUri.toString() : null,
+        R.id.notification_voicemail,
+        DefaultVoicemailNotifier.NOTIFICATION_TAG);
   }
 
   @Override
@@ -67,7 +84,7 @@
         serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS);
         mContext.startService(serviceIntent);
       } else {
-        Log.w(TAG, "Unknown update completed: ignoring: " + token);
+        LogUtil.w(TAG, "Unknown update completed: ignoring: " + token);
       }
     }
   }
diff --git a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java
index c342b7e..0399987 100644
--- a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java
+++ b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java
@@ -20,10 +20,10 @@
 import android.support.annotation.VisibleForTesting;
 import android.telecom.PhoneAccountHandle;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.Pair;
-import com.android.dialer.app.calllog.PhoneAccountUtils;
+import com.android.dialer.calllogutils.PhoneAccountUtils;
 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
-import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -45,9 +45,9 @@
   final Map<Pair<PhoneAccountHandle, CharSequence>, Boolean> mVoicemailQueryCache =
       new ConcurrentHashMap<>();
 
-  private final Map<PhoneAccountHandle, String> mPhoneAccountLabelCache = new HashMap<>();
-  private final Map<PhoneAccountHandle, Integer> mPhoneAccountColorCache = new HashMap<>();
-  private final Map<PhoneAccountHandle, Boolean> mPhoneAccountCallWithNoteCache = new HashMap<>();
+  private final Map<PhoneAccountHandle, String> mPhoneAccountLabelCache = new ArrayMap<>();
+  private final Map<PhoneAccountHandle, Integer> mPhoneAccountColorCache = new ArrayMap<>();
+  private final Map<PhoneAccountHandle, Boolean> mPhoneAccountCallWithNoteCache = new ArrayMap<>();
 
   /* package */ CallLogCacheLollipopMr1(Context context) {
     super(context);
diff --git a/java/com/android/dialer/app/contactinfo/ContactInfoCache.java b/java/com/android/dialer/app/contactinfo/ContactInfoCache.java
index 4135cb7..6c35711 100644
--- a/java/com/android/dialer/app/contactinfo/ContactInfoCache.java
+++ b/java/com/android/dialer/app/contactinfo/ContactInfoCache.java
@@ -29,8 +29,8 @@
 import java.util.concurrent.PriorityBlockingQueue;
 
 /**
- * This is a cache of contact details for the phone numbers in the c all log. The key is the phone
- * number with the country in which teh call was placed or received. The content of the cache is
+ * This is a cache of contact details for the phone numbers in the call log. The key is the phone
+ * number with the country in which the call was placed or received. The content of the cache is
  * expired (but not purged) whenever the application comes to the foreground.
  *
  * <p>This cache queues request for information and queries for information on a background thread,
diff --git a/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java b/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java
index a8c7185..71e4a16 100644
--- a/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java
+++ b/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java
@@ -104,7 +104,7 @@
       final RoundedBitmapDrawable drawable =
           RoundedBitmapDrawableFactory.create(mContext.getResources(), bitmap);
       drawable.setAntiAlias(true);
-      drawable.setCornerRadius(bitmap.getHeight() / 2);
+      drawable.setCircular(true);
       return drawable;
     } catch (IOException e) {
       LogUtil.e("ContactPhotoLoader.createPhotoIconDrawable", e.toString());
diff --git a/java/com/android/dialer/app/dialpad/DialpadFragment.java b/java/com/android/dialer/app/dialpad/DialpadFragment.java
index 18bb250..4785ab1 100644
--- a/java/com/android/dialer/app/dialpad/DialpadFragment.java
+++ b/java/com/android/dialer/app/dialpad/DialpadFragment.java
@@ -78,9 +78,9 @@
 import com.android.dialer.app.R;
 import com.android.dialer.app.SpecialCharSequenceMgr;
 import com.android.dialer.app.calllog.CallLogAsync;
-import com.android.dialer.app.calllog.PhoneAccountUtils;
 import com.android.dialer.callintent.CallIntentBuilder;
 import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.calllogutils.PhoneAccountUtils;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.dialpadview.DialpadKeyButton;
 import com.android.dialer.dialpadview.DialpadView;
@@ -598,6 +598,7 @@
 
   @Override
   public void onStart() {
+    LogUtil.d("DialpadFragment.onStart", "first launch: %b", mFirstLaunch);
     Trace.beginSection(TAG + " onStart");
     super.onStart();
     // if the mToneGenerator creation fails, just continue without it.  It is
@@ -624,6 +625,7 @@
 
   @Override
   public void onResume() {
+    LogUtil.d("DialpadFragment.onResume", "");
     Trace.beginSection(TAG + " onResume");
     super.onResume();
 
diff --git a/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java b/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java
index eef9207..9ec6042 100644
--- a/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java
+++ b/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java
@@ -135,11 +135,6 @@
   }
 
   @Override
-  public int getActionBarHideOffset() {
-    return 0;
-  }
-
-  @Override
   public int getActionBarHeight() {
     return 0;
   }
diff --git a/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java b/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java
index 2125a15..1cdeb21 100644
--- a/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java
+++ b/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java
@@ -17,12 +17,14 @@
 package com.android.dialer.app.legacybindings;
 
 import android.app.Activity;
+import android.support.annotation.NonNull;
 import android.view.ViewGroup;
 import com.android.dialer.app.calllog.CallLogAdapter;
 import com.android.dialer.app.calllog.calllogcache.CallLogCache;
 import com.android.dialer.app.contactinfo.ContactInfoCache;
 import com.android.dialer.app.list.RegularSearchFragment;
 import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
 
 /**
  * These are old bindings between Dialer and the container application. All new bindings should be
@@ -41,6 +43,7 @@
       CallLogCache callLogCache,
       ContactInfoCache contactInfoCache,
       VoicemailPlaybackPresenter voicemailPlaybackPresenter,
+      @NonNull FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler,
       int activityType);
 
   RegularSearchFragment newRegularSearchFragment();
diff --git a/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java
index f01df78..6e32843 100644
--- a/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java
+++ b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java
@@ -17,12 +17,14 @@
 package com.android.dialer.app.legacybindings;
 
 import android.app.Activity;
+import android.support.annotation.NonNull;
 import android.view.ViewGroup;
 import com.android.dialer.app.calllog.CallLogAdapter;
 import com.android.dialer.app.calllog.calllogcache.CallLogCache;
 import com.android.dialer.app.contactinfo.ContactInfoCache;
 import com.android.dialer.app.list.RegularSearchFragment;
 import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
 
 /** Default implementation for dialer legacy bindings. */
 public class DialerLegacyBindingsStub implements DialerLegacyBindings {
@@ -35,6 +37,7 @@
       CallLogCache callLogCache,
       ContactInfoCache contactInfoCache,
       VoicemailPlaybackPresenter voicemailPlaybackPresenter,
+      @NonNull FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler,
       int activityType) {
     return new CallLogAdapter(
         activity,
@@ -43,6 +46,7 @@
         callLogCache,
         contactInfoCache,
         voicemailPlaybackPresenter,
+        filteredNumberAsyncQueryHandler,
         activityType);
   }
 
diff --git a/java/com/android/dialer/app/list/ListsFragment.java b/java/com/android/dialer/app/list/ListsFragment.java
index 725ad30..13938f2 100644
--- a/java/com/android/dialer/app/list/ListsFragment.java
+++ b/java/com/android/dialer/app/list/ListsFragment.java
@@ -30,19 +30,16 @@
 import android.support.v13.app.FragmentPagerAdapter;
 import android.support.v4.view.ViewPager;
 import android.support.v4.view.ViewPager.OnPageChangeListener;
-import android.support.v7.app.ActionBar;
-import android.support.v7.app.AppCompatActivity;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import com.android.contacts.common.list.ViewPagerTabs;
 import com.android.dialer.app.R;
 import com.android.dialer.app.calllog.CallLogFragment;
-import com.android.dialer.app.calllog.CallLogNotificationsHelper;
+import com.android.dialer.app.calllog.CallLogNotificationsService;
 import com.android.dialer.app.calllog.VisualVoicemailCallLogFragment;
 import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler;
 import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler.Source;
-import com.android.dialer.app.widget.ActionBarController;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.database.CallLogQueryHandler;
 import com.android.dialer.logging.Logger;
@@ -92,7 +89,6 @@
   public static final int TAB_COUNT_DEFAULT = 3;
   public static final int TAB_COUNT_WITH_VOICEMAIL = 4;
   private static final String TAG = "ListsFragment";
-  private ActionBar mActionBar;
   private ViewPager mViewPager;
   private ViewPagerTabs mViewPagerTabs;
   private ViewPagerAdapter mViewPagerAdapter;
@@ -108,8 +104,7 @@
   private boolean mHasFetchedVoicemailStatus;
   private boolean mShowVoicemailTabAfterVoicemailStatusIsFetched;
   private VoicemailStatusHelper mVoicemailStatusHelper;
-  private ArrayList<OnPageChangeListener> mOnPageChangeListeners =
-      new ArrayList<OnPageChangeListener>();
+  private final ArrayList<OnPageChangeListener> mOnPageChangeListeners = new ArrayList<>();
   private String[] mTabTitles;
   private int[] mTabIcons;
   /** The position of the currently selected tab. */
@@ -149,7 +144,6 @@
     Trace.beginSection(TAG + " onResume");
     super.onResume();
 
-    mActionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
     if (getUserVisibleHint()) {
       sendScreenViewForCurrentPosition();
     }
@@ -329,7 +323,7 @@
           .putBoolean(
               VisualVoicemailEnabledChecker.PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER,
               hasActiveVoicemailProvider)
-          .commit();
+          .apply();
     }
 
     if (hasActiveVoicemailProvider) {
@@ -403,7 +397,7 @@
   public void markMissedCallsAsReadAndRemoveNotifications() {
     if (mCallLogQueryHandler != null) {
       mCallLogQueryHandler.markMissedCallsAsRead();
-      CallLogNotificationsHelper.removeMissedCallNotifications(getActivity());
+      CallLogNotificationsService.markNewMissedCallsAsOld(getContext(), null);
     }
   }
 
@@ -413,11 +407,6 @@
     mRemoveView.animate().alpha(show ? 1 : 0).start();
   }
 
-  public boolean shouldShowActionBar() {
-    // TODO: Update this based on scroll state.
-    return mActionBar != null;
-  }
-
   public SpeedDialFragment getSpeedDialFragment() {
     return mSpeedDialFragment;
   }
@@ -486,11 +475,6 @@
     throw new IllegalStateException("No fragment at position " + position);
   }
 
-  public interface HostInterface {
-
-    ActionBarController getActionBarController();
-  }
-
   public class ViewPagerAdapter extends FragmentPagerAdapter {
 
     private final List<Fragment> mFragments = new ArrayList<>();
@@ -518,7 +502,7 @@
           return mSpeedDialFragment;
         case TAB_INDEX_HISTORY:
           if (mHistoryFragment == null) {
-            mHistoryFragment = new CallLogFragment();
+            mHistoryFragment = new CallLogFragment(CallLogQueryHandler.CALL_TYPE_ALL);
           }
           return mHistoryFragment;
         case TAB_INDEX_ALL_CONTACTS:
diff --git a/java/com/android/dialer/app/list/SearchFragment.java b/java/com/android/dialer/app/list/SearchFragment.java
index 4a7d48a..e6615aa 100644
--- a/java/com/android/dialer/app/list/SearchFragment.java
+++ b/java/com/android/dialer/app/list/SearchFragment.java
@@ -98,6 +98,7 @@
 
   @Override
   public void onStart() {
+    LogUtil.d("SearchFragment.onStart", "");
     super.onStart();
     if (isSearchMode()) {
       getAdapter().setHasHeader(0, false);
@@ -301,6 +302,7 @@
    * shown. This can be optionally animated.
    */
   public void updatePosition(boolean animate) {
+    LogUtil.d("SearchFragment.updatePosition", "animate: %b", animate);
     if (mActivity == null) {
       // Activity will be set in onStart, and this method will be called again
       return;
@@ -363,6 +365,13 @@
       return;
     }
     int spacerHeight = mActivity.isDialpadShown() ? mActivity.getDialpadHeight() : 0;
+    LogUtil.d(
+        "SearchFragment.resizeListView",
+        "spacerHeight: %d -> %d, isDialpadShown: %b, dialpad height: %d",
+        mSpacer.getHeight(),
+        spacerHeight,
+        mActivity.isDialpadShown(),
+        mActivity.getDialpadHeight());
     if (spacerHeight != mSpacer.getHeight()) {
       final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mSpacer.getLayoutParams();
       lp.height = spacerHeight;
@@ -418,8 +427,6 @@
 
     int getDialpadHeight();
 
-    int getActionBarHideOffset();
-
     int getActionBarHeight();
   }
 }
diff --git a/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml b/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml
index 247b34f..7e450c4 100644
--- a/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml
+++ b/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml
@@ -29,18 +29,6 @@
       android:theme="@style/SettingsStyle">
     </activity>
 
-    <activity
-      android:label="@string/callDetailTitle"
-      android:name="com.android.dialer.app.CallDetailActivity"
-      android:parentActivityName="com.android.dialer.calllog.CallLogActivity"
-      android:theme="@style/CallDetailActivityTheme">
-      <intent-filter>
-        <action android:name="android.intent.action.VIEW"/>
-        <category android:name="android.intent.category.DEFAULT"/>
-        <data android:mimeType="vnd.android.cursor.item/calls"/>
-      </intent-filter>
-    </activity>
-
     <!-- The entrance point for Phone UI.
          stateAlwaysHidden is set to suppress keyboard show up on
          dialpad screen. -->
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/search_shadow.9.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/search_shadow.9.png
new file mode 100644
index 0000000..ff55620
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/search_shadow.9.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/layout/call_detail.xml b/java/com/android/dialer/app/res/layout/call_detail.xml
deleted file mode 100644
index 58a7bf0..0000000
--- a/java/com/android/dialer/app/res/layout/call_detail.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2009 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-  android:id="@+id/call_detail"
-  android:layout_width="match_parent"
-  android:layout_height="match_parent"
-  android:background="@color/background_dialer_call_log">
-
-  <!--
-    The list view is under everything.
-    It contains a first header element which is hidden under the controls UI.
-    When scrolling, the controls move up until the name bar hits the top.
-    -->
-  <ListView
-    android:id="@+id/history"
-    android:layout_width="match_parent"
-    android:layout_height="fill_parent"/>
-
-</FrameLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_detail_footer.xml b/java/com/android/dialer/app/res/layout/call_detail_footer.xml
deleted file mode 100644
index 5771344..0000000
--- a/java/com/android/dialer/app/res/layout/call_detail_footer.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-  android:layout_width="match_parent"
-  android:layout_height="wrap_content"
-  android:orientation="vertical">
-
-  <View
-    android:layout_width="match_parent"
-    android:layout_height="@dimen/divider_line_thickness"
-    android:background="@color/call_log_action_divider"/>
-
-  <TextView
-    android:id="@+id/call_detail_action_copy"
-    style="@style/CallDetailActionItemStyle"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:drawableStart="@drawable/ic_call_detail_content_copy"
-    android:text="@string/action_copy_number_text"/>
-
-  <TextView
-    android:id="@+id/call_detail_action_edit_before_call"
-    style="@style/CallDetailActionItemStyle"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:drawableStart="@drawable/ic_call_detail_edit"
-    android:text="@string/action_edit_number_before_call"
-    android:visibility="gone"/>
-
-  <TextView
-    android:id="@+id/call_detail_action_report"
-    style="@style/CallDetailActionItemStyle"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:drawableStart="@drawable/ic_call_detail_report"
-    android:text="@string/action_report_number"
-    android:visibility="gone"/>
-
-</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_detail_header.xml b/java/com/android/dialer/app/res/layout/call_detail_header.xml
deleted file mode 100644
index fd85f0a..0000000
--- a/java/com/android/dialer/app/res/layout/call_detail_header.xml
+++ /dev/null
@@ -1,89 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-  android:id="@+id/caller_information"
-  android:layout_width="match_parent"
-  android:layout_height="wrap_content"
-  android:paddingTop="@dimen/call_detail_top_margin"
-  android:paddingBottom="@dimen/call_detail_bottom_margin"
-  android:paddingStart="@dimen/call_detail_horizontal_margin"
-  android:background="@color/background_dialer_white"
-  android:baselineAligned="false"
-  android:elevation="@dimen/call_detail_elevation"
-  android:focusable="true"
-  android:orientation="horizontal">
-
-  <QuickContactBadge
-    android:id="@+id/quick_contact_photo"
-    android:layout_width="@dimen/contact_photo_size"
-    android:layout_height="@dimen/contact_photo_size"
-    android:layout_marginTop="3dp"
-    android:layout_alignParentStart="true"
-    android:layout_gravity="top"
-    android:focusable="true"/>
-
-  <LinearLayout
-    android:layout_width="0dp"
-    android:layout_height="wrap_content"
-    android:layout_weight="1"
-    android:layout_marginStart="@dimen/call_detail_horizontal_margin"
-    android:gravity="center_vertical"
-    android:orientation="vertical">
-
-    <TextView
-      android:id="@+id/caller_name"
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"
-      android:layout_marginTop="2dp"
-      android:layout_marginBottom="3dp"
-      android:includeFontPadding="false"
-      android:singleLine="true"
-      android:textColor="?android:textColorPrimary"
-      android:textSize="@dimen/call_log_primary_text_size"/>
-
-    <TextView
-      android:id="@+id/caller_number"
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"
-      android:layout_marginBottom="1dp"
-      android:singleLine="true"
-      android:textColor="?android:textColorSecondary"
-      android:textSize="@dimen/call_log_detail_text_size"/>
-
-    <TextView
-      android:id="@+id/phone_account_label"
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"
-      android:singleLine="true"
-      android:textColor="?android:textColorSecondary"
-      android:textSize="@dimen/call_log_detail_text_size"
-      android:visibility="gone"/>
-
-  </LinearLayout>
-
-  <ImageView
-    android:id="@+id/call_back_button"
-    android:layout_width="@dimen/call_log_list_item_primary_action_dimen"
-    android:layout_height="@dimen/call_log_list_item_primary_action_dimen"
-    android:layout_marginEnd="4dp"
-    android:background="?android:attr/selectableItemBackgroundBorderless"
-    android:contentDescription="@string/description_call_log_call_action"
-    android:scaleType="center"
-    android:src="@drawable/ic_call_24dp"
-    android:tint="@color/call_log_list_item_primary_action_icon_tint"
-    android:visibility="gone"/>
-
-</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_detail_history_item.xml b/java/com/android/dialer/app/res/layout/call_detail_history_item.xml
index 5958ee8..0184a42 100644
--- a/java/com/android/dialer/app/res/layout/call_detail_history_item.xml
+++ b/java/com/android/dialer/app/res/layout/call_detail_history_item.xml
@@ -27,9 +27,8 @@
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:orientation="horizontal">
-    <view
+    <com.android.dialer.calllogutils.CallTypeIconsView
       android:id="@+id/call_type_icon"
-      class="com.android.dialer.app.calllog.CallTypeIconsView"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_gravity="center_vertical"/>
diff --git a/java/com/android/dialer/app/res/layout/call_log_activity.xml b/java/com/android/dialer/app/res/layout/call_log_activity.xml
new file mode 100644
index 0000000..4e2b188
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_log_activity.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:id="@+id/calllog_frame"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent"
+  android:orientation="vertical">
+  <com.android.contacts.common.list.ViewPagerTabs
+    android:id="@+id/viewpager_header"
+    style="@style/DialtactsActionBarTabTextStyle"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/tab_height"
+    android:layout_gravity="top"
+    android:elevation="@dimen/tab_elevation"
+    android:orientation="horizontal"
+    android:textAllCaps="true"/>
+  <android.support.v4.view.ViewPager
+    android:id="@+id/call_log_pager"
+    android:layout_width="match_parent"
+    android:layout_height="0dp"
+    android:layout_weight="1"/>
+  <RelativeLayout
+    android:id="@+id/floating_action_button_container"
+    android:layout_width="0dp"
+    android:layout_height="0dp"/>
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_log_list_item.xml b/java/com/android/dialer/app/res/layout/call_log_list_item.xml
index c22ac86..1592aa9 100644
--- a/java/com/android/dialer/app/res/layout/call_log_list_item.xml
+++ b/java/com/android/dialer/app/res/layout/call_log_list_item.xml
@@ -93,8 +93,7 @@
             android:layout_height="wrap_content"
             android:orientation="horizontal">
 
-            <view
-              class="com.android.dialer.app.calllog.CallTypeIconsView"
+            <com.android.dialer.calllogutils.CallTypeIconsView
               android:id="@+id/call_type_icons"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
diff --git a/java/com/android/voicemailomtp/res/values/attrs.xml b/java/com/android/dialer/app/res/menu/call_log_options.xml
similarity index 67%
copy from java/com/android/voicemailomtp/res/values/attrs.xml
copy to java/com/android/dialer/app/res/menu/call_log_options.xml
index d1c7329..e78b72e 100644
--- a/java/com/android/voicemailomtp/res/values/attrs.xml
+++ b/java/com/android/dialer/app/res/menu/call_log_options.xml
@@ -1,20 +1,22 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2006 The Android Open Source Project
+<!-- Copyright (C) 2013 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
      You may obtain a copy of the License at
-  
+
           http://www.apache.org/licenses/LICENSE-2.0
-  
+
      Unless required by applicable law or agreed to in writing, software
      distributed under the License is distributed on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-
-<resources>
-
-    <attr name="preferenceBackgroundColor" format="color" />
-</resources>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+  <item
+    android:id="@+id/delete_all"
+    android:orderInCategory="1"
+    android:showAsAction="never"
+    android:title="@string/call_log_delete_all"/>
+</menu>
diff --git a/java/com/android/dialer/app/res/menu/dialtacts_options.xml b/java/com/android/dialer/app/res/menu/dialtacts_options.xml
index 434aa81..25a3e18 100644
--- a/java/com/android/dialer/app/res/menu/dialtacts_options.xml
+++ b/java/com/android/dialer/app/res/menu/dialtacts_options.xml
@@ -16,13 +16,17 @@
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
 
   <item
-    android:id="@+id/menu_delete_all"
-    android:title="@string/call_log_delete_all"/>
+    android:id="@+id/menu_history"
+    android:icon="@drawable/ic_menu_history_lt"
+    android:title="@string/action_menu_call_history_description"/>
   <item
     android:id="@+id/menu_clear_frequents"
     android:title="@string/menu_clear_frequents"/>
   <item
     android:id="@+id/menu_call_settings"
     android:title="@string/dialer_settings_label"/>
+  <item
+    android:id="@+id/menu_simulator_submenu"
+    android:title="@string/simulator_submenu_label"/>
 
 </menu>
diff --git a/java/com/android/dialer/app/res/values/colors.xml b/java/com/android/dialer/app/res/values/colors.xml
index b88e552..cf6b926 100644
--- a/java/com/android/dialer/app/res/values/colors.xml
+++ b/java/com/android/dialer/app/res/values/colors.xml
@@ -16,7 +16,6 @@
 
 <resources>
   <color name="dialer_red_highlight_color">#ff1744</color>
-  <color name="dialer_green_highlight_color">#00c853</color>
 
   <color name="dialer_button_text_color">#fff</color>
   <color name="dialer_flat_button_text_color">@color/dialer_theme_color</color>
@@ -84,13 +83,6 @@
        as call back, play voicemail, etc. -->
   <color name="call_log_action_text">@color/dialer_theme_color</color>
 
-  <!-- Color for missed call icons. -->
-  <color name="missed_call">#ff2e58</color>
-  <!-- Color for answered or outgoing call icons. -->
-  <color name="answered_call">@color/dialer_green_highlight_color</color>
-  <!-- Color for blocked call icons. -->
-  <color name="blocked_call">@color/dialer_secondary_text_color</color>
-
   <color name="dialer_dialpad_touch_tint">@color/dialer_theme_color_20pct</color>
 
   <color name="floating_action_button_touch_tint">#80ffffff</color>
diff --git a/java/com/android/dialer/app/res/values/dimens.xml b/java/com/android/dialer/app/res/values/dimens.xml
index f3fd633..7da29c7 100644
--- a/java/com/android/dialer/app/res/values/dimens.xml
+++ b/java/com/android/dialer/app/res/values/dimens.xml
@@ -28,7 +28,6 @@
   <dimen name="call_log_horizontal_margin">8dp</dimen>
   <dimen name="call_log_call_action_size">32dp</dimen>
   <dimen name="call_log_call_action_width">54dp</dimen>
-  <dimen name="call_log_icon_margin">4dp</dimen>
   <dimen name="call_log_inner_margin">13dp</dimen>
   <dimen name="call_log_outer_margin">8dp</dimen>
   <dimen name="call_log_start_margin">8dp</dimen>
@@ -68,7 +67,7 @@
   <item name="contact_tile_height_to_width_ratio" type="dimen">76%</item>
   <dimen name="contact_tile_text_side_padding">12dp</dimen>
   <dimen name="contact_tile_text_bottom_padding">9dp</dimen>
-  <dimen name="favorites_row_top_padding">2dp</dimen>
+  <dimen name="favorites_row_top_padding">1dp</dimen>
   <dimen name="favorites_row_bottom_padding">0dp</dimen>
   <dimen name="favorites_row_start_padding">1dp</dimen>
 
@@ -143,6 +142,4 @@
   <dimen name="blocked_number_search_text_size">14sp</dimen>
   <dimen name="blocked_number_settings_description_text_size">14sp</dimen>
   <dimen name="blocked_number_header_height">48dp</dimen>
-
-  <dimen name="call_type_icon_size">12dp</dimen>
 </resources>
diff --git a/java/com/android/dialer/app/res/values/strings.xml b/java/com/android/dialer/app/res/values/strings.xml
index 689ee1b..66bf70f 100644
--- a/java/com/android/dialer/app/res/values/strings.xml
+++ b/java/com/android/dialer/app/res/values/strings.xml
@@ -55,9 +55,6 @@
   <!-- Label for action to unblock a number [CHAR LIMIT=48]-->
   <string name="action_unblock_number">Unblock number</string>
 
-  <!-- Menu item in call details used to remove a call or voicemail from the call log. -->
-  <string name="call_details_delete">Delete</string>
-
   <!-- Label for action to edit a number before calling it. [CHAR LIMIT=48] -->
   <string name="action_edit_number_before_call">Edit number before call</string>
 
@@ -94,7 +91,7 @@
   <!-- Missed call notification label, used when there are two or more missed calls -->
   <string name="notification_missedCallsTitle">Missed calls</string>
   <!-- Missed call notification message used when there are multiple missed calls -->
-  <string name="notification_missedCallsMsg"><xliff:g id="num_missed_calls">%s</xliff:g> missed calls</string>
+  <string name="notification_missedCallsMsg"><xliff:g id="num_missed_calls">%d</xliff:g> missed calls</string>
   <!-- Message for "call back" Action, which is displayed in the missed call notificaiton.
        The user will be able to call back to the person or the phone number.
        [CHAR LIMIT=18] -->
@@ -251,15 +248,13 @@
   <!-- Label for the dialer app setting page [CHAR LIMIT=30]-->
   <string name="dialer_settings_label">Settings</string>
 
+  <!-- Label for the simulator submenu. This is used to show actions that are useful for development
+       and testing. [CHAR LIMIT=30]-->
+  <string name="simulator_submenu_label">Simulator</string>
+
   <!-- Menu item to display all contacts [CHAR LIMIT=30] -->
   <string name="menu_allContacts">All contacts</string>
 
-  <!-- Title bar for call detail screen -->
-  <string name="callDetailTitle">Call details</string>
-
-  <!-- Toast for call detail screen when couldn't read the requested details -->
-  <string name="toast_call_detail_error">Details not available</string>
-
   <!-- Item label: jump to the in-call DTMF dialpad.
        (Part of a list of options shown in the dialer when another call
        is already in progress.) -->
@@ -275,52 +270,6 @@
        is already in progress.) -->
   <string name="dialer_addAnotherCall">Add call</string>
 
-  <!-- Title for incoming call type. [CHAR LIMIT=40] -->
-  <string name="type_incoming">Incoming call</string>
-
-  <!-- Title for incoming call which was transferred to another device. [CHAR LIMIT=60] -->
-  <string name="type_incoming_pulled">Incoming call transferred to another device</string>
-
-  <!-- Title for outgoing call type. [CHAR LIMIT=40] -->
-  <string name="type_outgoing">Outgoing call</string>
-
-  <!-- Title for outgoing call which was transferred to another device. [CHAR LIMIT=60] -->
-  <string name="type_outgoing_pulled">Outgoing call transferred to another device</string>
-
-  <!-- Title for missed call type. [CHAR LIMIT=40] -->
-  <string name="type_missed">Missed call</string>
-
-  <!-- Title for incoming video call in call details screen [CHAR LIMIT=60] -->
-  <string name="type_incoming_video">Incoming video call</string>
-
-  <!-- Title for incoming video call in call details screen which was transferred to another device.
-       [CHAR LIMIT=60] -->
-  <string name="type_incoming_video_pulled">Incoming video call transferred to another device</string>
-
-  <!-- Title for outgoing video call in call details screen [CHAR LIMIT=60] -->
-  <string name="type_outgoing_video">Outgoing video call</string>
-
-  <!-- Title for outgoing video call in call details screen which was transferred to another device.
-       [CHAR LIMIT=60] -->
-  <string name="type_outgoing_video_pulled">Outgoing video call transferred to another device</string>
-
-  <!-- Title for missed video call in call details screen [CHAR LIMIT=60] -->
-  <string name="type_missed_video">Missed video call</string>
-
-  <!-- Title for voicemail details screen -->
-  <string name="type_voicemail">Voicemail</string>
-
-  <!-- Title for rejected call type. [CHAR LIMIT=40] -->
-  <string name="type_rejected">Declined call</string>
-
-  <!-- Title for blocked call type. [CHAR LIMIT=40] -->
-  <string name="type_blocked">Blocked call</string>
-
-  <!-- Title for "answered elsewhere" call type. This will happen if a call was ringing
-       simultaneously on multiple devices, and the user answered it on a device other than the
-       current device. [CHAR LIMIT=60] -->
-  <string name="type_answered_elsewhere">Call answered on another device</string>
-
   <!-- Description for incoming calls going to voice mail vs. not -->
   <string name="actionIncomingCall">Incoming calls</string>
 
@@ -623,28 +572,10 @@
        [CHAR LIMIT=NONE] -->
   <string name="description_outgoing_call">Call to <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>, <xliff:g example="Mobile" id="typeOrLocation">^2</xliff:g>, <xliff:g example="2 min ago" id="timeOfCall">^3</xliff:g>, <xliff:g example="on SIM 1" id="phoneAccount">^4</xliff:g>.</string>
 
-  <!-- String describing the phone account the call was made on or to. This string will be used
-       in description_incoming_missed_call, description_incoming_answered_call, and
-       description_outgoing_call.
-       Note: AccessibilityServices uses this attribute to announce what the view represents.
-       [CHAR LIMIT=NONE] -->
-  <string name="description_phone_account">on <xliff:g example="SIM 1" id="phoneAccount">^1</xliff:g></string>
-
-  <!-- String describing the secondary line number the call was received via.
-       Note: AccessibilityServices use this attribute to announce what the view represents.
-       [CHAR LIMIT=NONE]-->
-  <string name="description_via_number">via <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g></string>
-
   <!-- TextView text item showing the secondary line number the call was received via.
        [CHAR LIMIT=NONE]-->
   <string name="call_log_via_number">via <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g></string>
 
-  <!-- String describing the PhoneAccount and via number that a call was received on, if both are
-       visible.
-       Note: AccessibilityServices use this attribute to announce what the view represents.
-       [CHAR LIMIT=NONE]-->
-  <string name="description_via_number_phone_account">on <xliff:g example="SIM 1" id="phoneAccount">%1$s</xliff:g>, via <xliff:g example="(555) 555-5555" id="number">%2$s</xliff:g></string>
-
   <!-- The order of the PhoneAccount and via number that a call was received on,
        if both are visible.
        [CHAR LIMIT=NONE]-->
@@ -827,6 +758,9 @@
   <!-- Label for the blocked numbers settings section [CHAR LIMIT=30] -->
   <string name="manage_blocked_numbers_label">Call blocking</string>
 
+  <!-- Label for the voicemail settings section [CHAR LIMIT=30] -->
+  <string name="voicemail_settings_label">Voicemail</string>
+
   <!-- Label for a section describing that call blocking is temporarily disabled because an
        emergency call was made. [CHAR LIMIT=50] -->
     <string name="blocked_numbers_disabled_emergency_header_label">
@@ -955,6 +889,6 @@
   <string name="spam_number_call_log_label">Spam</string>
 
   <!-- Shown as a message that notifies the user enriched calling isn't working -->
-  <string name="call_composer_connection_failed"><xliff:g id="feature">%1$s</xliff:g> unavailable right now</string>
+  <string name="call_composer_connection_failed"><xliff:g id="name">%1$s</xliff:g> is offline and can\'t be reached</string>
 
 </resources>
diff --git a/java/com/android/dialer/app/res/values/styles.xml b/java/com/android/dialer/app/res/values/styles.xml
index ac4422b..24521dd 100644
--- a/java/com/android/dialer/app/res/values/styles.xml
+++ b/java/com/android/dialer/app/res/values/styles.xml
@@ -111,11 +111,6 @@
     <item name="android:fastScrollTrackDrawable">@null</item>
   </style>
 
-  <style name="CallDetailActivityTheme" parent="DialtactsThemeWithoutActionBarOverlay">
-    <item name="android:windowBackground">@color/background_dialer_results</item>
-    <item name="android:actionOverflowButtonStyle">@style/DialtactsActionBarOverflowWhite</item>
-  </style>
-
   <style name="CallDetailActionItemStyle">
     <item name="android:foreground">?android:attr/selectableItemBackground</item>
     <item name="android:clickable">true</item>
diff --git a/java/com/android/dialer/app/settings/DialerSettingsActivity.java b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
index b046740..bbf1cfa 100644
--- a/java/com/android/dialer/app/settings/DialerSettingsActivity.java
+++ b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
@@ -33,8 +33,11 @@
 import com.android.dialer.blocking.FilteredNumberCompat;
 import com.android.dialer.compat.CompatUtils;
 import com.android.dialer.proguard.UsedByReflection;
+import com.android.voicemail.VoicemailComponent;
 import java.util.List;
 
+/** Activity for dialer settings. */
+@SuppressWarnings("FragmentInjection") // Activity not exported
 @UsedByReflection(value = "AndroidManifest-app.xml")
 public class DialerSettingsActivity extends AppCompatPreferenceActivity {
 
@@ -115,6 +118,16 @@
       target.add(blockedCallsHeader);
       migrationStatusOnBuildHeaders = FilteredNumberCompat.hasMigratedToNewBlocking(this);
     }
+
+    String voicemailSettingsFragment =
+        VoicemailComponent.get(this).getVoicemailClient().getSettingsFragment();
+    if (isPrimaryUser && voicemailSettingsFragment != null) {
+      Header voicemailSettings = new Header();
+      voicemailSettings.titleRes = R.string.voicemail_settings_label;
+      voicemailSettings.fragment = voicemailSettingsFragment;
+      target.add(voicemailSettings);
+    }
+
     if (isPrimaryUser
         && (TelephonyManagerCompat.isTtyModeSupported(telephonyManager)
             || TelephonyManagerCompat.isHearingAidCompatibilitySupported(telephonyManager))) {
diff --git a/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java b/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java
index fc6a376..f40ed27 100644
--- a/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java
+++ b/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java
@@ -30,7 +30,6 @@
 import android.widget.SeekBar;
 import android.widget.SeekBar.OnSeekBarChangeListener;
 import android.widget.TextView;
-import com.android.dialer.app.PhoneCallDetails;
 import com.android.dialer.app.R;
 import com.android.dialer.app.calllog.CallLogAsyncTaskUtil;
 import com.android.dialer.app.calllog.CallLogListItemViewHolder;
@@ -347,16 +346,10 @@
   }
 
   @Override
-  public void onDeleteCall() {}
-
-  @Override
   public void onDeleteVoicemail() {
     mPresenter.onVoicemailDeletedInDatabase();
   }
 
-  @Override
-  public void onGetCallDetails(PhoneCallDetails[] details) {}
-
   private String getString(int resId) {
     return mContext.getString(resId);
   }
diff --git a/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java b/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java
index 6570222..994160f 100644
--- a/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java
+++ b/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java
@@ -39,6 +39,7 @@
 import android.support.annotation.VisibleForTesting;
 import android.support.v4.content.FileProvider;
 import android.text.TextUtils;
+import android.view.View;
 import android.view.WindowManager.LayoutParams;
 import android.webkit.MimeTypeMap;
 import com.android.common.io.MoreCloseables;
@@ -47,8 +48,11 @@
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.AsyncTaskExecutor;
 import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.common.ConfigProviderBindings;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.constants.Constants;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
 import com.android.dialer.phonenumbercache.CallLogQuery;
 import com.google.common.io.ByteStreams;
 import java.io.File;
@@ -71,9 +75,9 @@
  * assumptions about the behaviors and lifecycle of the call log, in particular in the {@link
  * CallLogFragment} and {@link CallLogAdapter}.
  *
- * <p>This controls a single {@link com.android.dialer.voicemail.VoicemailPlaybackLayout}. A single
- * instance can be reused for different such layouts, using {@link #setPlaybackView}. This is to
- * facilitate reuse across different voicemail call log entries.
+ * <p>This controls a single {@link com.android.dialer.app.voicemail.VoicemailPlaybackLayout}. A
+ * single instance can be reused for different such layouts, using {@link #setPlaybackView}. This is
+ * to facilitate reuse across different voicemail call log entries.
  *
  * <p>This class is not thread safe. The thread policy for this class is thread-confinement, all
  * calls into this class from outside must be done from the main UI thread.
@@ -103,6 +107,8 @@
   private static final String IS_SPEAKERPHONE_ON_KEY =
       VoicemailPlaybackPresenter.class.getName() + ".IS_SPEAKER_PHONE_ON";
   private static final String VOICEMAIL_SHARE_FILE_NAME_DATE_FORMAT = "MM-dd-yy_hhmmaa";
+  private static final String CONFIG_SHARE_VOICEMAIL_ALLOWED = "share_voicemail_allowed";
+
   private static VoicemailPlaybackPresenter sInstance;
   private static ScheduledExecutorService mScheduledExecutorService;
   /**
@@ -138,6 +144,7 @@
   private PowerManager.WakeLock mProximityWakeLock;
   private VoicemailAudioManager mVoicemailAudioManager;
   private OnVoicemailDeletedListener mOnVoicemailDeletedListener;
+  private View shareVoicemailButtonView;
 
   /** Initialize variables which are activity-independent and state-independent. */
   protected VoicemailPlaybackPresenter(Activity activity) {
@@ -222,11 +229,17 @@
 
   /** Specify the view which this presenter controls and the voicemail to prepare to play. */
   public void setPlaybackView(
-      PlaybackView view, long rowId, Uri voicemailUri, final boolean startPlayingImmediately) {
+      PlaybackView view,
+      long rowId,
+      Uri voicemailUri,
+      final boolean startPlayingImmediately,
+      View shareVoicemailButtonView) {
     mRowId = rowId;
     mView = view;
     mView.setPresenter(this, voicemailUri);
     mView.onSpeakerphoneOn(mIsSpeakerphoneOn);
+    this.shareVoicemailButtonView = shareVoicemailButtonView;
+    showShareVoicemailButton(false);
 
     // Handles cases where the same entry is binded again when scrolling in list, or where
     // the MediaPlayer was retained after an orientation change.
@@ -236,6 +249,7 @@
       // media player.
       mPosition = mMediaPlayer.getCurrentPosition();
       onPrepared(mMediaPlayer);
+      showShareVoicemailButton(true);
     } else {
       if (!voicemailUri.equals(mVoicemailUri)) {
         mVoicemailUri = voicemailUri;
@@ -247,19 +261,17 @@
        * it if the content is not available.
        */
       checkForContent(
-          new OnContentCheckedListener() {
-            @Override
-            public void onContentChecked(boolean hasContent) {
-              if (hasContent) {
-                prepareContent();
-              } else {
-                if (startPlayingImmediately) {
-                  requestContent(PLAYBACK_REQUEST);
-                }
-                if (mView != null) {
-                  mView.resetSeekBar();
-                  mView.setClipPosition(0, mDuration.get());
-                }
+          hasContent -> {
+            if (hasContent) {
+              showShareVoicemailButton(true);
+              prepareContent();
+            } else {
+              if (startPlayingImmediately) {
+                requestContent(PLAYBACK_REQUEST);
+              }
+              if (mView != null) {
+                mView.resetSeekBar();
+                mView.setClipPosition(0, mDuration.get());
               }
             }
           });
@@ -547,6 +559,7 @@
 
     mPosition = 0;
     mIsPlaying = false;
+    showShareVoicemailButton(false);
   }
 
   /** After done playing the voicemail clip, reset the clip position to the start. */
@@ -600,18 +613,16 @@
        * timeout, but succeeded.
        */
       checkForContent(
-          new OnContentCheckedListener() {
-            @Override
-            public void onContentChecked(boolean hasContent) {
-              if (!hasContent) {
-                // No local content, download from server. Queue playing if the request was
-                // issued,
-                mIsPlaying = requestContent(PLAYBACK_REQUEST);
-              } else {
-                // Queue playing once the media play loaded the content.
-                mIsPlaying = true;
-                prepareContent();
-              }
+          hasContent -> {
+            if (!hasContent) {
+              // No local content, download from server. Queue playing if the request was
+              // issued,
+              mIsPlaying = requestContent(PLAYBACK_REQUEST);
+            } else {
+              showShareVoicemailButton(true);
+              // Queue playing once the media play loaded the content.
+              mIsPlaying = true;
+              prepareContent();
             }
           });
       return;
@@ -813,6 +824,20 @@
     sInstance = null;
   }
 
+  private void showShareVoicemailButton(boolean show) {
+    if (isShareVoicemailAllowed(mContext) && shareVoicemailButtonView != null) {
+      if (show) {
+        Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_VISIBLE);
+      }
+      LogUtil.d("VoicemailPlaybackPresenter.showShareVoicemailButton", "show: %b", show);
+      shareVoicemailButtonView.setVisibility(show ? View.VISIBLE : View.GONE);
+    }
+  }
+
+  private static boolean isShareVoicemailAllowed(Context context) {
+    return ConfigProviderBindings.get(context).getBoolean(CONFIG_SHARE_VOICEMAIL_ALLOWED, true);
+  }
+
   /**
    * Share voicemail to be opened by user selected apps. This method will collect information, copy
    * voicemail to a temporary file in background and launch a chooser intent to share it.
@@ -1041,6 +1066,7 @@
             public void onPostExecute(Boolean hasContent) {
               if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) {
                 mContext.getContentResolver().unregisterContentObserver(FetchResultHandler.this);
+                showShareVoicemailButton(true);
                 prepareContent();
               }
             }
diff --git a/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java b/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java
index e36406d..190426e 100644
--- a/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java
+++ b/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java
@@ -17,10 +17,18 @@
 package com.android.dialer.app.voicemail.error;
 
 import android.content.Context;
+import android.preference.PreferenceManager;
 import android.provider.VoicemailContract.Status;
 import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
 import com.android.dialer.app.voicemail.error.VoicemailErrorMessage.Action;
+import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.PerAccountSharedPreferences;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.voicemail.VoicemailClient;
+import com.android.voicemail.VoicemailComponent;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -32,14 +40,18 @@
 
   private static final float QUOTA_NEAR_FULL_THRESHOLD = 0.9f;
   private static final float QUOTA_FULL_THRESHOLD = 0.99f;
+  protected static final String VOICEMAIL_PROMO_DISMISSED_KEY =
+      "voicemail_archive_promo_was_dismissed";
+  protected static final String VOICEMAIL_PROMO_ALMOST_FULL_DISMISSED_KEY =
+      "voicemail_archive_almost_full_promo_was_dismissed";
 
   @Nullable
-  public static VoicemailErrorMessage create(Context context, VoicemailStatus status) {
+  public static VoicemailErrorMessage create(
+      Context context, VoicemailStatus status, final VoicemailStatusReader statusReader) {
     if (Status.CONFIGURATION_STATE_OK == status.configurationState
         && Status.DATA_CHANNEL_STATE_OK == status.dataChannelState
         && Status.NOTIFICATION_CHANNEL_STATE_OK == status.notificationChannelState) {
-
-      return checkQuota(context, status);
+      return checkQuota(context, status, statusReader);
     }
     // Initial state when the source is activating. Other error might be written into data and
     // notification channel during activation.
@@ -120,24 +132,98 @@
   }
 
   @Nullable
-  private static VoicemailErrorMessage checkQuota(Context context, VoicemailStatus status) {
+  private static VoicemailErrorMessage checkQuota(
+      Context context, VoicemailStatus status, VoicemailStatusReader statusReader) {
     if (status.quotaOccupied != Status.QUOTA_UNAVAILABLE
         && status.quotaTotal != Status.QUOTA_UNAVAILABLE) {
+
+      PhoneAccountHandle phoneAccountHandle = status.getPhoneAccountHandle();
+
+      VoicemailClient voicemailClient = VoicemailComponent.get(context).getVoicemailClient();
+
+      PerAccountSharedPreferences sharedPreferenceForAccount =
+          new PerAccountSharedPreferences(
+              context, phoneAccountHandle, PreferenceManager.getDefaultSharedPreferences(context));
+
+      boolean isVoicemailArchiveEnabled =
+          VoicemailComponent.get(context)
+              .getVoicemailClient()
+              .isVoicemailArchiveEnabled(context, phoneAccountHandle);
+
       if ((float) status.quotaOccupied / (float) status.quotaTotal >= QUOTA_FULL_THRESHOLD) {
-        return new VoicemailErrorMessage(
+        return createInboxErrorMessage(
+            context,
+            status,
+            status.getPhoneAccountHandle(),
+            statusReader,
+            sharedPreferenceForAccount,
+            voicemailClient,
+            isVoicemailArchiveEnabled,
+            context.getString(R.string.voicemail_error_inbox_full_turn_archive_on_title),
+            context.getString(R.string.voicemail_error_inbox_full_turn_archive_on_message),
             context.getString(R.string.voicemail_error_inbox_full_title),
-            context.getString(R.string.voicemail_error_inbox_full_message));
+            context.getString(R.string.voicemail_error_inbox_full_message),
+            VOICEMAIL_PROMO_DISMISSED_KEY);
       }
 
       if ((float) status.quotaOccupied / (float) status.quotaTotal >= QUOTA_NEAR_FULL_THRESHOLD) {
-        return new VoicemailErrorMessage(
+        return createInboxErrorMessage(
+            context,
+            status,
+            status.getPhoneAccountHandle(),
+            statusReader,
+            sharedPreferenceForAccount,
+            voicemailClient,
+            isVoicemailArchiveEnabled,
+            context.getString(R.string.voicemail_error_inbox_almost_full_turn_archive_on_title),
+            context.getString(R.string.voicemail_error_inbox_almost_full_turn_archive_on_message),
             context.getString(R.string.voicemail_error_inbox_near_full_title),
-            context.getString(R.string.voicemail_error_inbox_near_full_message));
+            context.getString(R.string.voicemail_error_inbox_near_full_message),
+            VOICEMAIL_PROMO_ALMOST_FULL_DISMISSED_KEY);
       }
     }
     return null;
   }
 
+  private static VoicemailErrorMessage createInboxErrorMessage(
+      Context context,
+      VoicemailStatus status,
+      PhoneAccountHandle phoneAccountHandle,
+      VoicemailStatusReader statusReader,
+      PerAccountSharedPreferences sharedPreferenceForAccount,
+      VoicemailClient voicemailClient,
+      boolean isVoicemailArchiveEnabled,
+      String promoTitle,
+      String promoMessage,
+      String nonPromoTitle,
+      String nonPromoMessage,
+      String preferenceKey) {
+
+    boolean wasPromoDismissed = sharedPreferenceForAccount.getBoolean(preferenceKey, false);
+
+    if (!wasPromoDismissed && !isVoicemailArchiveEnabled) {
+      logArchiveImpression(
+          context,
+          preferenceKey,
+          DialerImpression.Type.VVM_USER_SHOWN_VM_ALMOST_FULL_PROMO,
+          DialerImpression.Type.VVM_USER_SHOWN_VM_FULL_PROMO);
+      return new VoicemailErrorMessage(
+          promoTitle,
+          promoMessage,
+          VoicemailErrorMessage.createDismissTurnArchiveOnAction(
+              context, statusReader, sharedPreferenceForAccount, preferenceKey),
+          VoicemailErrorMessage.createTurnArchiveOnAction(
+              context, status, voicemailClient, phoneAccountHandle, preferenceKey));
+    } else {
+      logArchiveImpression(
+          context,
+          preferenceKey,
+          DialerImpression.Type.VVM_USER_SHOWN_VM_ALMOST_FULL_ERROR_MESSAGE,
+          DialerImpression.Type.VVM_USER_SHOWN_VM_FULL_ERROR_MESSAGE);
+      return new VoicemailErrorMessage(nonPromoTitle, nonPromoMessage);
+    }
+  }
+
   @Nullable
   private static VoicemailErrorMessage createNoSignalMessage(
       Context context, VoicemailStatus status) {
@@ -174,4 +260,15 @@
     }
     return new VoicemailErrorMessage(title, description, actions);
   }
+
+  protected static void logArchiveImpression(
+      Context context, String preference, int vmAlmostFullImpression, int vmFullImpression) {
+    if (preference.equals(VOICEMAIL_PROMO_DISMISSED_KEY)) {
+      Logger.get(context).logImpression(vmAlmostFullImpression);
+    } else if (preference.equals(VOICEMAIL_PROMO_ALMOST_FULL_DISMISSED_KEY)) {
+      Logger.get(context).logImpression(vmFullImpression);
+    } else {
+      throw Assert.createAssertionFailException("Invalid preference key " + preference);
+    }
+  }
 }
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java
index 6157200..f85d911 100644
--- a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java
@@ -22,12 +22,15 @@
 import android.provider.VoicemailContract;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
 import android.telephony.TelephonyManager;
 import android.view.View;
 import android.view.View.OnClickListener;
+import com.android.dialer.common.PerAccountSharedPreferences;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.logging.nano.DialerImpression;
 import com.android.dialer.util.CallUtil;
+import com.android.voicemail.VoicemailClient;
 import java.util.Arrays;
 import java.util.List;
 
@@ -175,4 +178,52 @@
           }
         });
   }
+
+  @NonNull
+  public static Action createTurnArchiveOnAction(
+      final Context context,
+      final VoicemailStatus status,
+      VoicemailClient voicemailClient,
+      PhoneAccountHandle phoneAccountHandle,
+      String preference) {
+    return new Action(
+        context.getString(R.string.voicemail_action_turn_archive_on),
+        new OnClickListener() {
+          @Override
+          public void onClick(View v) {
+            OmtpVoicemailMessageCreator.logArchiveImpression(
+                context,
+                preference,
+                DialerImpression.Type.VVM_USER_ENABLED_ARCHIVE_FROM_VM_FULL_PROMO,
+                DialerImpression.Type.VVM_USER_ENABLED_ARCHIVE_FROM_VM_ALMOST_FULL_PROMO);
+
+            voicemailClient.setVoicemailArchiveEnabled(context, phoneAccountHandle, true);
+            Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL);
+            intent.setPackage(status.sourcePackage);
+            context.sendBroadcast(intent);
+          }
+        });
+  }
+
+  @NonNull
+  public static Action createDismissTurnArchiveOnAction(
+      final Context context,
+      VoicemailStatusReader statusReader,
+      PerAccountSharedPreferences sharedPreferenceForAccount,
+      String preferenceKeyToUpdate) {
+    return new Action(
+        context.getString(R.string.voicemail_action_dimiss),
+        new OnClickListener() {
+          @Override
+          public void onClick(View v) {
+            OmtpVoicemailMessageCreator.logArchiveImpression(
+                context,
+                preferenceKeyToUpdate,
+                DialerImpression.Type.VVM_USER_DISMISSED_VM_FULL_PROMO,
+                DialerImpression.Type.VVM_USER_DISMISSED_VM_ALMOST_FULL_PROMO);
+            sharedPreferenceForAccount.edit().putBoolean(preferenceKeyToUpdate, true);
+            statusReader.refresh();
+          }
+        });
+  }
 }
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java
index 5ebef80..7dc18f0 100644
--- a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java
@@ -39,7 +39,7 @@
       case Vvm3VoicemailMessageCreator.VVM_TYPE_VVM3:
         return Vvm3VoicemailMessageCreator.create(context, status, statusReader);
       default:
-        return OmtpVoicemailMessageCreator.create(context, status);
+        return OmtpVoicemailMessageCreator.create(context, status, statusReader);
     }
   }
 }
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java b/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java
index a09941d..c429d6d 100644
--- a/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java
@@ -16,6 +16,7 @@
 
 package com.android.dialer.app.voicemail.error;
 
+import android.content.ComponentName;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
@@ -25,6 +26,7 @@
 import android.provider.Settings.Global;
 import android.provider.VoicemailContract.Status;
 import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
 import android.telephony.TelephonyManager;
 import com.android.dialer.database.VoicemailStatusQuery;
 
@@ -257,4 +259,9 @@
     }
     return cursor.getString(index);
   }
+
+  public PhoneAccountHandle getPhoneAccountHandle() {
+    return new PhoneAccountHandle(
+        ComponentName.unflattenFromString(phoneAccountComponentName), phoneAccountId);
+  }
 }
diff --git a/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java b/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java
index 6e9405c..356131b 100644
--- a/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java
+++ b/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java
@@ -269,7 +269,7 @@
           VoicemailErrorMessage.createSetPinAction(context));
     }
 
-    return OmtpVoicemailMessageCreator.create(context, status);
+    return OmtpVoicemailMessageCreator.create(context, status, statusReader);
   }
 
   @NonNull
diff --git a/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml b/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml
index 0dfb1c2..4a40857 100644
--- a/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml
+++ b/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml
@@ -48,7 +48,6 @@
 
       <TextView
         android:id="@+id/error_card_details"
-        android:autoLink="web"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:lineSpacingExtra="@dimen/alert_line_spacing"
diff --git a/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml b/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml
index 2b9d173..c193eaa 100644
--- a/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml
+++ b/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml
@@ -31,7 +31,6 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:paddingBottom="16dp"
-        android:autoLink="web"
         android:text="@string/verizon_terms_and_conditions_1.1_english"
         android:textColor="@color/secondary_text_color"
         android:textSize="@dimen/call_log_detail_text_size"/>
diff --git a/java/com/android/dialer/app/voicemail/error/res/values/strings.xml b/java/com/android/dialer/app/voicemail/error/res/values/strings.xml
index 1d39b9d..94d3dba 100644
--- a/java/com/android/dialer/app/voicemail/error/res/values/strings.xml
+++ b/java/com/android/dialer/app/voicemail/error/res/values/strings.xml
@@ -54,6 +54,11 @@
   <string name="voicemail_error_inbox_full_title">Can\'t receive new voicemails</string>
   <string name="voicemail_error_inbox_full_message">Your inbox is full. Try deleting some messages to receive new voicemail.</string>
 
+  <string name="voicemail_error_inbox_full_turn_archive_on_title">Turn on extra storage and backup</string>
+  <string name="voicemail_error_inbox_full_turn_archive_on_message">Your mailbox is full. To free up space, turn on extra storage so Google can manage and backup your voicemail messages.</string>
+
+  <string name="voicemail_error_inbox_almost_full_turn_archive_on_title">Turn on extra storage and backup</string>
+  <string name="voicemail_error_inbox_almost_full_turn_archive_on_message">Your mailbox is almost full. To free up space, turn on extra storage so Google can manage and backup your voicemail messages.</string>
 
   <string name="voicemail_error_pin_not_set_title">Set your voicemail PIN</string>
   <string name="voicemail_error_pin_not_set_message">You\'ll need a voicemail PIN anytime you call to access your voicemail.</string>
@@ -63,6 +68,8 @@
   <string name="voicemail_action_turn_off_airplane_mode">Airplane Mode Settings</string>
   <string name="voicemail_action_set_pin">Set PIN</string>
   <string name="voicemail_action_retry">Try Again</string>
+  <string name="voicemail_action_turn_archive_on">Turn on</string>
+  <string name="voicemail_action_dimiss">No Thanks</string>
   <string name="voicemail_action_sync">Sync</string>
   <string name="voicemail_action_call_voicemail">Call Voicemail</string>
   <string name="voicemail_action_call_customer_support">Call Customer Support</string>
diff --git a/java/com/android/dialer/app/widget/ActionBarController.java b/java/com/android/dialer/app/widget/ActionBarController.java
index 7fe056c..d0eb326 100644
--- a/java/com/android/dialer/app/widget/ActionBarController.java
+++ b/java/com/android/dialer/app/widget/ActionBarController.java
@@ -16,12 +16,9 @@
 package com.android.dialer.app.widget;
 
 import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.os.Bundle;
-import android.support.annotation.VisibleForTesting;
-import android.util.Log;
 import com.android.dialer.animation.AnimUtils.AnimationCallback;
-import com.android.dialer.app.DialtactsActivity;
+import com.android.dialer.common.LogUtil;
 
 /**
  * Controls the various animated properties of the actionBar: showing/hiding, fading/revealing, and
@@ -30,8 +27,6 @@
  */
 public class ActionBarController {
 
-  public static final boolean DEBUG = DialtactsActivity.DEBUG;
-  public static final String TAG = "ActionBarController";
   private static final String KEY_IS_SLID_UP = "key_actionbar_is_slid_up";
   private static final String KEY_IS_FADED_OUT = "key_actionbar_is_faded_out";
   private static final String KEY_IS_EXPANDED = "key_actionbar_is_expanded";
@@ -66,9 +61,8 @@
 
   /** Called when the user has tapped on the collapsed search box, to start a new search query. */
   public void onSearchBoxTapped() {
-    if (DEBUG) {
-      Log.d(TAG, "OnSearchBoxTapped: isInSearchUi " + mActivityUi.isInSearchUi());
-    }
+    LogUtil.d(
+        "ActionBarController.onSearchBoxTapped", "isInSearchUi " + mActivityUi.isInSearchUi());
     if (!mActivityUi.isInSearchUi()) {
       mSearchBox.expand(true /* animate */, true /* requestFocus */);
     }
@@ -76,16 +70,11 @@
 
   /** Called when search UI has been exited for some reason. */
   public void onSearchUiExited() {
-    if (DEBUG) {
-      Log.d(
-          TAG,
-          "OnSearchUIExited: isExpanded "
-              + mSearchBox.isExpanded()
-              + " isFadedOut: "
-              + mSearchBox.isFadedOut()
-              + " shouldShowActionBar: "
-              + mActivityUi.shouldShowActionBar());
-    }
+    LogUtil.d(
+        "ActionBarController.onSearchUIExited",
+        "isExpanded: %b, isFadedOut %b",
+        mSearchBox.isExpanded(),
+        mSearchBox.isFadedOut());
     if (mSearchBox.isExpanded()) {
       mSearchBox.collapse(true /* animate */);
     }
@@ -93,11 +82,7 @@
       mSearchBox.fadeIn();
     }
 
-    if (mActivityUi.shouldShowActionBar()) {
-      slideActionBar(false /* slideUp */, false /* animate */);
-    } else {
-      slideActionBar(true /* slideUp */, false /* animate */);
-    }
+    slideActionBar(false /* slideUp */, false /* animate */);
   }
 
   /**
@@ -105,18 +90,13 @@
    * state changes have actually occurred.
    */
   public void onDialpadDown() {
-    if (DEBUG) {
-      Log.d(
-          TAG,
-          "OnDialpadDown: isInSearchUi "
-              + mActivityUi.isInSearchUi()
-              + " hasSearchQuery: "
-              + mActivityUi.hasSearchQuery()
-              + " isFadedOut: "
-              + mSearchBox.isFadedOut()
-              + " isExpanded: "
-              + mSearchBox.isExpanded());
-    }
+    LogUtil.d(
+        "ActionBarController.onDialpadDown",
+        "isInSearchUi: %b, hasSearchQuery: %b, isFadedOut: %b, isExpanded: %b",
+        mActivityUi.isInSearchUi(),
+        mActivityUi.hasSearchQuery(),
+        mSearchBox.isFadedOut(),
+        mSearchBox.isExpanded());
     if (mActivityUi.isInSearchUi()) {
       if (mActivityUi.hasSearchQuery()) {
         if (mSearchBox.isFadedOut()) {
@@ -137,9 +117,7 @@
    * state changes have actually occurred.
    */
   public void onDialpadUp() {
-    if (DEBUG) {
-      Log.d(TAG, "OnDialpadUp: isInSearchUi " + mActivityUi.isInSearchUi());
-    }
+    LogUtil.d("ActionBarController.onDialpadUp", "isInSearchUi " + mActivityUi.isInSearchUi());
     if (mActivityUi.isInSearchUi()) {
       slideActionBar(true /* slideUp */, true /* animate */);
     } else {
@@ -149,18 +127,14 @@
   }
 
   public void slideActionBar(boolean slideUp, boolean animate) {
-    if (DEBUG) {
-      Log.d(TAG, "Sliding actionBar - up: " + slideUp + " animate: " + animate);
-    }
+    LogUtil.d("ActionBarController.slidingActionBar", "up: %b, animate: %b", slideUp, animate);
+
     if (animate) {
       ValueAnimator animator = slideUp ? ValueAnimator.ofFloat(0, 1) : ValueAnimator.ofFloat(1, 0);
       animator.addUpdateListener(
-          new AnimatorUpdateListener() {
-            @Override
-            public void onAnimationUpdate(ValueAnimator animation) {
-              final float value = (float) animation.getAnimatedValue();
-              setHideOffset((int) (mActivityUi.getActionBarHeight() * value));
-            }
+          animation -> {
+            final float value = (float) animation.getAnimatedValue();
+            setHideOffset((int) (mActivityUi.getActionBarHeight() * value));
           });
       animator.start();
     } else {
@@ -173,20 +147,11 @@
     mSearchBox.animate().alpha(alphaValue).start();
   }
 
-  /** @return The offset the action bar is being translated upwards by */
-  public int getHideOffset() {
-    return mActivityUi.getActionBarHideOffset();
-  }
-
   public void setHideOffset(int offset) {
     mIsActionBarSlidUp = offset >= mActivityUi.getActionBarHeight();
     mActivityUi.setActionBarHideOffset(offset);
   }
 
-  public int getActionBarHeight() {
-    return mActivityUi.getActionBarHeight();
-  }
-
   /** Saves the current state of the action bar into a provided {@link Bundle} */
   public void saveInstanceState(Bundle outState) {
     outState.putBoolean(KEY_IS_SLID_UP, mIsActionBarSlidUp);
@@ -225,23 +190,14 @@
     slideActionBar(mIsActionBarSlidUp /* slideUp */, false /* animate */);
   }
 
-  @VisibleForTesting
-  public boolean getIsActionBarSlidUp() {
-    return mIsActionBarSlidUp;
-  }
-
   public interface ActivityUi {
 
     boolean isInSearchUi();
 
     boolean hasSearchQuery();
 
-    boolean shouldShowActionBar();
-
     int getActionBarHeight();
 
-    int getActionBarHideOffset();
-
     void setActionBarHideOffset(int offset);
   }
 }
diff --git a/java/com/android/dialer/backup/AndroidManifest.xml b/java/com/android/dialer/backup/AndroidManifest.xml
index cfdb3d9..1cbbe53 100644
--- a/java/com/android/dialer/backup/AndroidManifest.xml
+++ b/java/com/android/dialer/backup/AndroidManifest.xml
@@ -21,7 +21,6 @@
     android:backupAgent="com.android.dialer.backup.DialerBackupAgent"
     android:fullBackupOnly="true"
     android:restoreAnyVersion="true"
-    android:name="com.android.dialer.app.DialerApplication"
     />
 
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/java/com/android/dialer/backup/DialerBackupAgent.java b/java/com/android/dialer/backup/DialerBackupAgent.java
index 391a93f..2f8684a 100644
--- a/java/com/android/dialer/backup/DialerBackupAgent.java
+++ b/java/com/android/dialer/backup/DialerBackupAgent.java
@@ -31,6 +31,7 @@
 import android.provider.CallLog.Calls;
 import android.provider.VoicemailContract;
 import android.provider.VoicemailContract.Voicemails;
+import android.telecom.PhoneAccountHandle;
 import android.util.Pair;
 import com.android.dialer.backup.nano.VoicemailInfo;
 import com.android.dialer.common.Assert;
@@ -42,6 +43,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.util.List;
 import java.util.Locale;
 
 /**
@@ -100,9 +102,11 @@
         ConfigProviderBindings.get(this).getBoolean("enable_autobackup", true);
     boolean vmBackupEnabled =
         ConfigProviderBindings.get(this).getBoolean("enable_vm_backup", false);
+    List<PhoneAccountHandle> phoneAccountsToArchive =
+        DialerBackupUtils.getPhoneAccountsToArchive(this);
 
     if (autoBackupEnabled) {
-      if (!maxVoicemailBackupReached && vmBackupEnabled) {
+      if (!maxVoicemailBackupReached && vmBackupEnabled && !phoneAccountsToArchive.isEmpty()) {
         voicemailsBackedupSoFar = 0;
         sizeOfVoicemailsBackedupSoFar = 0;
 
@@ -123,9 +127,12 @@
                 uri,
                 null,
                 String.format(
-                    "(%s = ? AND deleted = 0 AND  %s = ?)", Calls.TYPE, Voicemails.SOURCE_PACKAGE),
+                    "(%s = ? AND deleted = 0 AND  %s = ? AND ?)",
+                    Calls.TYPE, Voicemails.SOURCE_PACKAGE),
                 new String[] {
-                  Integer.toString(CallLog.Calls.VOICEMAIL_TYPE), VOICEMAIL_SOURCE_PACKAGE
+                  Integer.toString(CallLog.Calls.VOICEMAIL_TYPE),
+                  VOICEMAIL_SOURCE_PACKAGE,
+                  DialerBackupUtils.getPhoneAccountClause(phoneAccountsToArchive)
                 },
                 ORDER_BY_DATE,
                 null)) {
@@ -150,11 +157,12 @@
       LogUtil.i(
           "DialerBackupAgent.onFullBackup",
           "vm files backed up: %d, vm size backed up:%d, "
-              + "max vm backup reached:%b, vm backup enabled:%b",
+              + "max vm backup reached:%b, vm backup enabled:%b phone accounts to archive: %d",
           voicemailsBackedupSoFar,
           sizeOfVoicemailsBackedupSoFar,
           maxVoicemailBackupReached,
-          vmBackupEnabled);
+          vmBackupEnabled,
+          phoneAccountsToArchive.size());
       super.onFullBackup(data);
       Logger.get(this).logImpression(DialerImpression.Type.BACKUP_FULL_BACKED_UP);
     } else {
diff --git a/java/com/android/dialer/backup/DialerBackupUtils.java b/java/com/android/dialer/backup/DialerBackupUtils.java
index ff0cb4f..410772f 100644
--- a/java/com/android/dialer/backup/DialerBackupUtils.java
+++ b/java/com/android/dialer/backup/DialerBackupUtils.java
@@ -27,10 +27,14 @@
 import android.provider.VoicemailContract.Voicemails;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
 import android.util.Pair;
 import com.android.dialer.backup.nano.VoicemailInfo;
+import com.android.dialer.common.Assert;
 import com.android.dialer.common.ConfigProviderBindings;
 import com.android.dialer.common.LogUtil;
+import com.android.voicemail.VoicemailComponent;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Files;
 import com.google.protobuf.nano.MessageNano;
@@ -40,6 +44,8 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
 
 /** Helper functions for DialerBackupAgent */
 public class DialerBackupUtils {
@@ -317,4 +323,42 @@
     }
     return false;
   }
+
+  public static String getPhoneAccountClause(List<PhoneAccountHandle> phoneAccountsToArchive) {
+    Assert.checkArgument(!phoneAccountsToArchive.isEmpty());
+    StringBuilder whereQuery = new StringBuilder();
+
+    whereQuery.append("(");
+
+    for (int i = 0; i < phoneAccountsToArchive.size(); i++) {
+      whereQuery.append(
+          Voicemails.PHONE_ACCOUNT_ID + " = " + phoneAccountsToArchive.get(i).getId());
+
+      if (phoneAccountsToArchive.size() > 1 && i < phoneAccountsToArchive.size() - 1) {
+        whereQuery.append(" OR ");
+      }
+    }
+    whereQuery.append(")");
+    return whereQuery.toString();
+  }
+
+  public static List<PhoneAccountHandle> getPhoneAccountsToArchive(Context context) {
+    List<PhoneAccountHandle> phoneAccountsToBackUp = new ArrayList<>();
+
+    for (PhoneAccountHandle handle :
+        context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) {
+
+      if (VoicemailComponent.get(context)
+          .getVoicemailClient()
+          .isVoicemailArchiveEnabled(context, handle)) {
+        phoneAccountsToBackUp.add(handle);
+        LogUtil.i(
+            "DialerBackupUtils.getPhoneAccountsToArchive", "enabled for: " + handle.toString());
+      } else {
+        LogUtil.i(
+            "DialerBackupUtils.getPhoneAccountsToArchive", "not enabled for: " + handle.toString());
+      }
+    }
+    return phoneAccountsToBackUp;
+  }
 }
diff --git a/java/com/android/dialer/backup/proto/VoicemailInfo.java b/java/com/android/dialer/backup/nano/VoicemailInfo.java
similarity index 62%
rename from java/com/android/dialer/backup/proto/VoicemailInfo.java
rename to java/com/android/dialer/backup/nano/VoicemailInfo.java
index 9ff8423..f11595e 100644
--- a/java/com/android/dialer/backup/proto/VoicemailInfo.java
+++ b/java/com/android/dialer/backup/nano/VoicemailInfo.java
@@ -18,16 +18,17 @@
 
 package com.android.dialer.backup.nano;
 
+/** This file is autogenerated, but javadoc required. */
 @SuppressWarnings("hiding")
-public final class VoicemailInfo extends
-    com.google.protobuf.nano.ExtendableMessageNano<VoicemailInfo> {
+public final class VoicemailInfo
+    extends com.google.protobuf.nano.ExtendableMessageNano<VoicemailInfo> {
 
   private static volatile VoicemailInfo[] _emptyArray;
+
   public static VoicemailInfo[] emptyArray() {
     // Lazily initializes the empty array
     if (_emptyArray == null) {
-      synchronized (
-          com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+      synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
         if (_emptyArray == null) {
           _emptyArray = new VoicemailInfo[0];
         }
@@ -178,7 +179,8 @@
     if (this.voicemailUri != null && !this.voicemailUri.equals("")) {
       output.writeString(17, this.voicemailUri);
     }
-    if (!java.util.Arrays.equals(this.encodedVoicemailKey, com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES)) {
+    if (!java.util.Arrays.equals(
+        this.encodedVoicemailKey, com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES)) {
       output.writeBytes(18, this.encodedVoicemailKey);
     }
     if (this.archived != null && !this.archived.equals("")) {
@@ -191,175 +193,196 @@
   protected int computeSerializedSize() {
     int size = super.computeSerializedSize();
     if (this.date != null && !this.date.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(1, this.date);
+      size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(1, this.date);
     }
     if (this.deleted != null && !this.deleted.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(2, this.deleted);
+      size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(2, this.deleted);
     }
     if (this.dirty != null && !this.dirty.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(3, this.dirty);
+      size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(3, this.dirty);
     }
     if (this.dirType != null && !this.dirType.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(4, this.dirType);
+      size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(4, this.dirType);
     }
     if (this.duration != null && !this.duration.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(5, this.duration);
+      size +=
+          com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(5, this.duration);
     }
     if (this.hasContent != null && !this.hasContent.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(6, this.hasContent);
+      size +=
+          com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(6, this.hasContent);
     }
     if (this.isRead != null && !this.isRead.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(7, this.isRead);
+      size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(7, this.isRead);
     }
     if (this.itemType != null && !this.itemType.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(8, this.itemType);
+      size +=
+          com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(8, this.itemType);
     }
     if (this.lastModified != null && !this.lastModified.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(9, this.lastModified);
+      size +=
+          com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+              9, this.lastModified);
     }
     if (this.mimeType != null && !this.mimeType.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(10, this.mimeType);
+      size +=
+          com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(10, this.mimeType);
     }
     if (this.number != null && !this.number.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(11, this.number);
+      size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(11, this.number);
     }
     if (this.phoneAccountComponentName != null && !this.phoneAccountComponentName.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(12, this.phoneAccountComponentName);
+      size +=
+          com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+              12, this.phoneAccountComponentName);
     }
     if (this.phoneAccountId != null && !this.phoneAccountId.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(13, this.phoneAccountId);
+      size +=
+          com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+              13, this.phoneAccountId);
     }
     if (this.sourceData != null && !this.sourceData.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(14, this.sourceData);
+      size +=
+          com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(14, this.sourceData);
     }
     if (this.sourcePackage != null && !this.sourcePackage.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(15, this.sourcePackage);
+      size +=
+          com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+              15, this.sourcePackage);
     }
     if (this.transcription != null && !this.transcription.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(16, this.transcription);
+      size +=
+          com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+              16, this.transcription);
     }
     if (this.voicemailUri != null && !this.voicemailUri.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(17, this.voicemailUri);
+      size +=
+          com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+              17, this.voicemailUri);
     }
-    if (!java.util.Arrays.equals(this.encodedVoicemailKey, com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES)) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeBytesSize(18, this.encodedVoicemailKey);
+    if (!java.util.Arrays.equals(
+        this.encodedVoicemailKey, com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES)) {
+      size +=
+          com.google.protobuf.nano.CodedOutputByteBufferNano.computeBytesSize(
+              18, this.encodedVoicemailKey);
     }
     if (this.archived != null && !this.archived.equals("")) {
-      size += com.google.protobuf.nano.CodedOutputByteBufferNano
-          .computeStringSize(19, this.archived);
+      size +=
+          com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(19, this.archived);
     }
     return size;
   }
 
   @Override
-  public VoicemailInfo mergeFrom(
-          com.google.protobuf.nano.CodedInputByteBufferNano input)
+  public VoicemailInfo mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
       throws java.io.IOException {
     while (true) {
       int tag = input.readTag();
       switch (tag) {
         case 0:
           return this;
-        default: {
-          if (!super.storeUnknownField(input, tag)) {
-            return this;
+        default:
+          {
+            if (!super.storeUnknownField(input, tag)) {
+              return this;
+            }
+            break;
           }
-          break;
-        }
-        case 10: {
-          this.date = input.readString();
-          break;
-        }
-        case 18: {
-          this.deleted = input.readString();
-          break;
-        }
-        case 26: {
-          this.dirty = input.readString();
-          break;
-        }
-        case 34: {
-          this.dirType = input.readString();
-          break;
-        }
-        case 42: {
-          this.duration = input.readString();
-          break;
-        }
-        case 50: {
-          this.hasContent = input.readString();
-          break;
-        }
-        case 58: {
-          this.isRead = input.readString();
-          break;
-        }
-        case 66: {
-          this.itemType = input.readString();
-          break;
-        }
-        case 74: {
-          this.lastModified = input.readString();
-          break;
-        }
-        case 82: {
-          this.mimeType = input.readString();
-          break;
-        }
-        case 90: {
-          this.number = input.readString();
-          break;
-        }
-        case 98: {
-          this.phoneAccountComponentName = input.readString();
-          break;
-        }
-        case 106: {
-          this.phoneAccountId = input.readString();
-          break;
-        }
-        case 114: {
-          this.sourceData = input.readString();
-          break;
-        }
-        case 122: {
-          this.sourcePackage = input.readString();
-          break;
-        }
-        case 130: {
-          this.transcription = input.readString();
-          break;
-        }
-        case 138: {
-          this.voicemailUri = input.readString();
-          break;
-        }
-        case 146: {
-          this.encodedVoicemailKey = input.readBytes();
-          break;
-        }
-        case 154: {
-          this.archived = input.readString();
-          break;
-        }
+        case 10:
+          {
+            this.date = input.readString();
+            break;
+          }
+        case 18:
+          {
+            this.deleted = input.readString();
+            break;
+          }
+        case 26:
+          {
+            this.dirty = input.readString();
+            break;
+          }
+        case 34:
+          {
+            this.dirType = input.readString();
+            break;
+          }
+        case 42:
+          {
+            this.duration = input.readString();
+            break;
+          }
+        case 50:
+          {
+            this.hasContent = input.readString();
+            break;
+          }
+        case 58:
+          {
+            this.isRead = input.readString();
+            break;
+          }
+        case 66:
+          {
+            this.itemType = input.readString();
+            break;
+          }
+        case 74:
+          {
+            this.lastModified = input.readString();
+            break;
+          }
+        case 82:
+          {
+            this.mimeType = input.readString();
+            break;
+          }
+        case 90:
+          {
+            this.number = input.readString();
+            break;
+          }
+        case 98:
+          {
+            this.phoneAccountComponentName = input.readString();
+            break;
+          }
+        case 106:
+          {
+            this.phoneAccountId = input.readString();
+            break;
+          }
+        case 114:
+          {
+            this.sourceData = input.readString();
+            break;
+          }
+        case 122:
+          {
+            this.sourcePackage = input.readString();
+            break;
+          }
+        case 130:
+          {
+            this.transcription = input.readString();
+            break;
+          }
+        case 138:
+          {
+            this.voicemailUri = input.readString();
+            break;
+          }
+        case 146:
+          {
+            this.encodedVoicemailKey = input.readBytes();
+            break;
+          }
+        case 154:
+          {
+            this.archived = input.readString();
+            break;
+          }
       }
     }
   }
@@ -369,8 +392,7 @@
     return com.google.protobuf.nano.MessageNano.mergeFrom(new VoicemailInfo(), data);
   }
 
-  public static VoicemailInfo parseFrom(
-          com.google.protobuf.nano.CodedInputByteBufferNano input)
+  public static VoicemailInfo parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
       throws java.io.IOException {
     return new VoicemailInfo().mergeFrom(input);
   }
diff --git a/java/com/android/dialer/binary/aosp/AndroidManifest.xml b/java/com/android/dialer/binary/aosp/AndroidManifest.xml
new file mode 100644
index 0000000..63edb83
--- /dev/null
+++ b/java/com/android/dialer/binary/aosp/AndroidManifest.xml
@@ -0,0 +1,116 @@
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  coreApp="true"
+  package="com.android.dialer"
+  android:versionCode="100000"
+  android:versionName="10.0">
+
+  <uses-sdk
+    android:minSdkVersion="23"
+    android:targetSdkVersion="25"/>
+
+  <uses-permission android:name="android.permission.CALL_PHONE"/>
+  <uses-permission android:name="android.permission.READ_CONTACTS"/>
+  <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
+  <uses-permission android:name="android.permission.READ_CALL_LOG"/>
+  <uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
+  <uses-permission android:name="android.permission.READ_PROFILE"/>
+  <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
+  <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
+  <uses-permission android:name="android.permission.GET_ACCOUNTS_PRIVILEGED"/>
+  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+  <uses-permission android:name="android.permission.INTERNET"/>
+  <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
+  <uses-permission android:name="android.permission.NFC"/>
+  <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+  <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
+  <uses-permission android:name="android.permission.MODIFY_PHONE_STATE"/>
+  <uses-permission android:name="android.permission.WAKE_LOCK"/>
+  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+  <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
+  <uses-permission android:name="android.permission.USE_CREDENTIALS"/>
+  <uses-permission android:name="android.permission.VIBRATE"/>
+  <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
+  <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL"/>
+  <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL"/>
+  <uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL"/>
+  <uses-permission android:name="android.permission.ALLOW_ANY_CODEC_FOR_PLAYBACK"/>
+  <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
+  <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+  <uses-permission android:name="android.permission.BROADCAST_STICKY"/>
+  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+  <uses-permission android:name="android.permission.SEND_SMS"/>
+
+  <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE"/>
+  <!-- We use this to disable the status bar buttons of home, back and recent
+  during an incoming call. By doing so this allows us to not show the user
+  is viewing the activity in full screen alert, on a fresh system/factory
+  reset state of the app. -->
+  <uses-permission android:name="android.permission.STATUS_BAR"/>
+  <uses-permission android:name="android.permission.CAMERA"/>
+
+  <!-- This tells the activity manager to not delay any of our activity
+   start requests, even if they happen immediately after the user
+   presses home. -->
+  <uses-permission android:name="android.permission.STOP_APP_SWITCHES"/>
+
+  <!-- Permissions needed for badger count showing on launch icon. -->
+
+  <!--for Samsung-->
+  <uses-permission android:name="com.sec.android.provider.badge.permission.READ"/>
+  <uses-permission android:name="com.sec.android.provider.badge.permission.WRITE"/>
+
+  <!--for htc-->
+  <uses-permission android:name="com.htc.launcher.permission.READ_SETTINGS"/>
+  <uses-permission android:name="com.htc.launcher.permission.UPDATE_SHORTCUT"/>
+
+  <!--for sony-->
+  <uses-permission android:name="com.sonyericsson.home.permission.BROADCAST_BADGE"/>
+  <uses-permission android:name="com.sonymobile.home.permission.PROVIDER_INSERT_BADGE"/>
+
+  <!--for apex-->
+  <uses-permission android:name="com.anddoes.launcher.permission.UPDATE_COUNT"/>
+
+  <!--for solid-->
+  <uses-permission android:name="com.majeur.launcher.permission.UPDATE_BADGE"/>
+
+  <!--for huawei-->
+  <uses-permission android:name="com.huawei.android.launcher.permission.CHANGE_BADGE"/>
+  <uses-permission android:name="com.huawei.android.launcher.permission.READ_SETTINGS"/>
+  <uses-permission android:name="com.huawei.android.launcher.permission.WRITE_SETTINGS"/>
+
+  <!--for ZUK-->
+  <uses-permission android:name="android.permission.READ_APP_BADGE"/>
+
+  <!--for OPPO-->
+  <uses-permission android:name="com.oppo.launcher.permission.READ_SETTINGS"/>
+  <uses-permission android:name="com.oppo.launcher.permission.WRITE_SETTINGS"/>
+
+  <application
+    android:backupAgent='com.android.dialer.backup.DialerBackupAgent'
+    android:fullBackupOnly="true"
+    android:restoreAnyVersion="true"
+    android:hardwareAccelerated="true"
+    android:icon="@mipmap/ic_launcher_phone"
+    android:label="@string/applicationLabel"
+    android:name="com.android.dialer.binary.aosp.AospDialerApplication"
+    android:supportsRtl="true"
+    android:usesCleartextTraffic="false">
+  </application>
+
+</manifest>
diff --git a/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java b/java/com/android/dialer/binary/aosp/AospDialerApplication.java
similarity index 60%
copy from java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java
copy to java/com/android/dialer/binary/aosp/AospDialerApplication.java
index e1f2cdc..f657a39 100644
--- a/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java
+++ b/java/com/android/dialer/binary/aosp/AospDialerApplication.java
@@ -14,17 +14,17 @@
  * limitations under the License
  */
 
-package com.android.dialer.buildtype;
+package com.android.dialer.binary.aosp;
 
-import com.android.dialer.proguard.UsedByReflection;
+import android.support.annotation.NonNull;
+import com.android.dialer.binary.common.DialerApplication;
+import com.android.dialer.inject.ContextModule;
 
-/** Gets the build type. */
-@UsedByReflection(value = "BuildType.java")
-public class BuildTypeAccessorImpl implements BuildTypeAccessor {
+/**
+ * The application class for the AOSP Dialer. This is a version of the Dialer app that has no
+ * dependency on Google Play Services.
+ */
+public class AospDialerApplication extends DialerApplication {
 
-  @Override
-  @BuildType.Type
-  public int getBuildType() {
-    return BuildType.DOGFOOD;
-  }
+
 }
diff --git a/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java b/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java
new file mode 100644
index 0000000..8628e90
--- /dev/null
+++ b/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.binary.aosp;
+
+import com.android.dialer.binary.basecomponent.BaseDialerRootComponent;
+import com.android.dialer.enrichedcall.stub.StubEnrichedCallModule;
+import com.android.dialer.inject.ContextModule;
+import com.android.dialer.simulator.impl.SimulatorModule;
+import com.android.incallui.calllocation.stub.StubCallLocationModule;
+import com.android.incallui.maps.stub.StubMapsModule;
+import com.android.voicemail.impl.VoicemailModule;
+import dagger.Component;
+import javax.inject.Singleton;
+
+/** Root component for the AOSP Dialer application. */
+public interface AospDialerRootComponent extends BaseDialerRootComponent {}
diff --git a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java
new file mode 100644
index 0000000..907671b
--- /dev/null
+++ b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.binary.basecomponent;
+
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
+import com.android.dialer.simulator.SimulatorComponent;
+import com.android.incallui.calllocation.CallLocationComponent;
+import com.android.incallui.maps.MapsComponent;
+import com.android.voicemail.VoicemailComponent;
+
+/**
+ * Base class for the core application-wide {@link Component}. All variants of the Dialer app should
+ * extend from this component.
+ */
+public interface BaseDialerRootComponent
+    extends CallLocationComponent.HasComponent,
+        EnrichedCallComponent.HasComponent,
+        MapsComponent.HasComponent,
+        SimulatorComponent.HasComponent,
+        VoicemailComponent.HasComponent {}
diff --git a/java/com/android/dialer/binary/common/DialerApplication.java b/java/com/android/dialer/binary/common/DialerApplication.java
new file mode 100644
index 0000000..c0be432
--- /dev/null
+++ b/java/com/android/dialer/binary/common/DialerApplication.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.binary.common;
+
+import android.app.Application;
+import android.os.Trace;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import com.android.dialer.blocking.BlockedNumbersAutoMigrator;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+
+/** A common application subclass for all Dialer build variants. */
+public abstract class DialerApplication extends Application {
+
+  private volatile Object rootComponent;
+
+  @Override
+  public void onCreate() {
+    Trace.beginSection("DialerApplication.onCreate");
+    super.onCreate();
+    new BlockedNumbersAutoMigrator(
+            this,
+            PreferenceManager.getDefaultSharedPreferences(this),
+            new FilteredNumberAsyncQueryHandler(this))
+        .autoMigrate();
+    Trace.endSection();
+  }
+
+}
diff --git a/java/com/android/dialer/blocking/FilteredNumbersUtil.java b/java/com/android/dialer/blocking/FilteredNumbersUtil.java
index 61ecf18..f09370e 100644
--- a/java/com/android/dialer/blocking/FilteredNumbersUtil.java
+++ b/java/com/android/dialer/blocking/FilteredNumbersUtil.java
@@ -36,6 +36,8 @@
 import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.logging.nano.InteractionEvent;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
 import com.android.dialer.util.PermissionsUtil;
 import java.util.concurrent.TimeUnit;
 
@@ -43,7 +45,8 @@
 public class FilteredNumbersUtil {
 
   public static final String CALL_BLOCKING_NOTIFICATION_TAG = "call_blocking";
-  public static final int CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID = 10;
+  public static final int CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID =
+      R.id.notification_call_blocking_disabled_by_emergency_call;
   // Pref key for storing the time of end of the last emergency call in milliseconds after epoch.
   protected static final String LAST_EMERGENCY_CALL_MS_PREF_KEY = "last_emergency_call_ms";
   // Pref key for storing whether a notification has been dispatched to notify the user that call
@@ -289,6 +292,7 @@
                         context.getString(R.string.call_blocking_disabled_notification_text))
                     .setAutoCancel(true);
 
+            NotificationChannelManager.applyChannel(builder, context, Channel.MISC, null);
             builder.setContentIntent(
                 PendingIntent.getActivity(
                     context,
diff --git a/java/com/android/dialer/buildtype/BuildType.java b/java/com/android/dialer/buildtype/BuildType.java
index c0a54a5..6b6bc29 100644
--- a/java/com/android/dialer/buildtype/BuildType.java
+++ b/java/com/android/dialer/buildtype/BuildType.java
@@ -28,7 +28,7 @@
   /** The type of build. */
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({
-    BUGFOOD, FISHFOOD, DOGFOOD, RELEASE,
+    BUGFOOD, FISHFOOD, DOGFOOD, RELEASE, TEST,
   })
   public @interface Type {}
 
@@ -36,6 +36,7 @@
   public static final int FISHFOOD = 2;
   public static final int DOGFOOD = 3;
   public static final int RELEASE = 4;
+  public static final int TEST = 5;
 
   private static int cachedBuildType;
   private static boolean didInitializeBuildType;
diff --git a/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java b/java/com/android/dialer/buildtype/release/BuildTypeAccessorImpl.java
similarity index 96%
rename from java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java
rename to java/com/android/dialer/buildtype/release/BuildTypeAccessorImpl.java
index e1f2cdc..70b9f9e 100644
--- a/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java
+++ b/java/com/android/dialer/buildtype/release/BuildTypeAccessorImpl.java
@@ -25,6 +25,6 @@
   @Override
   @BuildType.Type
   public int getBuildType() {
-    return BuildType.DOGFOOD;
+    return BuildType.RELEASE;
   }
 }
diff --git a/java/com/android/dialer/callcomposer/AndroidManifest.xml b/java/com/android/dialer/callcomposer/AndroidManifest.xml
index c99f22b..369db6f 100644
--- a/java/com/android/dialer/callcomposer/AndroidManifest.xml
+++ b/java/com/android/dialer/callcomposer/AndroidManifest.xml
@@ -20,9 +20,8 @@
   <application>
     <activity
       android:name="com.android.dialer.callcomposer.CallComposerActivity"
-      android:exported="false"
+      android:exported="true"
       android:theme="@style/Theme.AppCompat.CallComposer"
-      android:windowSoftInputMode="adjustResize"
-      android:screenOrientation="portrait"/>
+      android:windowSoftInputMode="adjustPan"/>
   </application>
 </manifest>
diff --git a/java/com/android/dialer/callcomposer/CallComposerActivity.java b/java/com/android/dialer/callcomposer/CallComposerActivity.java
index eef3d21..f73563f 100644
--- a/java/com/android/dialer/callcomposer/CallComposerActivity.java
+++ b/java/com/android/dialer/callcomposer/CallComposerActivity.java
@@ -21,9 +21,9 @@
 import android.animation.AnimatorSet;
 import android.animation.ArgbEvaluator;
 import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Configuration;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
@@ -35,6 +35,7 @@
 import android.support.v4.view.animation.FastOutSlowInInterpolator;
 import android.support.v7.app.AppCompatActivity;
 import android.text.TextUtils;
+import android.util.Base64;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.View.OnLayoutChangeListener;
@@ -60,6 +61,7 @@
 import com.android.dialer.common.UiUtil;
 import com.android.dialer.compat.CompatUtils;
 import com.android.dialer.constants.Constants;
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
 import com.android.dialer.enrichedcall.EnrichedCallManager;
 import com.android.dialer.enrichedcall.EnrichedCallManager.State;
 import com.android.dialer.enrichedcall.Session;
@@ -70,6 +72,7 @@
 import com.android.dialer.protos.ProtoParsers;
 import com.android.dialer.telecom.TelecomUtil;
 import com.android.dialer.util.ViewUtil;
+import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
 import java.io.File;
 
 /**
@@ -88,18 +91,18 @@
         OnPageChangeListener,
         CallComposerListener,
         OnLayoutChangeListener,
-        AnimatorListener,
         EnrichedCallManager.StateChangedListener {
 
-  private static final int VIEW_PAGER_ANIMATION_DURATION_MILLIS = 300;
+  public static final String KEY_CONTACT_NAME = "contact_name";
+
   private static final int ENTRANCE_ANIMATION_DURATION_MILLIS = 500;
+  private static final int EXIT_ANIMATION_DURATION_MILLIS = 500;
 
   private static final String ARG_CALL_COMPOSER_CONTACT = "CALL_COMPOSER_CONTACT";
 
   private static final String ENTRANCE_ANIMATION_KEY = "entrance_animation_key";
   private static final String CURRENT_INDEX_KEY = "current_index_key";
   private static final String VIEW_PAGER_STATE_KEY = "view_pager_state_key";
-  private static final String LOCATIONS_KEY = "locations_key";
   private static final String SESSION_ID_KEY = "session_id_key";
 
   private CallComposerContact contact;
@@ -111,6 +114,7 @@
   private RelativeLayout contactContainer;
   private Toolbar toolbar;
   private View sendAndCall;
+  private TextView sendAndCallText;
 
   private ImageView cameraIcon;
   private ImageView galleryIcon;
@@ -125,13 +129,8 @@
   private boolean shouldAnimateEntrance = true;
   private boolean inFullscreenMode;
   private boolean isSendAndCallHidingOrHidden = true;
-  private boolean isAnimatingContactBar;
   private boolean layoutChanged;
   private int currentIndex;
-  private int[] locations;
-  private int currentLocation;
-
-  @NonNull private EnrichedCallManager enrichedCallManager;
 
   public static Intent newIntent(Context context, CallComposerContact contact) {
     Intent intent = new Intent(context, CallComposerActivity.class);
@@ -156,6 +155,7 @@
     windowContainer = (LinearLayout) findViewById(R.id.call_composer_container);
     toolbar = (Toolbar) findViewById(R.id.toolbar);
     sendAndCall = findViewById(R.id.send_and_call_button);
+    sendAndCallText = (TextView) findViewById(R.id.send_and_call_text);
 
     interpolator = new FastOutSlowInInterpolator();
     adapter =
@@ -166,14 +166,8 @@
     pager.addOnPageChangeListener(this);
 
     setActionBar(toolbar);
-    toolbar.setNavigationIcon(R.drawable.quantum_ic_arrow_back_white_24);
-    toolbar.setNavigationOnClickListener(
-        new OnClickListener() {
-          @Override
-          public void onClick(View v) {
-            finish();
-          }
-        });
+    toolbar.setNavigationIcon(R.drawable.quantum_ic_close_white_24);
+    toolbar.setNavigationOnClickListener(v -> finish());
 
     background.addOnLayoutChangeListener(this);
     cameraIcon.setOnClickListener(this);
@@ -183,20 +177,12 @@
 
     onHandleIntent(getIntent());
 
-    enrichedCallManager = EnrichedCallManager.Accessor.getInstance(getApplication());
     if (savedInstanceState != null) {
       shouldAnimateEntrance = savedInstanceState.getBoolean(ENTRANCE_ANIMATION_KEY);
-      locations = savedInstanceState.getIntArray(LOCATIONS_KEY);
       pager.onRestoreInstanceState(savedInstanceState.getParcelable(VIEW_PAGER_STATE_KEY));
       currentIndex = savedInstanceState.getInt(CURRENT_INDEX_KEY);
-      sessionId = savedInstanceState.getLong(SESSION_ID_KEY);
+      sessionId = savedInstanceState.getLong(SESSION_ID_KEY, Session.NO_SESSION_ID);
       onPageSelected(currentIndex);
-    } else {
-      locations = new int[adapter.getCount()];
-      for (int i = 0; i < locations.length; i++) {
-        locations[i] = CallComposerFragment.CONTENT_TOP_UNSET;
-      }
-      sessionId = enrichedCallManager.startCallComposerSession(contact.number);
     }
 
     // Since we can't animate the views until they are ready to be drawn, we use this listener to
@@ -204,37 +190,39 @@
     ViewUtil.doOnPreDraw(
         windowContainer,
         false,
-        new Runnable() {
-          @Override
-          public void run() {
-            runEntranceAnimation();
-          }
+        () -> {
+          showFullscreen(inFullscreenMode);
+          runEntranceAnimation();
         });
 
     setMediaIconSelected(0);
-
-    // This activity is started using startActivityForResult. By default, mark this as succeeded
-    // and flip this to RESULT_CANCELED if something goes wrong.
-    setResult(RESULT_OK);
-
-    if (sessionId == Session.NO_SESSION_ID) {
-      LogUtil.w("CallComposerActivity.onCreate", "failed to create call composer session");
-      setResult(RESULT_CANCELED);
-      finish();
-    }
   }
 
   @Override
   protected void onResume() {
     super.onResume();
-    enrichedCallManager.registerStateChangedListener(this);
+    getEnrichedCallManager().registerStateChangedListener(this);
+    if (sessionId == Session.NO_SESSION_ID) {
+      LogUtil.i("CallComposerActivity.onResume", "creating new session");
+      sessionId = getEnrichedCallManager().startCallComposerSession(contact.number);
+    } else if (getEnrichedCallManager().getSession(sessionId) == null) {
+      LogUtil.i(
+          "CallComposerActivity.onResume", "session closed while activity paused, creating new");
+      sessionId = getEnrichedCallManager().startCallComposerSession(contact.number);
+    } else {
+      LogUtil.i("CallComposerActivity.onResume", "session still open, using old");
+    }
+    if (sessionId == Session.NO_SESSION_ID) {
+      LogUtil.w("CallComposerActivity.onResume", "failed to create call composer session");
+      setFailedResultAndFinish();
+    }
     refreshUiForCallComposerState();
   }
 
   @Override
   protected void onPause() {
     super.onPause();
-    enrichedCallManager.unregisterStateChangedListener(this);
+    getEnrichedCallManager().unregisterStateChangedListener(this);
   }
 
   @Override
@@ -243,7 +231,7 @@
   }
 
   private void refreshUiForCallComposerState() {
-    Session session = enrichedCallManager.getSession(sessionId);
+    Session session = getEnrichedCallManager().getSession(sessionId);
     if (session == null) {
       return;
     }
@@ -256,8 +244,7 @@
 
     if (state == EnrichedCallManager.STATE_START_FAILED
         || state == EnrichedCallManager.STATE_CLOSED) {
-      setResult(RESULT_CANCELED);
-      finish();
+      setFailedResultAndFinish();
     }
   }
 
@@ -293,7 +280,7 @@
 
       if (fragment instanceof MessageComposerFragment) {
         MessageComposerFragment messageComposerFragment = (MessageComposerFragment) fragment;
-        builder.setSubject(messageComposerFragment.getMessage());
+        builder.setText(messageComposerFragment.getMessage());
         placeRCSCall(builder);
       }
       if (fragment instanceof GalleryComposerFragment) {
@@ -351,7 +338,7 @@
   }
 
   private boolean sessionReady() {
-    Session session = enrichedCallManager.getSession(sessionId);
+    Session session = getEnrichedCallManager().getSession(sessionId);
     if (session == null) {
       return false;
     }
@@ -362,9 +349,10 @@
   private void placeRCSCall(MultimediaData.Builder builder) {
     LogUtil.i("CallComposerActivity.placeRCSCall", "placing enriched call");
     Logger.get(this).logImpression(DialerImpression.Type.CALL_COMPOSER_ACTIVITY_PLACE_RCS_CALL);
-    enrichedCallManager.sendCallComposerData(sessionId, builder.build());
+    getEnrichedCallManager().sendCallComposerData(sessionId, builder.build());
     TelecomUtil.placeCall(
         this, new CallIntentBuilder(contact.number, CallInitiationType.Type.CALL_COMPOSER).build());
+    setResult(RESULT_OK);
     finish();
   }
 
@@ -379,24 +367,26 @@
   /** Animates {@code contactContainer} to align with content inside viewpager. */
   @Override
   public void onPageSelected(int position) {
+    if (position == CallComposerPagerAdapter.INDEX_MESSAGE) {
+      sendAndCallText.setText(R.string.send_and_call);
+    } else {
+      sendAndCallText.setText(R.string.share_and_call);
+    }
     if (currentIndex == CallComposerPagerAdapter.INDEX_MESSAGE) {
       UiUtil.hideKeyboardFrom(this, windowContainer);
-    } else if (position == CallComposerPagerAdapter.INDEX_MESSAGE && inFullscreenMode) {
+    } else if (position == CallComposerPagerAdapter.INDEX_MESSAGE
+        && inFullscreenMode
+        && !isLandscapeLayout()) {
       UiUtil.openKeyboardFrom(this, windowContainer);
     }
     currentIndex = position;
     CallComposerFragment fragment = (CallComposerFragment) adapter.instantiateItem(pager, position);
-    locations[currentIndex] = fragment.getContentTopPx();
-    animateContactContainer(locations[currentIndex]);
     animateSendAndCall(fragment.shouldHide());
     setMediaIconSelected(position);
   }
 
   @Override
-  public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
-    CallComposerFragment fragment = (CallComposerFragment) adapter.instantiateItem(pager, position);
-    animateContactContainer(fragment.getContentTopPx());
-  }
+  public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
 
   @Override
   public void onPageScrollStateChanged(int state) {}
@@ -407,13 +397,19 @@
     outState.putParcelable(VIEW_PAGER_STATE_KEY, pager.onSaveInstanceState());
     outState.putBoolean(ENTRANCE_ANIMATION_KEY, shouldAnimateEntrance);
     outState.putInt(CURRENT_INDEX_KEY, currentIndex);
-    outState.putIntArray(LOCATIONS_KEY, locations);
     outState.putLong(SESSION_ID_KEY, sessionId);
   }
 
   @Override
   public void onBackPressed() {
-    runExitAnimation();
+    if (!isSendAndCallHidingOrHidden) {
+      ((CallComposerFragment) adapter.instantiateItem(pager, currentIndex)).clearComposer();
+    } else {
+      // Unregister first to avoid receiving a callback when the session closes
+      getEnrichedCallManager().unregisterStateChangedListener(this);
+      getEnrichedCallManager().endCallComposerSession(sessionId);
+      runExitAnimation();
+    }
   }
 
   @Override
@@ -445,29 +441,9 @@
     }
 
     layoutChanged = true;
-    if (pager.getTop() < 0 || inFullscreenMode) {
-      ViewGroup.LayoutParams layoutParams = pager.getLayoutParams();
-      layoutParams.height = background.getHeight() - toolbar.getHeight() - messageIcon.getHeight();
-      pager.setLayoutParams(layoutParams);
-    }
+    showFullscreen(contactContainer.getTop() < 0 || inFullscreenMode);
   }
 
-  @Override
-  public void onAnimationStart(Animator animation) {
-    isAnimatingContactBar = true;
-  }
-
-  @Override
-  public void onAnimationEnd(Animator animation) {
-    isAnimatingContactBar = false;
-  }
-
-  @Override
-  public void onAnimationCancel(Animator animation) {}
-
-  @Override
-  public void onAnimationRepeat(Animator animation) {}
-
   /**
    * Reads arguments from the fragment arguments and populates the necessary instance variables.
    * Copied from {@link com.android.contacts.common.dialog.CallSubjectDialog}.
@@ -477,12 +453,26 @@
     if (arguments == null) {
       throw new RuntimeException("CallComposerActivity.onHandleIntent, Arguments cannot be null.");
     }
-    contact =
-        ProtoParsers.getFromInstanceState(
-            arguments, ARG_CALL_COMPOSER_CONTACT, new CallComposerContact());
+    if (arguments.get(ARG_CALL_COMPOSER_CONTACT) instanceof String) {
+      byte[] bytes = Base64.decode(arguments.getString(ARG_CALL_COMPOSER_CONTACT), Base64.DEFAULT);
+      try {
+        contact = CallComposerContact.parseFrom(bytes);
+      } catch (InvalidProtocolBufferNanoException e) {
+        Assert.fail(e.toString());
+      }
+    } else {
+      contact =
+          ProtoParsers.getFromInstanceState(
+              arguments, ARG_CALL_COMPOSER_CONTACT, new CallComposerContact());
+    }
     updateContactInfo();
   }
 
+  @Override
+  public boolean isLandscapeLayout() {
+    return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
+  }
+
   /**
    * Populates the contact info fields based on the current contact information. Copied from {@link
    * com.android.contacts.common.dialog.CallSubjectDialog}.
@@ -552,25 +542,6 @@
     }
   }
 
-  private void animateContactContainer(int toY) {
-    if (toY == CallComposerFragment.CONTENT_TOP_UNSET
-        || toY == currentLocation
-        || (toY != locations[currentIndex]
-            && locations[currentIndex] != CallComposerFragment.CONTENT_TOP_UNSET)
-        || isAnimatingContactBar
-        || inFullscreenMode) {
-      return;
-    }
-    currentLocation = toY;
-    contactContainer
-        .animate()
-        .translationY(toY)
-        .setInterpolator(interpolator)
-        .setDuration(VIEW_PAGER_ANIMATION_DURATION_MILLIS)
-        .setListener(this)
-        .start();
-  }
-
   /** Animates compose UI into view */
   private void runEntranceAnimation() {
     if (!shouldAnimateEntrance) {
@@ -578,84 +549,90 @@
     }
     shouldAnimateEntrance = false;
 
-    int colorFrom = ContextCompat.getColor(this, android.R.color.transparent);
-    int colorTo = ContextCompat.getColor(this, R.color.call_composer_background_color);
-    ValueAnimator backgroundAnimation =
-        ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
-    backgroundAnimation.setInterpolator(interpolator);
-    backgroundAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); // milliseconds
-    backgroundAnimation.addUpdateListener(
-        new AnimatorUpdateListener() {
-          @Override
-          public void onAnimationUpdate(ValueAnimator animator) {
-            background.setBackgroundColor((int) animator.getAnimatedValue());
-          }
-        });
-
-    ValueAnimator contentAnimation = ValueAnimator.ofFloat(windowContainer.getHeight(), 0);
+    int value = isLandscapeLayout() ? windowContainer.getWidth() : windowContainer.getHeight();
+    ValueAnimator contentAnimation = ValueAnimator.ofFloat(value, 0);
     contentAnimation.setInterpolator(interpolator);
     contentAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS);
     contentAnimation.addUpdateListener(
-        new AnimatorUpdateListener() {
-          @Override
-          public void onAnimationUpdate(ValueAnimator animation) {
+        animation -> {
+          if (isLandscapeLayout()) {
+            windowContainer.setX((Float) animation.getAnimatedValue());
+          } else {
             windowContainer.setY((Float) animation.getAnimatedValue());
           }
         });
 
-    AnimatorSet set = new AnimatorSet();
-    set.play(contentAnimation).with(backgroundAnimation);
-    set.start();
+    if (!isLandscapeLayout()) {
+      int colorFrom = ContextCompat.getColor(this, android.R.color.transparent);
+      int colorTo = ContextCompat.getColor(this, R.color.call_composer_background_color);
+      ValueAnimator backgroundAnimation =
+          ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
+      backgroundAnimation.setInterpolator(interpolator);
+      backgroundAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); // milliseconds
+      backgroundAnimation.addUpdateListener(
+          animator -> background.setBackgroundColor((int) animator.getAnimatedValue()));
+
+      AnimatorSet set = new AnimatorSet();
+      set.play(contentAnimation).with(backgroundAnimation);
+      set.start();
+    } else {
+      contentAnimation.start();
+    }
   }
 
   /** Animates compose UI out of view and ends the activity. */
   private void runExitAnimation() {
-    int colorTo = ContextCompat.getColor(this, android.R.color.transparent);
-    int colorFrom = ContextCompat.getColor(this, R.color.call_composer_background_color);
-    ValueAnimator backgroundAnimation =
-        ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
-    backgroundAnimation.setInterpolator(interpolator);
-    backgroundAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); // milliseconds
-    backgroundAnimation.addUpdateListener(
-        new AnimatorUpdateListener() {
-          @Override
-          public void onAnimationUpdate(ValueAnimator animator) {
-            background.setBackgroundColor((int) animator.getAnimatedValue());
+    int value = isLandscapeLayout() ? windowContainer.getWidth() : windowContainer.getHeight();
+    ValueAnimator contentAnimation = ValueAnimator.ofFloat(0, value);
+    contentAnimation.setInterpolator(interpolator);
+    contentAnimation.setDuration(EXIT_ANIMATION_DURATION_MILLIS);
+    contentAnimation.addUpdateListener(
+        animation -> {
+          if (isLandscapeLayout()) {
+            windowContainer.setX((Float) animation.getAnimatedValue());
+          } else {
+            windowContainer.setY((Float) animation.getAnimatedValue());
+          }
+          if (animation.getAnimatedFraction() > .95) {
+            finish();
           }
         });
 
-    ValueAnimator contentAnimation = ValueAnimator.ofFloat(0, windowContainer.getHeight());
-    contentAnimation.setInterpolator(interpolator);
-    contentAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS);
-    contentAnimation.addUpdateListener(
-        new AnimatorUpdateListener() {
-          @Override
-          public void onAnimationUpdate(ValueAnimator animation) {
-            windowContainer.setY((Float) animation.getAnimatedValue());
-            if (animation.getAnimatedFraction() > .75) {
-              finish();
-            }
-          }
-        });
-    AnimatorSet set = new AnimatorSet();
-    set.play(contentAnimation).with(backgroundAnimation);
-    set.start();
+    if (!isLandscapeLayout()) {
+      int colorTo = ContextCompat.getColor(this, android.R.color.transparent);
+      int colorFrom = ContextCompat.getColor(this, R.color.call_composer_background_color);
+      ValueAnimator backgroundAnimation =
+          ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
+      backgroundAnimation.setInterpolator(interpolator);
+      backgroundAnimation.setDuration(EXIT_ANIMATION_DURATION_MILLIS);
+      backgroundAnimation.addUpdateListener(
+          animator -> background.setBackgroundColor((int) animator.getAnimatedValue()));
+
+      AnimatorSet set = new AnimatorSet();
+      set.play(contentAnimation).with(backgroundAnimation);
+      set.start();
+    } else {
+      contentAnimation.start();
+    }
   }
 
   @Override
-  public void showFullscreen(boolean show) {
-    if (inFullscreenMode == show) {
-      return;
-    }
-    inFullscreenMode = show;
-    toolbar.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
-    contactContainer.setVisibility(show ? View.GONE : View.VISIBLE);
+  public void showFullscreen(boolean fullscreen) {
+    inFullscreenMode = fullscreen;
     ViewGroup.LayoutParams layoutParams = pager.getLayoutParams();
-    if (show) {
+    if (isLandscapeLayout()) {
+      layoutParams.height = background.getHeight() - messageIcon.getHeight();
+      toolbar.setVisibility(View.INVISIBLE);
+      contactContainer.setVisibility(View.GONE);
+    } else if (fullscreen || getResources().getBoolean(R.bool.show_toolbar)) {
       layoutParams.height = background.getHeight() - toolbar.getHeight() - messageIcon.getHeight();
+      toolbar.setVisibility(View.VISIBLE);
+      contactContainer.setVisibility(View.GONE);
     } else {
       layoutParams.height =
           getResources().getDimensionPixelSize(R.dimen.call_composer_view_pager_height);
+      toolbar.setVisibility(View.INVISIBLE);
+      contactContainer.setVisibility(View.VISIBLE);
     }
     pager.setLayoutParams(layoutParams);
   }
@@ -686,35 +663,32 @@
       ViewUtil.doOnPreDraw(
           sendAndCall,
           true,
-          new Runnable() {
-            @Override
-            public void run() {
-              Animator animator =
-                  ViewAnimationUtils.createCircularReveal(
-                      sendAndCall, centerX, centerY, startRadius, endRadius);
-              animator.addListener(
-                  new AnimatorListener() {
-                    @Override
-                    public void onAnimationStart(Animator animation) {
-                      isSendAndCallHidingOrHidden = shouldHide;
-                      sendAndCall.setVisibility(View.VISIBLE);
+          () -> {
+            Animator animator =
+                ViewAnimationUtils.createCircularReveal(
+                    sendAndCall, centerX, centerY, startRadius, endRadius);
+            animator.addListener(
+                new AnimatorListener() {
+                  @Override
+                  public void onAnimationStart(Animator animation) {
+                    isSendAndCallHidingOrHidden = shouldHide;
+                    sendAndCall.setVisibility(View.VISIBLE);
+                  }
+
+                  @Override
+                  public void onAnimationEnd(Animator animation) {
+                    if (isSendAndCallHidingOrHidden) {
+                      sendAndCall.setVisibility(View.INVISIBLE);
                     }
+                  }
 
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                      if (isSendAndCallHidingOrHidden) {
-                        sendAndCall.setVisibility(View.INVISIBLE);
-                      }
-                    }
+                  @Override
+                  public void onAnimationCancel(Animator animation) {}
 
-                    @Override
-                    public void onAnimationCancel(Animator animation) {}
-
-                    @Override
-                    public void onAnimationRepeat(Animator animation) {}
-                  });
-              animator.start();
-            }
+                  @Override
+                  public void onAnimationRepeat(Animator animation) {}
+                });
+            animator.start();
           });
     }
   }
@@ -725,4 +699,14 @@
     galleryIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_GALLERY ? 1 : alpha);
     messageIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_MESSAGE ? 1 : alpha);
   }
+
+  private void setFailedResultAndFinish() {
+    setResult(RESULT_FIRST_USER, new Intent().putExtra(KEY_CONTACT_NAME, contact.nameOrNumber));
+    finish();
+  }
+
+  @NonNull
+  private EnrichedCallManager getEnrichedCallManager() {
+    return EnrichedCallComponent.get(this).getEnrichedCallManager();
+  }
 }
diff --git a/java/com/android/dialer/callcomposer/CallComposerFragment.java b/java/com/android/dialer/callcomposer/CallComposerFragment.java
index d6f9449..ee1eb46 100644
--- a/java/com/android/dialer/callcomposer/CallComposerFragment.java
+++ b/java/com/android/dialer/callcomposer/CallComposerFragment.java
@@ -17,13 +17,8 @@
 package com.android.dialer.callcomposer;
 
 import android.content.Context;
-import android.os.Bundle;
 import android.support.annotation.Nullable;
 import android.support.v4.app.Fragment;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.FragmentUtils;
 import com.android.dialer.common.LogUtil;
@@ -34,26 +29,10 @@
   protected static final int CAMERA_PERMISSION = 1;
   protected static final int STORAGE_PERMISSION = 2;
 
-  private static final String LOCATION_KEY = "location_key";
-  public static final int CONTENT_TOP_UNSET = Integer.MAX_VALUE;
-
-  private View topView;
-  private int contentTopPx = CONTENT_TOP_UNSET;
-  private CallComposerListener testListener;
-
-  @Nullable
-  @Override
-  public View onCreateView(
-      LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
-    View view = super.onCreateView(layoutInflater, viewGroup, bundle);
-    Assert.isNotNull(topView);
-    return view;
-  }
-
   @Override
   public void onAttach(Context context) {
     super.onAttach(context);
-    if (!(context instanceof CallComposerListener) && testListener == null) {
+    if (FragmentUtils.getParent(this, CallComposerListener.class) == null) {
       LogUtil.e(
           "CallComposerFragment.onAttach",
           "Container activity must implement CallComposerListener.");
@@ -61,56 +40,15 @@
     }
   }
 
-  /** Call this method to declare which view is located at the top of the fragment's layout. */
-  public void setTopView(View view) {
-    topView = view;
-    // For each fragment that extends CallComposerFragment, the heights may vary and since
-    // ViewPagers cannot have their height set to wrap_content, we have to adjust the top of our
-    // container to match the top of the fragment. This listener populates {@code contentTopPx} as
-    // it's available.
-    topView
-        .getViewTreeObserver()
-        .addOnGlobalLayoutListener(
-            new OnGlobalLayoutListener() {
-              @Override
-              public void onGlobalLayout() {
-                topView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
-                contentTopPx = topView.getTop();
-              }
-            });
-  }
-
-  public int getContentTopPx() {
-    return contentTopPx;
-  }
-
-  public void setParentForTesting(CallComposerListener listener) {
-    testListener = listener;
-  }
-
+  @Nullable
   public CallComposerListener getListener() {
-    if (testListener != null) {
-      return testListener;
-    }
-    return FragmentUtils.getParentUnsafe(this, CallComposerListener.class);
-  }
-
-  @Override
-  public void onSaveInstanceState(Bundle outState) {
-    super.onSaveInstanceState(outState);
-    outState.putInt(LOCATION_KEY, contentTopPx);
-  }
-
-  @Override
-  public void onViewStateRestored(Bundle savedInstanceState) {
-    super.onViewStateRestored(savedInstanceState);
-    if (savedInstanceState != null) {
-      contentTopPx = savedInstanceState.getInt(LOCATION_KEY);
-    }
+    return FragmentUtils.getParent(this, CallComposerListener.class);
   }
 
   public abstract boolean shouldHide();
 
+  public abstract void clearComposer();
+
   /** Interface used to listen to CallComposeFragments */
   public interface CallComposerListener {
     /** Let the listener know when a call is ready to be composed. */
@@ -121,5 +59,8 @@
 
     /** True is the listener is in fullscreen. */
     boolean isFullscreen();
+
+    /** True if the layout is in landscape mode. */
+    boolean isLandscapeLayout();
   }
 }
diff --git a/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java b/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java
index 4d4058a..edf980e 100644
--- a/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java
+++ b/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java
@@ -18,11 +18,11 @@
 
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentStatePagerAdapter;
+import android.support.v4.app.FragmentPagerAdapter;
 import com.android.dialer.common.Assert;
 
 /** ViewPager adapter for call compose UI. */
-public class CallComposerPagerAdapter extends FragmentStatePagerAdapter {
+public class CallComposerPagerAdapter extends FragmentPagerAdapter {
 
   public static final int INDEX_CAMERA = 0;
   public static final int INDEX_GALLERY = 1;
diff --git a/java/com/android/dialer/callcomposer/CameraComposerFragment.java b/java/com/android/dialer/callcomposer/CameraComposerFragment.java
index f2d0a94..583fb54 100644
--- a/java/com/android/dialer/callcomposer/CameraComposerFragment.java
+++ b/java/com/android/dialer/callcomposer/CameraComposerFragment.java
@@ -56,6 +56,9 @@
 public class CameraComposerFragment extends CallComposerFragment
     implements CameraManagerListener, OnClickListener, CameraManager.MediaCallback {
 
+  private static final String CAMERA_DIRECTION_KEY = "camera_direction";
+  private static final String CAMERA_URI_KEY = "camera_key";
+
   private View permissionView;
   private ImageButton exitFullscreen;
   private ImageButton fullscreen;
@@ -68,11 +71,13 @@
   private View allowPermission;
   private CameraPreviewHost preview;
   private ProgressBar loading;
+  private ImageView previewImageView;
 
   private Uri cameraUri;
   private boolean processingUri;
   private String[] permissions = new String[] {Manifest.permission.CAMERA};
   private CameraUriCallback uriCallback;
+  private int cameraDirection = CameraInfo.CAMERA_FACING_BACK;
 
   public static CameraComposerFragment newInstance() {
     return new CameraComposerFragment();
@@ -94,6 +99,7 @@
     cancel = (ImageButton) cameraView.findViewById(R.id.camera_cancel_button);
     focus = (RenderOverlay) cameraView.findViewById(R.id.focus_visual);
     preview = (CameraPreviewHost) cameraView.findViewById(R.id.camera_preview);
+    previewImageView = (ImageView) root.findViewById(R.id.preview_image_view);
 
     exitFullscreen.setOnClickListener(this);
     fullscreen.setOnClickListener(this);
@@ -115,10 +121,12 @@
           ContextCompat.getColor(getContext(), R.color.dialer_theme_color));
       permissionView.setVisibility(View.VISIBLE);
     } else {
+      if (bundle != null) {
+        cameraDirection = bundle.getInt(CAMERA_DIRECTION_KEY);
+        cameraUri = bundle.getParcelable(CAMERA_URI_KEY);
+      }
       setupCamera();
     }
-
-    setTopView(cameraView);
     return root;
   }
 
@@ -126,8 +134,8 @@
     CameraManager.get().setListener(this);
     preview.setShown();
     CameraManager.get().setRenderOverlay(focus);
-    CameraManager.get().selectCamera(CameraInfo.CAMERA_FACING_BACK);
-    setCameraUri(null);
+    CameraManager.get().selectCamera(cameraDirection);
+    setCameraUri(cameraUri);
   }
 
   @Override
@@ -146,10 +154,16 @@
   }
 
   @Override
+  public void clearComposer() {
+    processingUri = false;
+    setCameraUri(null);
+  }
+
+  @Override
   public void onClick(View view) {
     if (view == capture) {
       float heightPercent = 1;
-      if (!getListener().isFullscreen()) {
+      if (!getListener().isFullscreen() && !getListener().isLandscapeLayout()) {
         heightPercent = Math.min((float) cameraView.getHeight() / preview.getView().getHeight(), 1);
       }
 
@@ -162,8 +176,7 @@
       ((Animatable) swapCamera.getDrawable()).start();
       CameraManager.get().swapCamera();
     } else if (view == cancel) {
-      processingUri = false;
-      setCameraUri(null);
+      clearComposer();
     } else if (view == exitFullscreen) {
       getListener().showFullscreen(false);
       fullscreen.setVisibility(View.VISIBLE);
@@ -314,6 +327,13 @@
     boolean isCameraAvailable = CameraManager.get().isCameraAvailable();
     boolean uriReadyOrProcessing = cameraUri != null || processingUri;
 
+    if (cameraUri != null) {
+      previewImageView.setImageURI(cameraUri);
+      previewImageView.setVisibility(View.VISIBLE);
+    } else {
+      previewImageView.setVisibility(View.GONE);
+    }
+
     if (cameraUri == null && isCameraAvailable) {
       CameraManager.get().resetPreview();
       cancel.setVisibility(View.GONE);
@@ -328,7 +348,7 @@
     capture.setVisibility(uriReadyOrProcessing ? View.GONE : View.VISIBLE);
     cancel.setVisibility(uriReadyOrProcessing ? View.VISIBLE : View.GONE);
 
-    if (uriReadyOrProcessing) {
+    if (uriReadyOrProcessing || getListener().isLandscapeLayout()) {
       fullscreen.setVisibility(View.GONE);
       exitFullscreen.setVisibility(View.GONE);
     } else if (getListener().isFullscreen()) {
@@ -344,6 +364,13 @@
   }
 
   @Override
+  public void onSaveInstanceState(Bundle outState) {
+    super.onSaveInstanceState(outState);
+    outState.putInt(CAMERA_DIRECTION_KEY, CameraManager.get().getCameraInfo().facing);
+    outState.putParcelable(CAMERA_URI_KEY, cameraUri);
+  }
+
+  @Override
   public void onRequestPermissionsResult(
       int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
     if (permissions.length > 0 && permissions[0].equals(this.permissions[0])) {
diff --git a/java/com/android/dialer/callcomposer/GalleryComposerFragment.java b/java/com/android/dialer/callcomposer/GalleryComposerFragment.java
index 6231279..b53d6a9 100644
--- a/java/com/android/dialer/callcomposer/GalleryComposerFragment.java
+++ b/java/com/android/dialer/callcomposer/GalleryComposerFragment.java
@@ -24,6 +24,7 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Parcelable;
 import android.provider.Settings;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
@@ -45,11 +46,17 @@
 import com.android.dialer.logging.nano.DialerImpression;
 import com.android.dialer.util.PermissionsUtil;
 import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
 
 /** Fragment used to compose call with image from the user's gallery. */
 public class GalleryComposerFragment extends CallComposerFragment
     implements LoaderCallbacks<Cursor>, OnClickListener {
 
+  private static final String SELECTED_DATA_KEY = "selected_data";
+  private static final String IS_COPY_KEY = "is_copy";
+  private static final String INSERTED_IMAGES_KEY = "inserted_images";
+
   private static final int RESULT_LOAD_IMAGE = 1;
   private static final int RESULT_OPEN_SETTINGS = 2;
 
@@ -62,6 +69,7 @@
   private CursorLoader cursorLoader;
   private GalleryGridItemData selectedData = null;
   private boolean selectedDataIsCopy;
+  private List<GalleryGridItemData> insertedImages = new ArrayList<>();
 
   public static GalleryComposerFragment newInstance() {
     return new GalleryComposerFragment();
@@ -89,10 +97,13 @@
           ContextCompat.getColor(getContext(), R.color.dialer_theme_color));
       permissionView.setVisibility(View.VISIBLE);
     } else {
+      if (bundle != null) {
+        selectedData = bundle.getParcelable(SELECTED_DATA_KEY);
+        selectedDataIsCopy = bundle.getBoolean(IS_COPY_KEY);
+        insertedImages = bundle.getParcelableArrayList(INSERTED_IMAGES_KEY);
+      }
       setupGallery();
     }
-
-    setTopView(galleryGridView);
     return view;
   }
 
@@ -110,6 +121,10 @@
   @Override
   public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
     adapter.swapCursor(cursor);
+    if (insertedImages != null && !insertedImages.isEmpty()) {
+      adapter.insertEntries(insertedImages);
+    }
+    setSelected(selectedData, selectedDataIsCopy);
   }
 
   @Override
@@ -147,7 +162,7 @@
         intent.addCategory(Intent.CATEGORY_OPENABLE);
         startActivityForResult(intent, RESULT_LOAD_IMAGE);
       } else if (itemView.getData().equals(selectedData)) {
-        setSelected(null, false);
+        clearComposer();
       } else {
         setSelected(new GalleryGridItemData(itemView.getData()), false);
       }
@@ -179,7 +194,10 @@
     selectedData = data;
     selectedDataIsCopy = isCopy;
     adapter.setSelected(selectedData);
-    getListener().composeCall(this);
+    CallComposerListener listener = getListener();
+    if (listener != null) {
+      getListener().composeCall(this);
+    }
   }
 
   @Override
@@ -190,6 +208,20 @@
   }
 
   @Override
+  public void clearComposer() {
+    setSelected(null, false);
+  }
+
+  @Override
+  public void onSaveInstanceState(Bundle outState) {
+    super.onSaveInstanceState(outState);
+    outState.putParcelable(SELECTED_DATA_KEY, selectedData);
+    outState.putBoolean(IS_COPY_KEY, selectedDataIsCopy);
+    outState.putParcelableArrayList(
+        INSERTED_IMAGES_KEY, (ArrayList<? extends Parcelable>) insertedImages);
+  }
+
+  @Override
   public void onRequestPermissionsResult(
       int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
     if (permissions.length > 0 && permissions[0].equals(this.permissions[0])) {
@@ -238,7 +270,9 @@
               new Callback() {
                 @Override
                 public void onCopySuccessful(File file, String mimeType) {
-                  setSelected(adapter.insertEntry(file.getAbsolutePath(), mimeType), true);
+                  GalleryGridItemData data = adapter.insertEntry(file.getAbsolutePath(), mimeType);
+                  insertedImages.add(0, data);
+                  setSelected(data, true);
                 }
 
                 @Override
diff --git a/java/com/android/dialer/callcomposer/GalleryGridAdapter.java b/java/com/android/dialer/callcomposer/GalleryGridAdapter.java
index 0a7fd76..84257b2 100644
--- a/java/com/android/dialer/callcomposer/GalleryGridAdapter.java
+++ b/java/com/android/dialer/callcomposer/GalleryGridAdapter.java
@@ -104,6 +104,18 @@
     }
   }
 
+  public void insertEntries(@NonNull List<GalleryGridItemData> entries) {
+    Assert.checkArgument(entries.size() != 0);
+    LogUtil.i("GalleryGridAdapter.insertRows", "inserting %d rows", entries.size());
+    MatrixCursor extraRow = new MatrixCursor(GalleryGridItemData.IMAGE_PROJECTION);
+    for (GalleryGridItemData entry : entries) {
+      extraRow.addRow(new Object[] {0L, entry.getFilePath(), entry.getMimeType(), ""});
+    }
+    extraRow.moveToFirst();
+    Cursor extendedCursor = new MergeCursor(new Cursor[] {extraRow, getCursor()});
+    swapCursor(extendedCursor);
+  }
+
   public GalleryGridItemData insertEntry(String filePath, String mimeType) {
     LogUtil.i("GalleryGridAdapter.insertRow", mimeType + " " + filePath);
 
diff --git a/java/com/android/dialer/callcomposer/GalleryGridItemData.java b/java/com/android/dialer/callcomposer/GalleryGridItemData.java
index 402c6ce..43db96d 100644
--- a/java/com/android/dialer/callcomposer/GalleryGridItemData.java
+++ b/java/com/android/dialer/callcomposer/GalleryGridItemData.java
@@ -18,6 +18,8 @@
 
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.provider.MediaStore.Images.Media;
 import android.support.annotation.Nullable;
 import android.text.TextUtils;
@@ -26,7 +28,7 @@
 import java.util.Objects;
 
 /** Provides data for GalleryGridItemView */
-public final class GalleryGridItemData {
+public final class GalleryGridItemData implements Parcelable {
   public static final String[] IMAGE_PROJECTION =
       new String[] {Media._ID, Media.DATA, Media.MIME_TYPE, Media.DATE_MODIFIED};
 
@@ -88,4 +90,35 @@
   public int hashCode() {
     return Objects.hash(filePath, mimeType, dateModifiedSeconds);
   }
+
+  @Override
+  public int describeContents() {
+    return 0;
+  }
+
+  @Override
+  public void writeToParcel(Parcel dest, int flags) {
+    dest.writeString(filePath);
+    dest.writeString(mimeType);
+    dest.writeLong(dateModifiedSeconds);
+  }
+
+  public static final Creator<GalleryGridItemData> CREATOR =
+      new Creator<GalleryGridItemData>() {
+        @Override
+        public GalleryGridItemData createFromParcel(Parcel in) {
+          return new GalleryGridItemData(in);
+        }
+
+        @Override
+        public GalleryGridItemData[] newArray(int size) {
+          return new GalleryGridItemData[size];
+        }
+      };
+
+  private GalleryGridItemData(Parcel in) {
+    filePath = in.readString();
+    mimeType = in.readString();
+    dateModifiedSeconds = in.readLong();
+  }
 }
diff --git a/java/com/android/dialer/callcomposer/MessageComposerFragment.java b/java/com/android/dialer/callcomposer/MessageComposerFragment.java
index 521b714..d810003 100644
--- a/java/com/android/dialer/callcomposer/MessageComposerFragment.java
+++ b/java/com/android/dialer/callcomposer/MessageComposerFragment.java
@@ -77,6 +77,7 @@
       customMessage.addTextChangedListener(
           new TextWatcher() {
             @Override
+
             public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
 
             @Override
@@ -90,8 +91,6 @@
     }
     view.findViewById(R.id.message_chat).setOnClickListener(this);
     view.findViewById(R.id.message_question).setOnClickListener(this);
-
-    setTopView(urgent);
     return view;
   }
 
@@ -140,4 +139,9 @@
   public boolean shouldHide() {
     return TextUtils.isEmpty(getMessage());
   }
+
+  @Override
+  public void clearComposer() {
+    customMessage.getText().clear();
+  }
 }
diff --git a/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java b/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java
index 1500094..a23014b 100644
--- a/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java
+++ b/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java
@@ -74,7 +74,9 @@
       if (mHeightPercent != 1.0f) {
         writeClippedBitmap(outputStream);
       } else {
-        outputStream.write(mBytes, 0, mBytes.length);
+        Bitmap bitmap = BitmapFactory.decodeByteArray(mBytes, 0, mBytes.length);
+        bitmap = CopyAndResizeImageTask.resizeForEnrichedCalling(bitmap);
+        bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);
       }
     }
 
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml b/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml
index 75401b1..a4198fc 100644
--- a/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml
+++ b/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml
@@ -46,6 +46,14 @@
       android:background="@android:color/white"
       android:visibility="gone" />
 
+    <ImageView
+      android:id="@+id/preview_image_view"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      android:scaleType="centerCrop"
+      android:background="#000000"
+      android:visibility="gone"/>
+
     <!-- Need a background on this view in order for the ripple effect to have a place to draw -->
     <FrameLayout
       android:id="@+id/camera_button_container"
diff --git a/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml b/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml
index 518b53f..f687f0b 100644
--- a/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml
+++ b/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml
@@ -120,13 +120,14 @@
         android:visibility="invisible"
         android:background="@color/compose_and_call_background">
         <TextView
+          android:id="@+id/send_and_call_text"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_gravity="center"
           android:drawableStart="@drawable/quantum_ic_call_white_18"
           android:drawablePadding="@dimen/send_and_call_drawable_padding"
           android:textAllCaps="true"
-          android:text="@string/send_and_call"
+          android:text="@string/share_and_call"
           android:textSize="@dimen/send_and_call_text_size"
           android:fontFamily="sans-serif-medium"
           android:textColor="@color/background_dialer_white"/>
@@ -140,8 +141,8 @@
     android:layout_height="wrap_content"
     android:minHeight="?attr/actionBarSize"
     android:visibility="invisible"
-    android:titleTextAppearance="@style/call_composer_toolbar_title_text"
-    android:subtitleTextAppearance="@style/call_composer_toolbar_subtitle_text"
+    android:titleTextAppearance="@style/toolbar_title_text"
+    android:subtitleTextAppearance="@style/toolbar_subtitle_text"
     android:navigationIcon="@drawable/quantum_ic_close_white_24"
     android:background="@color/dialer_theme_color"/>
 </FrameLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml b/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml
index 58893ba..a4bd4df 100644
--- a/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml
+++ b/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml
@@ -27,7 +27,7 @@
     android:paddingLeft="@dimen/gallery_item_padding"
     android:paddingRight="@dimen/gallery_item_padding"
     android:paddingTop="@dimen/gallery_item_padding"
-    android:numColumns="3"/>
+    android:numColumns="@integer/gallery_composer_grid_view_rows"/>
 
   <include
     android:id="@+id/permission_view"
diff --git a/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml b/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml
index 97f232b..577887b 100644
--- a/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml
+++ b/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml
@@ -15,43 +15,45 @@
   ~ limitations under the License
   -->
 <LinearLayout
-  xmlns:android="http://schemas.android.com/apk/res/android"
-  android:layout_width="match_parent"
-  android:layout_height="@dimen/call_composer_view_pager_height"
-  android:orientation="vertical"
-  android:gravity="bottom"
-  android:background="@color/background_dialer_white">
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/call_composer_view_pager_height"
+    android:orientation="vertical"
+    android:gravity="bottom"
+    android:background="@color/background_dialer_white">
 
   <TextView
-    android:id="@+id/message_urgent"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:text="@string/urgent"
-    style="@style/message_composer_textview"/>
+      android:id="@+id/message_urgent"
+      android:layout_width="match_parent"
+      android:layout_height="56dp"
+      android:layout_marginTop="8dp"
+      android:text="@string/urgent"
+      style="@style/message_composer_textview"/>
 
   <TextView
-    android:id="@+id/message_chat"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:text="@string/want_to_chat"
-    style="@style/message_composer_textview"/>
+      android:id="@+id/message_chat"
+      android:layout_width="match_parent"
+      android:layout_height="56dp"
+      android:text="@string/want_to_chat"
+      style="@style/message_composer_textview"/>
 
   <TextView
-    android:id="@+id/message_question"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:text="@string/quick_question"
-    style="@style/message_composer_textview"/>
+      android:id="@+id/message_question"
+      android:layout_width="match_parent"
+      android:layout_height="56dp"
+      android:layout_marginBottom="8dp"
+      android:text="@string/quick_question"
+      style="@style/message_composer_textview"/>
 
   <View
-    android:layout_width="match_parent"
-    android:layout_height="@dimen/message_composer_divider_height"
-    android:background="@color/call_composer_divider"/>
+      android:layout_width="match_parent"
+      android:layout_height="@dimen/message_composer_divider_height"
+      android:background="@color/call_composer_divider"/>
 
   <RelativeLayout
-    android:orientation="horizontal"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
+      android:orientation="horizontal"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content">
 
     <EditText
       android:id="@+id/custom_message"
@@ -59,21 +61,22 @@
       android:layout_height="wrap_content"
       android:padding="@dimen/message_composer_item_padding"
       android:textSize="@dimen/message_compose_item_text_size"
-      android:hint="@string/custom_message_hint"
+      android:hint="@string/message_composer_custom_message_hint"
       android:textColor="@color/dialer_primary_text_color"
       android:textColorHint="@color/dialer_edit_text_hint_color"
       android:background="@color/background_dialer_white"
       android:textCursorDrawable="@drawable/searchedittext_custom_cursor"
-      android:layout_toLeftOf="@+id/remaining_characters"/>
+      android:layout_toStartOf="@+id/remaining_characters"
+      android:imeOptions="flagNoExtractUi"/>
 
     <TextView
-      android:id="@+id/remaining_characters"
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"
-      android:layout_marginRight="@dimen/message_composer_item_padding"
-      android:layout_alignParentRight="true"
-      android:layout_centerVertical="true"
-      android:textSize="@dimen/message_compose_remaining_char_text_size"
-      android:textColor="@color/dialer_edit_text_hint_color"/>
+        android:id="@+id/remaining_characters"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/message_composer_item_padding"
+        android:layout_alignParentEnd="true"
+        android:layout_centerVertical="true"
+        android:textSize="@dimen/message_compose_remaining_char_text_size"
+        android:textColor="@color/dialer_edit_text_hint_color"/>
   </RelativeLayout>
 </LinearLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values-h260dp/values.xml b/java/com/android/dialer/callcomposer/res/values-h260dp/values.xml
new file mode 100644
index 0000000..c31f3b0
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values-h260dp/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2016 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<resources>
+  <bool name="show_toolbar">true</bool>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values-h480dp/values.xml b/java/com/android/dialer/callcomposer/res/values-h480dp/values.xml
new file mode 100644
index 0000000..77b77a5
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values-h480dp/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2016 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<resources>
+  <bool name="show_toolbar">false</bool>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values-w360dp/values.xml b/java/com/android/dialer/callcomposer/res/values-w360dp/values.xml
new file mode 100644
index 0000000..adff635
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values-w360dp/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2016 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<resources>
+  <integer name="gallery_composer_grid_view_rows">3</integer>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values-w500dp/values.xml b/java/com/android/dialer/callcomposer/res/values-w500dp/values.xml
new file mode 100644
index 0000000..3ec2b35
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values-w500dp/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2016 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<resources>
+  <integer name="gallery_composer_grid_view_rows">4</integer>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values/dimens.xml b/java/com/android/dialer/callcomposer/res/values/dimens.xml
index 3ebda7a..5571170 100644
--- a/java/com/android/dialer/callcomposer/res/values/dimens.xml
+++ b/java/com/android/dialer/callcomposer/res/values/dimens.xml
@@ -17,10 +17,6 @@
 <resources>
   <dimen name="call_composer_view_pager_height">258dp</dimen>
 
-  <!-- Toolbar -->
-  <dimen name="toolbar_title_text_size">16sp</dimen>
-  <dimen name="toolbar_subtitle_text_size">14sp</dimen>
-
   <!-- Contact bar -->
   <dimen name="call_composer_contact_photo_border_thickness">2dp</dimen>
   <dimen name="call_composer_contact_photo_size">116dp</dimen>
diff --git a/java/com/android/dialer/callcomposer/res/values/strings.xml b/java/com/android/dialer/callcomposer/res/values/strings.xml
index 35a8cf9..cc7762b 100644
--- a/java/com/android/dialer/callcomposer/res/values/strings.xml
+++ b/java/com/android/dialer/callcomposer/res/values/strings.xml
@@ -22,9 +22,11 @@
   <!-- A default message to send with a phone call. [CHAR LIMIT=27] -->
   <string name="quick_question">Quick question…</string>
   <!-- Hint in a text field to compose a custom message to send with a phone call [CHAR LIMIT=27] -->
-  <string name="custom_message_hint">Write a custom message</string>
-  <!-- Text for a button to make a phone call combined with a picture or text message [CHAR LIMIT=26] -->
+  <string name="message_composer_custom_message_hint">Write a custom message</string>
+  <!-- Text for a button to make a phone call combined with a text message [CHAR LIMIT=26] -->
   <string name="send_and_call">Send and call</string>
+  <!-- Text for a button to make a phone call combined with a picture or other media [CHAR LIMIT=26] -->
+  <string name="share_and_call">Share and call</string>
   <!-- Accessibility description for each image in the gallery. For example, "image January 17 2015 1 59 pm". -->
   <string name="gallery_item_description">image <xliff:g id="date">%1$tB %1$te %1$tY %1$tl %1$tM %1$tp</xliff:g></string>
   <!-- Accessibility description for each image in the gallery when no date is present. -->
diff --git a/java/com/android/dialer/callcomposer/res/values/styles.xml b/java/com/android/dialer/callcomposer/res/values/styles.xml
index 891f639..29ac4dd 100644
--- a/java/com/android/dialer/callcomposer/res/values/styles.xml
+++ b/java/com/android/dialer/callcomposer/res/values/styles.xml
@@ -36,15 +36,6 @@
     <item name="android:textColor">@color/dialer_primary_text_color</item>
     <item name="android:padding">@dimen/message_composer_item_padding</item>
     <item name="android:background">@drawable/item_background_material_light</item>
-  </style>
-
-  <style name="call_composer_toolbar_title_text">
-    <item name="android:textSize">@dimen/toolbar_title_text_size</item>
-    <item name="android:textColor">@color/background_dialer_white</item>
-  </style>
-
-  <style name="call_composer_toolbar_subtitle_text">
-    <item name="android:textSize">@dimen/toolbar_subtitle_text_size</item>
-    <item name="android:textColor">@color/background_dialer_white</item>
+    <item name="android:gravity">center_vertical</item>
   </style>
 </resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values/values.xml b/java/com/android/dialer/callcomposer/res/values/values.xml
new file mode 100644
index 0000000..39b8e40
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values/values.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2016 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<resources>
+  <integer name="gallery_composer_grid_view_rows">2</integer>
+  <bool name="show_toolbar">false</bool>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/calldetails/AndroidManifest.xml b/java/com/android/dialer/calldetails/AndroidManifest.xml
new file mode 100644
index 0000000..b71207b
--- /dev/null
+++ b/java/com/android/dialer/calldetails/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~      http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<manifest
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.android.dialer.calldetails">
+  <application>
+    <activity
+      android:label="@string/call_details"
+      android:name="com.android.dialer.calldetails.CallDetailsActivity"
+      android:theme="@style/Theme.AppCompat.NoActionBar">
+      <intent-filter>
+        <action android:name="android.intent.action.VIEW"/>
+        <category android:name="android.intent.category.DEFAULT"/>
+        <data android:mimeType="vnd.android.cursor.item/calls"/>
+      </intent-filter>
+    </activity>
+  </application>
+</manifest>
diff --git a/java/com/android/dialer/calldetails/CallDetailsActivity.java b/java/com/android/dialer/calldetails/CallDetailsActivity.java
new file mode 100644
index 0000000..6070640
--- /dev/null
+++ b/java/com/android/dialer/calldetails/CallDetailsActivity.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.calldetails;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.MenuItem;
+import android.widget.Toolbar;
+import com.android.dialer.callcomposer.nano.CallComposerContact;
+import com.android.dialer.calldetails.nano.CallDetailsEntries;
+import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.protos.ProtoParsers;
+
+/** Displays the details of a specific call log entry. */
+public class CallDetailsActivity extends AppCompatActivity {
+
+  private static final String EXTRA_CALL_DETAILS_ENTRIES = "call_details_entries";
+  private static final String EXTRA_CONTACT = "contact";
+  private static final String TASK_DELETE = "task_delete";
+
+  private CallDetailsEntry[] entries;
+
+  public static Intent newInstance(
+      Context context, @NonNull CallDetailsEntries details, @NonNull CallComposerContact contact) {
+    Assert.isNotNull(details);
+    Assert.isNotNull(contact);
+
+    Intent intent = new Intent(context, CallDetailsActivity.class);
+    ProtoParsers.put(intent, EXTRA_CONTACT, contact);
+    ProtoParsers.put(intent, EXTRA_CALL_DETAILS_ENTRIES, details);
+    return intent;
+  }
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.call_details_activity);
+
+    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+    setActionBar(toolbar);
+    toolbar.inflateMenu(R.menu.call_details_menu);
+    toolbar.setNavigationOnClickListener(v -> finish());
+    onHandleIntent(getIntent());
+  }
+
+  @Override
+  protected void onNewIntent(Intent intent) {
+    super.onNewIntent(intent);
+    onHandleIntent(intent);
+  }
+
+  private void onHandleIntent(Intent intent) {
+    Bundle arguments = intent.getExtras();
+    CallComposerContact contact =
+        ProtoParsers.getFromInstanceState(arguments, EXTRA_CONTACT, new CallComposerContact());
+    entries =
+        ProtoParsers.getFromInstanceState(
+                arguments, EXTRA_CALL_DETAILS_ENTRIES, new CallDetailsEntries())
+            .entries;
+    RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
+    recyclerView.setLayoutManager(new LinearLayoutManager(this));
+    recyclerView.setAdapter(new CallDetailsAdapter(this, contact, entries));
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    if (item.getItemId() == R.id.call_detail_delete_menu_item) {
+      Logger.get(this).logImpression(DialerImpression.Type.USER_DELETED_CALL_LOG_ITEM);
+      AsyncTaskExecutors.createAsyncTaskExecutor().submit(TASK_DELETE, new DeleteCallsTask());
+      item.setEnabled(false);
+      return true;
+    }
+    return super.onOptionsItemSelected(item);
+  }
+
+  /** Delete specified calls from the call log. */
+  private class DeleteCallsTask extends AsyncTask<Void, Void, Void> {
+
+    private final String callIds;
+
+    DeleteCallsTask() {
+      StringBuilder callIds = new StringBuilder();
+      for (CallDetailsEntry entry : entries) {
+        if (callIds.length() != 0) {
+          callIds.append(",");
+        }
+        callIds.append(entry.callId);
+      }
+      this.callIds = callIds.toString();
+    }
+
+    @Override
+    protected Void doInBackground(Void... params) {
+      getContentResolver()
+          .delete(Calls.CONTENT_URI, CallLog.Calls._ID + " IN (" + callIds + ")", null);
+      return null;
+    }
+
+    @Override
+    public void onPostExecute(Void result) {
+      finish();
+    }
+  }
+}
diff --git a/java/com/android/dialer/calldetails/CallDetailsAdapter.java b/java/com/android/dialer/calldetails/CallDetailsAdapter.java
new file mode 100644
index 0000000..9545830
--- /dev/null
+++ b/java/com/android/dialer/calldetails/CallDetailsAdapter.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.calldetails;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import com.android.dialer.callcomposer.nano.CallComposerContact;
+import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry;
+import com.android.dialer.calllogutils.CallTypeHelper;
+import com.android.dialer.common.Assert;
+
+/** Adapter for RecyclerView in {@link CallDetailsActivity}. */
+public class CallDetailsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+
+  private static final int HEADER_VIEW_TYPE = 1;
+  private static final int CALL_ENTRY_VIEW_TYPE = 2;
+  private static final int FOOTER_VIEW_TYPE = 3;
+
+  private final CallComposerContact contact;
+  private final CallDetailsEntry[] callDetailsEntries;
+  private final CallTypeHelper callTypeHelper;
+
+  public CallDetailsAdapter(
+      Context context, CallComposerContact contact, CallDetailsEntry[] callDetailsEntries) {
+    this.contact = Assert.isNotNull(contact);
+    this.callDetailsEntries = Assert.isNotNull(callDetailsEntries);
+    callTypeHelper = new CallTypeHelper(context.getResources());
+  }
+
+  @Override
+  public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+    LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+    switch (viewType) {
+      case HEADER_VIEW_TYPE:
+        return new CallDetailsHeaderViewHolder(
+            inflater.inflate(R.layout.contact_container, parent, false));
+      case CALL_ENTRY_VIEW_TYPE:
+        return new CallDetailsEntryViewHolder(
+            inflater.inflate(R.layout.call_details_entry, parent, false));
+      case FOOTER_VIEW_TYPE:
+        return new CallDetailsFooterViewHolder(
+            inflater.inflate(R.layout.call_details_footer, parent, false));
+      default:
+        Assert.fail("No ViewHolder available for viewType: " + viewType);
+        return null;
+    }
+  }
+
+  @Override
+  public void onBindViewHolder(ViewHolder holder, int position) {
+    if (position == 0) { // Header
+      ((CallDetailsHeaderViewHolder) holder).updateContactInfo(contact);
+    } else if (position == getItemCount() - 1) {
+      ((CallDetailsFooterViewHolder) holder).setPhoneNumber(contact.number);
+    } else {
+      CallDetailsEntryViewHolder viewHolder = (CallDetailsEntryViewHolder) holder;
+      viewHolder.setCallDetails(
+          contact.number,
+          callDetailsEntries[position - 1],
+          callTypeHelper,
+          position != getItemCount() - 2);
+    }
+  }
+
+  @Override
+  public int getItemViewType(int position) {
+    if (position == 0) { // Header
+      return HEADER_VIEW_TYPE;
+    } else if (position == getItemCount() - 1) {
+      return FOOTER_VIEW_TYPE;
+    } else {
+      return CALL_ENTRY_VIEW_TYPE;
+    }
+  }
+
+  @Override
+  public int getItemCount() {
+    return callDetailsEntries.length + 2; // Header + footer
+  }
+}
diff --git a/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java
new file mode 100644
index 0000000..b1a70af
--- /dev/null
+++ b/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.calldetails;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry;
+import com.android.dialer.calllogutils.CallEntryFormatter;
+import com.android.dialer.calllogutils.CallTypeHelper;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.AppCompatConstants;
+import com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult;
+import com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type;
+import com.android.dialer.util.CallUtil;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.IntentUtil;
+
+/** ViewHolder for call entries in {@link CallDetailsActivity}. */
+public class CallDetailsEntryViewHolder extends ViewHolder {
+
+  private final ImageView callTypeIcon;
+  private final TextView callTypeText;
+  private final TextView callTime;
+  private final TextView callDuration;
+
+  private final View multimediaImageContainer;
+  private final View multimediaDetailsContainer;
+  private final View multimediaDivider;
+
+  private final TextView multimediaDetails;
+
+  private final ImageView multimediaImage;
+
+  // TODO: Display this when location is stored - b/36160042
+  @SuppressWarnings("unused")
+  private final TextView multimediaAttachmentsNumber;
+
+  private final Context context;
+
+  public CallDetailsEntryViewHolder(View container) {
+    super(container);
+    context = container.getContext();
+
+    callTypeIcon = (ImageView) container.findViewById(R.id.call_direction);
+    callTypeText = (TextView) container.findViewById(R.id.call_type);
+    callTime = (TextView) container.findViewById(R.id.call_time);
+    callDuration = (TextView) container.findViewById(R.id.call_duration);
+
+    multimediaImageContainer = container.findViewById(R.id.multimedia_image_container);
+    multimediaDetailsContainer = container.findViewById(R.id.ec_container);
+    multimediaDivider = container.findViewById(R.id.divider);
+    multimediaDetails = (TextView) container.findViewById(R.id.multimedia_details);
+    multimediaImage = (ImageView) container.findViewById(R.id.multimedia_image);
+    multimediaAttachmentsNumber =
+        (TextView) container.findViewById(R.id.multimedia_attachments_number);
+  }
+
+  void setCallDetails(
+      String number,
+      CallDetailsEntry entry,
+      CallTypeHelper callTypeHelper,
+      boolean showMultimediaDivider) {
+    int callType = entry.callType;
+    boolean isVideoCall =
+        (entry.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO
+            && CallUtil.isVideoEnabled(context);
+    boolean isPulledCall =
+        (entry.features & Calls.FEATURES_PULLED_EXTERNALLY) == Calls.FEATURES_PULLED_EXTERNALLY;
+
+    Drawable callIcon = getIconForCallType(context.getResources(), callType);
+    int color = getColorForCallType(context, callType);
+    callIcon.setColorFilter(color, PorterDuff.Mode.MULTIPLY);
+    callTime.setTextColor(color);
+    callTypeIcon.setImageDrawable(callIcon);
+
+    callTypeText.setText(callTypeHelper.getCallTypeText(callType, isVideoCall, isPulledCall));
+    callTime.setText(CallEntryFormatter.formatDate(context, entry.date));
+    if (CallTypeHelper.isMissedCallType(callType)) {
+      callDuration.setVisibility(View.GONE);
+    } else {
+      callDuration.setVisibility(View.VISIBLE);
+      callDuration.setText(
+          CallEntryFormatter.formatDurationAndDataUsage(context, entry.duration, entry.dataUsage));
+    }
+    setMultimediaDetails(number, entry, showMultimediaDivider);
+  }
+
+  private void setMultimediaDetails(String number, CallDetailsEntry entry, boolean showDivider) {
+    multimediaDivider.setVisibility(showDivider ? View.VISIBLE : View.GONE);
+    if (entry.historyResults == null || entry.historyResults.length <= 0) {
+      LogUtil.i("CallDetailsEntryViewHolder.setMultimediaDetails", "no data, hiding UI");
+      multimediaDetailsContainer.setVisibility(View.GONE);
+    } else {
+
+      // TODO: b/36158891 Add room for 2 pieces of enriched call data. It's possible
+      // to have both call composer data and post call data for a single call.
+      HistoryResult historyResult = entry.historyResults[0];
+      multimediaDetailsContainer.setVisibility(View.VISIBLE);
+      multimediaDetailsContainer.setOnClickListener(
+          (v) -> {
+            DialerUtils.startActivityWithErrorToast(context, IntentUtil.getSendSmsIntent(number));
+          });
+      multimediaImageContainer.setClipToOutline(true);
+
+      if (!TextUtils.isEmpty(historyResult.imageUri)) {
+        LogUtil.i("CallDetailsEntryViewHolder.setMultimediaDetails", "setting image");
+        multimediaImageContainer.setVisibility(View.VISIBLE);
+        multimediaImage.setImageURI(Uri.parse(historyResult.imageUri));
+        multimediaDetails.setText(
+            isIncoming(historyResult) ? R.string.received_a_photo : R.string.sent_a_photo);
+      } else {
+        LogUtil.i("CallDetailsEntryViewHolder.setMultimediaDetails", "no image");
+      }
+
+      // Set text after image to overwrite the received/sent a photo text
+      if (!TextUtils.isEmpty(historyResult.text)) {
+        LogUtil.i("CallDetailsEntryViewHolder.setMultimediaDetails", "showing text");
+        multimediaDetails.setText(
+            context.getString(R.string.message_in_quotes, historyResult.text));
+      } else {
+        LogUtil.i("CallDetailsEntryViewHolder.setMultimediaDetails", "no text");
+      }
+    }
+  }
+
+  private static boolean isIncoming(@NonNull HistoryResult historyResult) {
+    return historyResult.type == Type.INCOMING_POST_CALL
+        || historyResult.type == Type.INCOMING_CALL_COMPOSER;
+  }
+
+  private static Drawable getIconForCallType(Resources resources, int callType) {
+    switch (callType) {
+      case AppCompatConstants.CALLS_OUTGOING_TYPE:
+        return resources.getDrawable(R.drawable.quantum_ic_call_made_white_24);
+      case AppCompatConstants.CALLS_BLOCKED_TYPE:
+        return resources.getDrawable(R.drawable.quantum_ic_block_white_24);
+      case AppCompatConstants.CALLS_INCOMING_TYPE:
+      case AppCompatConstants.CALLS_ANSWERED_EXTERNALLY_TYPE:
+      case AppCompatConstants.CALLS_REJECTED_TYPE:
+        return resources.getDrawable(R.drawable.quantum_ic_call_received_white_24);
+      case AppCompatConstants.CALLS_MISSED_TYPE:
+      default:
+        // It is possible for users to end up with calls with unknown call types in their
+        // call history, possibly due to 3rd party call log implementations (e.g. to
+        // distinguish between rejected and missed calls). Instead of crashing, just
+        // assume that all unknown call types are missed calls.
+        return resources.getDrawable(R.drawable.quantum_ic_call_missed_white_24);
+    }
+  }
+
+  private static @ColorInt int getColorForCallType(Context context, int callType) {
+    switch (callType) {
+      case AppCompatConstants.CALLS_OUTGOING_TYPE:
+      case AppCompatConstants.CALLS_VOICEMAIL_TYPE:
+      case AppCompatConstants.CALLS_BLOCKED_TYPE:
+      case AppCompatConstants.CALLS_INCOMING_TYPE:
+      case AppCompatConstants.CALLS_ANSWERED_EXTERNALLY_TYPE:
+      case AppCompatConstants.CALLS_REJECTED_TYPE:
+        return ContextCompat.getColor(context, R.color.dialer_secondary_text_color);
+      case AppCompatConstants.CALLS_MISSED_TYPE:
+      default:
+        // It is possible for users to end up with calls with unknown call types in their
+        // call history, possibly due to 3rd party call log implementations (e.g. to
+        // distinguish between rejected and missed calls). Instead of crashing, just
+        // assume that all unknown call types are missed calls.
+        return ContextCompat.getColor(context, R.color.missed_call);
+    }
+  }
+}
diff --git a/java/com/android/dialer/calldetails/CallDetailsFooterViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsFooterViewHolder.java
new file mode 100644
index 0000000..36662ba
--- /dev/null
+++ b/java/com/android/dialer/calldetails/CallDetailsFooterViewHolder.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.calldetails;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.contacts.common.ClipboardUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.util.CallUtil;
+import com.android.dialer.util.DialerUtils;
+
+/** ViewHolder container for {@link CallDetailsActivity} footer. */
+public class CallDetailsFooterViewHolder extends RecyclerView.ViewHolder
+    implements OnClickListener {
+
+  private final View copy;
+  private final View edit;
+
+  private String number;
+
+  public CallDetailsFooterViewHolder(View view) {
+    super(view);
+    copy = view.findViewById(R.id.call_detail_action_copy);
+    edit = view.findViewById(R.id.call_detail_action_edit_before_call);
+
+    copy.setOnClickListener(this);
+    edit.setOnClickListener(this);
+  }
+
+  public void setPhoneNumber(String number) {
+    this.number = number;
+  }
+
+  @Override
+  public void onClick(View view) {
+    Context context = view.getContext();
+    if (view == copy) {
+      Logger.get(context).logImpression(DialerImpression.Type.CALL_DETAILS_COPY_NUMBER);
+      ClipboardUtils.copyText(context, null, number, true);
+    } else if (view == edit) {
+      Logger.get(context).logImpression(DialerImpression.Type.CALL_DETAILS_EDIT_BEFORE_CALL);
+      Intent dialIntent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(number));
+      DialerUtils.startActivityWithErrorToast(context, dialIntent);
+    } else {
+      Assert.fail("View on click not implemented: " + view);
+    }
+  }
+}
diff --git a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
new file mode 100644
index 0000000..1679c2b
--- /dev/null
+++ b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.calldetails;
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.callcomposer.nano.CallComposerContact;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.common.Assert;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.util.DialerUtils;
+
+/** ViewHolder for Header/Contact in {@link CallDetailsActivity}. */
+public class CallDetailsHeaderViewHolder extends RecyclerView.ViewHolder
+    implements OnClickListener {
+
+  private final View callBackButton;
+  private final TextView nameView;
+  private final TextView numberView;
+  private final QuickContactBadge contactPhoto;
+  private final Context context;
+
+  private CallComposerContact contact;
+
+  CallDetailsHeaderViewHolder(View container) {
+    super(container);
+    context = container.getContext();
+    callBackButton = container.findViewById(R.id.call_back_button);
+    nameView = (TextView) container.findViewById(R.id.contact_name);
+    numberView = (TextView) container.findViewById(R.id.phone_number);
+    contactPhoto = (QuickContactBadge) container.findViewById(R.id.quick_contact_photo);
+    callBackButton.setOnClickListener(this);
+  }
+
+  /**
+   * Populates the contact info fields based on the current contact information. Copied from {@link
+   * com.android.contacts.common.dialog.CallSubjectDialog}.
+   */
+  public void updateContactInfo(CallComposerContact contact) {
+    this.contact = contact;
+    setPhoto(
+        contact.photoId,
+        Uri.parse(contact.photoUri),
+        Uri.parse(contact.contactUri),
+        contact.nameOrNumber,
+        contact.isBusiness);
+
+    nameView.setText(contact.nameOrNumber);
+    if (!TextUtils.isEmpty(contact.numberLabel) && !TextUtils.isEmpty(contact.displayNumber)) {
+      numberView.setVisibility(View.VISIBLE);
+      String secondaryInfo =
+          context.getString(
+              com.android.contacts.common.R.string.call_subject_type_and_number,
+              contact.numberLabel,
+              contact.displayNumber);
+      numberView.setText(secondaryInfo);
+    } else {
+      numberView.setVisibility(View.GONE);
+      numberView.setText(null);
+    }
+  }
+
+  /**
+   * Sets the photo on the quick contact galleryIcon. Copied from {@link
+   * com.android.contacts.common.dialog.CallSubjectDialog}.
+   */
+  private void setPhoto(
+      long photoId, Uri photoUri, Uri contactUri, String displayName, boolean isBusiness) {
+    contactPhoto.assignContactUri(contactUri);
+    contactPhoto.setOverlay(null);
+
+    int contactType =
+        isBusiness ? ContactPhotoManager.TYPE_BUSINESS : ContactPhotoManager.TYPE_DEFAULT;
+    String lookupKey = contactUri == null ? null : UriUtils.getLookupKeyFromUri(contactUri);
+
+    ContactPhotoManager.DefaultImageRequest request =
+        new ContactPhotoManager.DefaultImageRequest(
+            displayName, lookupKey, contactType, true /* isCircular */);
+
+    if (photoId == 0 && photoUri != null) {
+      contactPhoto.setImageDrawable(
+          context.getDrawable(R.drawable.product_logo_avatar_anonymous_color_120));
+    } else {
+      ContactPhotoManager.getInstance(context)
+          .loadThumbnail(
+              contactPhoto, photoId, false /* darkTheme */, true /* isCircular */, request);
+    }
+  }
+
+  @Override
+  public void onClick(View view) {
+    if (view == callBackButton) {
+      Logger.get(view.getContext()).logImpression(DialerImpression.Type.CALL_DETAILS_CALL_BACK);
+      DialerUtils.startActivityWithErrorToast(
+          view.getContext(),
+          new CallIntentBuilder(contact.number, CallInitiationType.Type.CALL_DETAILS).build());
+    } else {
+      Assert.fail("View OnClickListener not implemented: " + view);
+    }
+  }
+}
diff --git a/java/com/android/dialer/calldetails/nano/CallDetailsEntries.java b/java/com/android/dialer/calldetails/nano/CallDetailsEntries.java
new file mode 100644
index 0000000..aee8f36
--- /dev/null
+++ b/java/com/android/dialer/calldetails/nano/CallDetailsEntries.java
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler.  DO NOT EDIT!
+
+package com.android.dialer.calldetails.nano;
+
+/** This file is autogenerated, but javadoc required. */
+@SuppressWarnings("hiding")
+public final class CallDetailsEntries
+    extends com.google.protobuf.nano.ExtendableMessageNano<CallDetailsEntries> {
+
+  /** This file is autogenerated, but javadoc required. */
+  public static final class CallDetailsEntry
+      extends com.google.protobuf.nano.ExtendableMessageNano<CallDetailsEntry> {
+
+    private static volatile CallDetailsEntry[] _emptyArray;
+    public static CallDetailsEntry[] emptyArray() {
+      // Lazily initializes the empty array
+      if (_emptyArray == null) {
+        synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+          if (_emptyArray == null) {
+            _emptyArray = new CallDetailsEntry[0];
+          }
+        }
+      }
+      return _emptyArray;
+    }
+
+    // optional int64 call_id = 1;
+    public long callId;
+
+    // optional int32 call_type = 2;
+    public int callType;
+
+    // optional int32 features = 3;
+    public int features;
+
+    // optional int64 date = 4;
+    public long date;
+
+    // optional int64 duration = 5;
+    public long duration;
+
+    // optional int64 data_usage = 6;
+    public long dataUsage;
+
+    // repeated .com.android.dialer.enrichedcall.historyquery.proto.
+    // HistoryResult history_results = 7;
+    public com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult[] historyResults;
+
+    // @@protoc_insertion_point(class_scope:com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry)
+
+    public CallDetailsEntry() {
+      clear();
+    }
+
+    public CallDetailsEntry clear() {
+      callId = 0L;
+      callType = 0;
+      features = 0;
+      date = 0L;
+      duration = 0L;
+      dataUsage = 0L;
+      historyResults =
+          com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.emptyArray();
+      unknownFieldData = null;
+      cachedSize = -1;
+      return this;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (o == this) {
+        return true;
+      }
+      if (!(o instanceof CallDetailsEntry)) {
+        return false;
+      }
+      CallDetailsEntry other = (CallDetailsEntry) o;
+      if (this.callId != other.callId) {
+        return false;
+      }
+      if (this.callType != other.callType) {
+        return false;
+      }
+      if (this.features != other.features) {
+        return false;
+      }
+      if (this.date != other.date) {
+        return false;
+      }
+      if (this.duration != other.duration) {
+        return false;
+      }
+      if (this.dataUsage != other.dataUsage) {
+        return false;
+      }
+      if (!com.google.protobuf.nano.InternalNano.equals(
+          this.historyResults, other.historyResults)) {
+        return false;
+      }
+      if (unknownFieldData == null || unknownFieldData.isEmpty()) {
+        return other.unknownFieldData == null || other.unknownFieldData.isEmpty();
+      } else {
+        return unknownFieldData.equals(other.unknownFieldData);
+      }
+    }
+
+    @Override
+    public int hashCode() {
+      int result = 17;
+      result = 31 * result + getClass().getName().hashCode();
+      result = 31 * result + (int) (this.callId ^ (this.callId >>> 32));
+      result = 31 * result + this.callType;
+      result = 31 * result + this.features;
+      result = 31 * result + (int) (this.date ^ (this.date >>> 32));
+      result = 31 * result + (int) (this.duration ^ (this.duration >>> 32));
+      result = 31 * result + (int) (this.dataUsage ^ (this.dataUsage >>> 32));
+      result = 31 * result + com.google.protobuf.nano.InternalNano.hashCode(this.historyResults);
+      result =
+          31 * result
+              + (unknownFieldData == null || unknownFieldData.isEmpty()
+                  ? 0
+                  : unknownFieldData.hashCode());
+      return result;
+    }
+
+    @Override
+    public void writeTo(com.google.protobuf.nano.CodedOutputByteBufferNano output)
+        throws java.io.IOException {
+      if (this.callId != 0L) {
+        output.writeInt64(1, this.callId);
+      }
+      if (this.callType != 0) {
+        output.writeInt32(2, this.callType);
+      }
+      if (this.features != 0) {
+        output.writeInt32(3, this.features);
+      }
+      if (this.date != 0L) {
+        output.writeInt64(4, this.date);
+      }
+      if (this.duration != 0L) {
+        output.writeInt64(5, this.duration);
+      }
+      if (this.dataUsage != 0L) {
+        output.writeInt64(6, this.dataUsage);
+      }
+      if (this.historyResults != null && this.historyResults.length > 0) {
+        for (int i = 0; i < this.historyResults.length; i++) {
+          com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult element =
+              this.historyResults[i];
+          if (element != null) {
+            output.writeMessage(7, element);
+          }
+        }
+      }
+      super.writeTo(output);
+    }
+
+    @Override
+    protected int computeSerializedSize() {
+      int size = super.computeSerializedSize();
+      if (this.callId != 0L) {
+        size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt64Size(1, this.callId);
+      }
+      if (this.callType != 0) {
+        size +=
+            com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt32Size(2, this.callType);
+      }
+      if (this.features != 0) {
+        size +=
+            com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt32Size(3, this.features);
+      }
+      if (this.date != 0L) {
+        size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt64Size(4, this.date);
+      }
+      if (this.duration != 0L) {
+        size +=
+            com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt64Size(5, this.duration);
+      }
+      if (this.dataUsage != 0L) {
+        size +=
+            com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt64Size(6, this.dataUsage);
+      }
+      if (this.historyResults != null && this.historyResults.length > 0) {
+        for (int i = 0; i < this.historyResults.length; i++) {
+          com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult element =
+              this.historyResults[i];
+          if (element != null) {
+            size +=
+                com.google.protobuf.nano.CodedOutputByteBufferNano.computeMessageSize(7, element);
+          }
+        }
+      }
+      return size;
+    }
+
+    @Override
+    public CallDetailsEntry mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+        throws java.io.IOException {
+      while (true) {
+        int tag = input.readTag();
+        switch (tag) {
+          case 0:
+            return this;
+          default:
+            {
+              if (!super.storeUnknownField(input, tag)) {
+                return this;
+              }
+              break;
+            }
+          case 8:
+            {
+              this.callId = input.readInt64();
+              break;
+            }
+          case 16:
+            {
+              this.callType = input.readInt32();
+              break;
+            }
+          case 24:
+            {
+              this.features = input.readInt32();
+              break;
+            }
+          case 32:
+            {
+              this.date = input.readInt64();
+              break;
+            }
+          case 40:
+            {
+              this.duration = input.readInt64();
+              break;
+            }
+          case 48:
+            {
+              this.dataUsage = input.readInt64();
+              break;
+            }
+          case 58:
+            {
+              int arrayLength =
+                  com.google.protobuf.nano.WireFormatNano.getRepeatedFieldArrayLength(input, 58);
+              int i = this.historyResults == null ? 0 : this.historyResults.length;
+              com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult[] newArray =
+                  new com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult
+                      [i + arrayLength];
+              if (i != 0) {
+                java.lang.System.arraycopy(this.historyResults, 0, newArray, 0, i);
+              }
+              for (; i < newArray.length - 1; i++) {
+                newArray[i] =
+                    new com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult();
+                input.readMessage(newArray[i]);
+                input.readTag();
+              }
+              // Last one without readTag.
+              newArray[i] =
+                  new com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult();
+              input.readMessage(newArray[i]);
+              this.historyResults = newArray;
+              break;
+            }
+        }
+      }
+    }
+
+    public static CallDetailsEntry parseFrom(byte[] data)
+        throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+      return com.google.protobuf.nano.MessageNano.mergeFrom(new CallDetailsEntry(), data);
+    }
+
+    public static CallDetailsEntry parseFrom(
+        com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException {
+      return new CallDetailsEntry().mergeFrom(input);
+    }
+  }
+
+  private static volatile CallDetailsEntries[] _emptyArray;
+  public static CallDetailsEntries[] emptyArray() {
+    // Lazily initializes the empty array
+    if (_emptyArray == null) {
+      synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+        if (_emptyArray == null) {
+          _emptyArray = new CallDetailsEntries[0];
+        }
+      }
+    }
+    return _emptyArray;
+  }
+
+  // repeated .com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry entries = 1;
+  public com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry[] entries;
+
+  // @@protoc_insertion_point(class_scope:com.android.dialer.calldetails.CallDetailsEntries)
+
+  public CallDetailsEntries() {
+    clear();
+  }
+
+  public CallDetailsEntries clear() {
+    entries = com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry.emptyArray();
+    unknownFieldData = null;
+    cachedSize = -1;
+    return this;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o == this) {
+      return true;
+    }
+    if (!(o instanceof CallDetailsEntries)) {
+      return false;
+    }
+    CallDetailsEntries other = (CallDetailsEntries) o;
+    if (!com.google.protobuf.nano.InternalNano.equals(this.entries, other.entries)) {
+      return false;
+    }
+    if (unknownFieldData == null || unknownFieldData.isEmpty()) {
+      return other.unknownFieldData == null || other.unknownFieldData.isEmpty();
+    } else {
+      return unknownFieldData.equals(other.unknownFieldData);
+    }
+  }
+
+  @Override
+  public int hashCode() {
+    int result = 17;
+    result = 31 * result + getClass().getName().hashCode();
+    result = 31 * result + com.google.protobuf.nano.InternalNano.hashCode(this.entries);
+    result =
+        31 * result
+            + (unknownFieldData == null || unknownFieldData.isEmpty()
+                ? 0
+                : unknownFieldData.hashCode());
+    return result;
+  }
+
+  @Override
+  public void writeTo(com.google.protobuf.nano.CodedOutputByteBufferNano output)
+      throws java.io.IOException {
+    if (this.entries != null && this.entries.length > 0) {
+      for (int i = 0; i < this.entries.length; i++) {
+        com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry element =
+            this.entries[i];
+        if (element != null) {
+          output.writeMessage(1, element);
+        }
+      }
+    }
+    super.writeTo(output);
+  }
+
+  @Override
+  protected int computeSerializedSize() {
+    int size = super.computeSerializedSize();
+    if (this.entries != null && this.entries.length > 0) {
+      for (int i = 0; i < this.entries.length; i++) {
+        com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry element =
+            this.entries[i];
+        if (element != null) {
+          size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeMessageSize(1, element);
+        }
+      }
+    }
+    return size;
+  }
+
+  @Override
+  public CallDetailsEntries mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+      throws java.io.IOException {
+    while (true) {
+      int tag = input.readTag();
+      switch (tag) {
+        case 0:
+          return this;
+        default:
+          {
+            if (!super.storeUnknownField(input, tag)) {
+              return this;
+            }
+            break;
+          }
+        case 10:
+          {
+            int arrayLength =
+                com.google.protobuf.nano.WireFormatNano.getRepeatedFieldArrayLength(input, 10);
+            int i = this.entries == null ? 0 : this.entries.length;
+            com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry[] newArray =
+                new com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry
+                    [i + arrayLength];
+            if (i != 0) {
+              java.lang.System.arraycopy(this.entries, 0, newArray, 0, i);
+            }
+            for (; i < newArray.length - 1; i++) {
+              newArray[i] =
+                  new com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry();
+              input.readMessage(newArray[i]);
+              input.readTag();
+            }
+            // Last one without readTag.
+            newArray[i] =
+                new com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry();
+            input.readMessage(newArray[i]);
+            this.entries = newArray;
+            break;
+          }
+      }
+    }
+  }
+
+  public static CallDetailsEntries parseFrom(byte[] data)
+      throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+    return com.google.protobuf.nano.MessageNano.mergeFrom(new CallDetailsEntries(), data);
+  }
+
+  public static CallDetailsEntries parseFrom(
+      com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException {
+    return new CallDetailsEntries().mergeFrom(input);
+  }
+}
diff --git a/java/com/android/dialer/calldetails/res/drawable/multimedia_image_background.xml b/java/com/android/dialer/calldetails/res/drawable/multimedia_image_background.xml
new file mode 100644
index 0000000..421bdbf
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/drawable/multimedia_image_background.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+  android:shape="rectangle">
+  <corners android:radius="2dp"/>
+</shape>
diff --git a/java/com/android/dialer/calldetails/res/layout/call_details_activity.xml b/java/com/android/dialer/calldetails/res/layout/call_details_activity.xml
new file mode 100644
index 0000000..038a874
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/layout/call_details_activity.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:orientation="vertical"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent">
+
+  <Toolbar
+    android:id="@+id/toolbar"
+    android:layout_width="match_parent"
+    android:layout_height="?attr/actionBarSize"
+    android:background="@color/dialer_theme_color"
+    android:elevation="4dp"
+    android:titleTextAppearance="@style/toolbar_title_text"
+    android:title="@string/call_details"
+    android:navigationIcon="@drawable/quantum_ic_arrow_back_white_24"/>
+
+  <android.support.v7.widget.RecyclerView
+    android:id="@+id/recycler_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@color/background_dialer_white"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml b/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml
new file mode 100644
index 0000000..7f8bb80
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingTop="@dimen/call_entry_padding">
+
+  <ImageView
+      android:id="@+id/call_direction"
+      android:layout_width="@dimen/call_entry_icon_size"
+      android:layout_height="@dimen/call_entry_icon_size"
+      android:layout_marginStart="@dimen/call_entry_padding"
+      android:layout_marginEnd="@dimen/call_entry_left_margin"/>
+
+  <TextView
+      android:id="@+id/call_type"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_toEndOf="@+id/call_direction"
+      style="@style/PrimaryText"/>
+
+  <TextView
+      android:id="@+id/call_time"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_toEndOf="@id/call_direction"
+      android:layout_below="@+id/call_type"
+      android:layout_marginBottom="@dimen/call_entry_bottom_padding"
+      style="@style/SecondaryText"/>
+
+  <TextView
+      android:id="@+id/call_duration"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_alignParentEnd="true"
+      android:layout_marginEnd="@dimen/call_entry_padding"
+      style="@style/PrimaryText"/>
+
+  <include
+      layout="@layout/ec_data_container"
+      android:id="@+id/ec_container"
+      android:layout_height="@dimen/ec_container_height"
+      android:layout_width="match_parent"
+      android:layout_marginStart="@dimen/ec_text_left_margin"
+      android:layout_below="@+id/call_time"
+      android:visibility="gone"/>
+
+  <View
+      android:id="@+id/divider"
+      android:layout_width="match_parent"
+      android:layout_height="1dp"
+      android:layout_below="@id/ec_container"
+      android:layout_marginTop="@dimen/ec_divider_top_bottom_margin"
+      android:layout_marginBottom="@dimen/ec_divider_top_bottom_margin"
+      android:layout_marginStart="@dimen/ec_text_left_margin"
+      android:background="#12000000"
+      android:visibility="gone"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/calldetails/res/layout/call_details_footer.xml b/java/com/android/dialer/calldetails/res/layout/call_details_footer.xml
new file mode 100644
index 0000000..885cb09
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/layout/call_details_footer.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="match_parent"
+  android:layout_height="wrap_content"
+  android:orientation="vertical">
+
+  <View
+      android:layout_width="match_parent"
+      android:layout_height="1dp"
+      android:layout_marginTop="@dimen/ec_divider_top_bottom_margin"
+      android:layout_marginBottom="@dimen/ec_divider_top_bottom_margin"
+      android:background="#12000000"/>
+
+  <TextView
+      android:id="@+id/call_detail_action_copy"
+      style="@style/CallDetailsActionItemStyle"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:drawableStart="@drawable/quantum_ic_content_copy_grey600_24"
+      android:text="@string/call_details_copy_number"/>
+
+  <TextView
+      android:id="@+id/call_detail_action_edit_before_call"
+      style="@style/CallDetailsActionItemStyle"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:drawableStart="@drawable/quantum_ic_edit_grey600_24"
+      android:text="@string/call_details_edit_number"/>
+</LinearLayout>
diff --git a/java/com/android/dialer/calldetails/res/layout/contact_container.xml b/java/com/android/dialer/calldetails/res/layout/contact_container.xml
new file mode 100644
index 0000000..95fe189
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/layout/contact_container.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center_vertical"
+    android:padding="@dimen/contact_container_padding">
+
+  <QuickContactBadge
+      android:id="@+id/quick_contact_photo"
+      android:layout_width="@dimen/call_details_contact_photo_size"
+      android:layout_height="@dimen/call_details_contact_photo_size"
+      android:focusable="true"/>
+
+  <TextView
+      android:id="@+id/contact_name"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_marginBottom="@dimen/text_bottom_margin"
+      android:layout_marginStart="@dimen/photo_text_margin"
+      android:layout_toEndOf="@+id/quick_contact_photo"
+      android:layout_toStartOf="@+id/call_back_button"
+      style="@style/PrimaryText"/>
+
+  <TextView
+      android:id="@+id/phone_number"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_marginStart="@dimen/photo_text_margin"
+      android:layout_toEndOf="@+id/quick_contact_photo"
+      android:layout_toStartOf="@+id/call_back_button"
+      android:layout_below="@+id/contact_name"
+      style="@style/SecondaryText"/>
+
+  <ImageView
+      android:id="@+id/call_back_button"
+      android:layout_width="@dimen/call_back_button_size"
+      android:layout_height="@dimen/call_back_button_size"
+      android:layout_alignParentEnd="true"
+      android:layout_centerVertical="true"
+      android:background="?android:attr/selectableItemBackgroundBorderless"
+      android:contentDescription="@string/description_call_log_call_action"
+      android:src="@drawable/quantum_ic_call_white_24"
+      android:tint="@color/secondary_text_color"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/calldetails/res/layout/ec_data_container.xml b/java/com/android/dialer/calldetails/res/layout/ec_data_container.xml
new file mode 100644
index 0000000..5ad7912
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/layout/ec_data_container.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/ec_container_height">
+
+  <TextView
+      android:id="@+id/multimedia_details"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_centerVertical="true"
+      android:maxLines="2"
+      style="@style/SecondaryText"/>
+
+  <FrameLayout
+      android:id="@+id/multimedia_image_container"
+      android:layout_width="@dimen/ec_photo_size"
+      android:layout_height="@dimen/ec_photo_size"
+      android:layout_alignParentEnd="true"
+      android:layout_marginEnd="@dimen/call_entry_padding"
+      android:layout_centerVertical="true"
+      android:background="@drawable/multimedia_image_background"
+      android:outlineProvider="background"
+      android:visibility="gone">
+
+    <ImageView
+        android:id="@+id/multimedia_image"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scaleType="centerCrop"/>
+
+    <com.android.incallui.autoresizetext.AutoResizeTextView
+        android:id="@+id/multimedia_attachments_number"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:padding="4dp"
+        android:gravity="center"
+        android:textColor="@color/background_dialer_white"
+        android:textSize="100sp"
+        android:background="#80000000"
+        android:visibility="gone"/>
+  </FrameLayout>
+</RelativeLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/calldetails/res/menu/call_details_menu.xml b/java/com/android/dialer/calldetails/res/menu/call_details_menu.xml
new file mode 100644
index 0000000..c2d1032
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/menu/call_details_menu.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+  <item
+    android:id="@+id/call_detail_delete_menu_item"
+    android:icon="@drawable/quantum_ic_delete_white_24"
+    android:title="@string/delete"
+    android:showAsAction="ifRoom"/>
+</menu>
\ No newline at end of file
diff --git a/java/com/android/dialer/calldetails/res/values/dimens.xml b/java/com/android/dialer/calldetails/res/values/dimens.xml
new file mode 100644
index 0000000..b1a8f1c
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/values/dimens.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<resources>
+  <dimen name="text_bottom_margin">2dp</dimen>
+  <dimen name="call_details_primary_text_size">16sp</dimen>
+  <dimen name="call_details_secondary_text_size">14sp</dimen>
+
+  <!-- contact container -->
+  <dimen name="contact_container_padding">16dp</dimen>
+  <dimen name="call_details_contact_photo_size">40dp</dimen>
+  <dimen name="photo_text_margin">16dp</dimen>
+  <dimen name="call_back_button_size">24dp</dimen>
+
+  <!-- call entry container -->
+  <dimen name="call_entry_icon_size">24dp</dimen>
+  <dimen name="call_entry_padding">16dp</dimen>
+  <dimen name="call_entry_bottom_padding">14dp</dimen>
+  <dimen name="call_entry_left_margin">32dp</dimen>
+
+  <!-- EC container -->
+  <dimen name="call_details_ec_text_size">12sp</dimen>
+  <dimen name="ec_container_height">48dp</dimen>
+  <dimen name="ec_text_left_margin">72dp</dimen>
+  <dimen name="ec_photo_size">40dp</dimen>
+  <dimen name="ec_divider_top_bottom_margin">8dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/calldetails/res/values/strings.xml b/java/com/android/dialer/calldetails/res/values/strings.xml
new file mode 100644
index 0000000..8a7cc4c
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/values/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+  <!-- Title bar for call detail screen -->
+  <string name="call_details">Call details</string>
+
+  <!-- Menu item in call details used to remove a call or voicemail from the call log. -->
+  <string name="delete">Delete</string>
+
+  <!-- Option displayed in context menu to copy long pressed phone number. [CHAR LIMIT=48] -->
+  <string name="call_details_copy_number">Copy number</string>
+
+  <!-- Label for action to edit a number before calling it. [CHAR LIMIT=48] -->
+  <string name="call_details_edit_number">Edit number before call</string>
+
+  <!-- String describing the phone icon on a call log list item. When tapped, it will place a
+       call to the number represented by that call log entry. [CHAR LIMIT=NONE]-->
+  <string name="description_call_log_call_action">Call</string>
+
+  <!-- String shown when the call details show a image that was sent -->
+  <string name="sent_a_photo">Sent a photo</string>
+
+  <!-- String shown when the call details show a image that was received -->
+  <string name="received_a_photo">Received a photo</string>
+
+  <!-- Messages shown to the user are wrapped in quotes, e.g. the user would see "Some text" -->
+  <string name="message_in_quotes">\"<xliff:g id="message">%1$s</xliff:g>\"</string>
+</resources>
diff --git a/java/com/android/dialer/calldetails/res/values/styles.xml b/java/com/android/dialer/calldetails/res/values/styles.xml
new file mode 100644
index 0000000..4fffe1a
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/values/styles.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<resources>
+  <style name="PrimaryText">
+    <item name="android:textColor">#DE000000</item>
+    <item name="android:textSize">@dimen/call_details_primary_text_size</item>
+    <item name="android:maxLines">1</item>
+  </style>
+
+  <style name="SecondaryText">
+    <item name="android:textColor">#8A000000</item>
+    <item name="android:textSize">@dimen/call_details_secondary_text_size</item>
+    <item name="android:maxLines">1</item>
+  </style>
+
+  <style name="ECText">
+    <item name="android:textColor">#8A000000</item>
+    <item name="android:textSize">@dimen/call_details_ec_text_size</item>
+    <item name="android:maxLines">1</item>
+  </style>
+
+  <style name="CallDetailsActionItemStyle">
+    <item name="android:foreground">?android:attr/selectableItemBackground</item>
+    <item name="android:clickable">true</item>
+    <item name="android:drawablePadding">28dp</item>
+    <item name="android:gravity">center_vertical</item>
+    <item name="android:paddingStart">28dp</item>
+    <item name="android:paddingEnd">28dp</item>
+    <item name="android:paddingTop">16dp</item>
+    <item name="android:paddingBottom">16dp</item>
+    <item name="android:textColor">#8A000000</item>
+    <item name="android:textSize">14sp</item>
+  </style>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/callintent/nano/CallInitiationType.java b/java/com/android/dialer/callintent/nano/CallInitiationType.java
index 4badd6e..1dddb6c 100644
--- a/java/com/android/dialer/callintent/nano/CallInitiationType.java
+++ b/java/com/android/dialer/callintent/nano/CallInitiationType.java
@@ -11,17 +11,19 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
  */
 
 // Generated by the protocol buffer compiler.  DO NOT EDIT!
 
 package com.android.dialer.callintent.nano;
 
+/** This file is autogenerated, but javadoc required. */
 @SuppressWarnings("hiding")
-public final class CallInitiationType extends
-    com.google.protobuf.nano.ExtendableMessageNano<CallInitiationType> {
+public final class CallInitiationType
+    extends com.google.protobuf.nano.ExtendableMessageNano<CallInitiationType> {
 
+  /** This file is autogenerated, but javadoc required. */
   // enum Type
   public interface Type {
     public static final int UNKNOWN_INITIATION = 0;
@@ -44,11 +46,11 @@
   }
 
   private static volatile CallInitiationType[] _emptyArray;
+
   public static CallInitiationType[] emptyArray() {
     // Lazily initializes the empty array
     if (_emptyArray == null) {
-      synchronized (
-          com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+      synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
         if (_emptyArray == null) {
           _emptyArray = new CallInitiationType[0];
         }
@@ -70,20 +72,20 @@
   }
 
   @Override
-  public CallInitiationType mergeFrom(
-          com.google.protobuf.nano.CodedInputByteBufferNano input)
+  public CallInitiationType mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
       throws java.io.IOException {
     while (true) {
       int tag = input.readTag();
       switch (tag) {
         case 0:
           return this;
-        default: {
-          if (!super.storeUnknownField(input, tag)) {
-            return this;
+        default:
+          {
+            if (!super.storeUnknownField(input, tag)) {
+              return this;
+            }
+            break;
           }
-          break;
-        }
       }
     }
   }
@@ -94,8 +96,7 @@
   }
 
   public static CallInitiationType parseFrom(
-          com.google.protobuf.nano.CodedInputByteBufferNano input)
-      throws java.io.IOException {
+      com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException {
     return new CallInitiationType().mergeFrom(input);
   }
 }
diff --git a/java/com/android/dialer/calllogutils/AndroidManifest.xml b/java/com/android/dialer/calllogutils/AndroidManifest.xml
new file mode 100644
index 0000000..228865a
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~      http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<manifest package="com.android.dialer.calllogutils"/>
\ No newline at end of file
diff --git a/java/com/android/dialer/calllogutils/CallEntryFormatter.java b/java/com/android/dialer/calllogutils/CallEntryFormatter.java
new file mode 100644
index 0000000..bd6d53f
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/CallEntryFormatter.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.calllogutils;
+
+import android.content.Context;
+import android.icu.lang.UCharacter;
+import android.icu.text.BreakIterator;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.text.format.DateUtils;
+import android.text.format.Formatter;
+import com.android.dialer.util.DialerUtils;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/** Utility class for formatting data and data usage in call log entries. */
+public class CallEntryFormatter {
+
+  /**
+   * Formats the provided date into a value suitable for display in the current locale.
+   *
+   * <p>For example, returns a string like "Wednesday, May 25, 2016, 8:02PM" or "Chorshanba, 2016
+   * may 25,20:02".
+   *
+   * <p>For pre-N devices, the returned value may not start with a capital if the local convention
+   * is to not capitalize day names. On N+ devices, the returned value is always capitalized.
+   */
+  public static CharSequence formatDate(Context context, long callDateMillis) {
+    CharSequence dateValue =
+        DateUtils.formatDateRange(
+            context,
+            callDateMillis /* startDate */,
+            callDateMillis /* endDate */,
+            DateUtils.FORMAT_SHOW_TIME
+                | DateUtils.FORMAT_SHOW_DATE
+                | DateUtils.FORMAT_SHOW_WEEKDAY
+                | DateUtils.FORMAT_SHOW_YEAR);
+
+    // We want the beginning of the date string to be capitalized, even if the word at the beginning
+    // of the string is not usually capitalized. For example, "Wednesdsay" in Uzbek is "chorshanba”
+    // (not capitalized). To handle this issue we apply title casing to the start of the sentence so
+    // that "chorshanba, 2016 may 25,20:02" becomes "Chorshanba, 2016 may 25,20:02".
+    //
+    // The ICU library was not available in Android until N, so we can only do this in N+ devices.
+    // Pre-N devices will still see incorrect capitalization in some languages.
+    if (VERSION.SDK_INT < VERSION_CODES.N) {
+      return dateValue;
+    }
+
+    // Using the ICU library is safer than just applying toUpperCase() on the first letter of the
+    // word because in some languages, there can be multiple starting characters which should be
+    // upper-cased together. For example in Dutch "ij" is a digraph in which both letters should be
+    // capitalized together.
+
+    // TITLECASE_NO_LOWERCASE is necessary so that things that are already capitalized like the
+    // month ("May") are not lower-cased as part of the conversion.
+    return UCharacter.toTitleCase(
+        Locale.getDefault(),
+        dateValue.toString(),
+        BreakIterator.getSentenceInstance(),
+        UCharacter.TITLECASE_NO_LOWERCASE);
+  }
+
+  private static CharSequence formatDuration(Context context, long elapsedSeconds) {
+    long minutes = 0;
+    long seconds = 0;
+
+    if (elapsedSeconds >= 60) {
+      minutes = elapsedSeconds / 60;
+      elapsedSeconds -= minutes * 60;
+      seconds = elapsedSeconds;
+      return context.getString(R.string.call_details_duration_format, minutes, seconds);
+    } else {
+      seconds = elapsedSeconds;
+      return context.getString(R.string.call_details_short_duration_format, seconds);
+    }
+  }
+
+  /**
+   * Formats a string containing the call duration and the data usage (if specified).
+   *
+   * @param elapsedSeconds Total elapsed seconds.
+   * @param dataUsage Data usage in bytes, or null if not specified.
+   * @return String containing call duration and data usage.
+   */
+  public static CharSequence formatDurationAndDataUsage(
+      Context context, long elapsedSeconds, Long dataUsage) {
+    CharSequence duration = formatDuration(context, elapsedSeconds);
+    List<CharSequence> durationItems = new ArrayList<>();
+    if (dataUsage != null) {
+      durationItems.add(duration);
+      durationItems.add(Formatter.formatShortFileSize(context, dataUsage));
+      return DialerUtils.join(durationItems);
+    } else {
+      return duration;
+    }
+  }
+}
diff --git a/java/com/android/dialer/app/calllog/CallTypeHelper.java b/java/com/android/dialer/calllogutils/CallTypeHelper.java
similarity index 98%
rename from java/com/android/dialer/app/calllog/CallTypeHelper.java
rename to java/com/android/dialer/calllogutils/CallTypeHelper.java
index f3c27a1..d3b5b67 100644
--- a/java/com/android/dialer/app/calllog/CallTypeHelper.java
+++ b/java/com/android/dialer/calllogutils/CallTypeHelper.java
@@ -14,10 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.dialer.app.calllog;
+package com.android.dialer.calllogutils;
 
 import android.content.res.Resources;
-import com.android.dialer.app.R;
 import com.android.dialer.compat.AppCompatConstants;
 
 /** Helper class to perform operations related to call types. */
diff --git a/java/com/android/dialer/app/calllog/CallTypeIconsView.java b/java/com/android/dialer/calllogutils/CallTypeIconsView.java
similarity index 88%
rename from java/com/android/dialer/app/calllog/CallTypeIconsView.java
rename to java/com/android/dialer/calllogutils/CallTypeIconsView.java
index cd5c546..61208bc 100644
--- a/java/com/android/dialer/app/calllog/CallTypeIconsView.java
+++ b/java/com/android/dialer/calllogutils/CallTypeIconsView.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.dialer.app.calllog;
+package com.android.dialer.calllogutils;
 
 import android.content.Context;
 import android.graphics.Bitmap;
@@ -26,7 +26,6 @@
 import android.util.AttributeSet;
 import android.view.View;
 import com.android.contacts.common.util.BitmapUtil;
-import com.android.dialer.app.R;
 import com.android.dialer.compat.AppCompatConstants;
 import java.util.ArrayList;
 import java.util.List;
@@ -41,6 +40,7 @@
   private static Resources sResources;
   private List<Integer> mCallTypes = new ArrayList<>(3);
   private boolean mShowVideo = false;
+  private boolean mShowHd = false;
   private int mWidth;
   private int mHeight;
 
@@ -94,6 +94,15 @@
     return mShowVideo;
   }
 
+  public void setShowHd(boolean showHd) {
+    mShowHd = showHd;
+    if (showHd) {
+      mWidth += sResources.hdCall.getIntrinsicWidth();
+      mHeight = Math.max(mHeight, sResources.hdCall.getIntrinsicHeight());
+      invalidate();
+    }
+  }
+
   public int getCount() {
     return mCallTypes.size();
   }
@@ -147,6 +156,13 @@
       drawable.setBounds(left, 0, right, sResources.videoCall.getIntrinsicHeight());
       drawable.draw(canvas);
     }
+    // If showing HD call icon, draw it scaled appropriately.
+    if (mShowHd) {
+      final Drawable drawable = sResources.hdCall;
+      final int right = left + sResources.hdCall.getIntrinsicWidth();
+      drawable.setBounds(left, 0, right, sResources.hdCall.getIntrinsicHeight());
+      drawable.draw(canvas);
+    }
   }
 
   private static class Resources {
@@ -166,9 +182,12 @@
     // Drawable representing a blocked call.
     public final Drawable blocked;
 
-    //  Drawable repesenting a video call.
+    // Drawable repesenting a video call.
     public final Drawable videoCall;
 
+    // Drawable represeting a hd call.
+    public final Drawable hdCall;
+
     /** The margin to use for icons. */
     public final int iconMargin;
 
@@ -204,6 +223,10 @@
       videoCall.setColorFilter(
           r.getColor(R.color.dialer_secondary_text_color), PorterDuff.Mode.MULTIPLY);
 
+      hdCall = getScaledBitmap(context, R.drawable.quantum_ic_hd_white_24);
+      hdCall.setColorFilter(
+          r.getColor(R.color.dialer_secondary_text_color), PorterDuff.Mode.MULTIPLY);
+
       iconMargin = r.getDimensionPixelSize(R.dimen.call_log_icon_margin);
     }
 
diff --git a/java/com/android/dialer/app/calllog/PhoneAccountUtils.java b/java/com/android/dialer/calllogutils/PhoneAccountUtils.java
similarity index 98%
rename from java/com/android/dialer/app/calllog/PhoneAccountUtils.java
rename to java/com/android/dialer/calllogutils/PhoneAccountUtils.java
index c6d94d3..c639893 100644
--- a/java/com/android/dialer/app/calllog/PhoneAccountUtils.java
+++ b/java/com/android/dialer/calllogutils/PhoneAccountUtils.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.dialer.app.calllog;
+package com.android.dialer.calllogutils;
 
 import android.content.ComponentName;
 import android.content.Context;
diff --git a/java/com/android/dialer/app/PhoneCallDetails.java b/java/com/android/dialer/calllogutils/PhoneCallDetails.java
similarity index 98%
rename from java/com/android/dialer/app/PhoneCallDetails.java
rename to java/com/android/dialer/calllogutils/PhoneCallDetails.java
index 436f68e..ba05a87 100644
--- a/java/com/android/dialer/app/PhoneCallDetails.java
+++ b/java/com/android/dialer/calllogutils/PhoneCallDetails.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.dialer.app;
+package com.android.dialer.calllogutils;
 
 import android.content.Context;
 import android.content.res.Resources;
@@ -27,7 +27,6 @@
 import com.android.contacts.common.ContactsUtils.UserType;
 import com.android.contacts.common.preference.ContactsPreferences;
 import com.android.contacts.common.util.ContactDisplayUtils;
-import com.android.dialer.app.calllog.PhoneNumberDisplayUtil;
 import com.android.dialer.phonenumbercache.ContactInfo;
 
 /** The details of a phone call to be shown in the UI. */
diff --git a/java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java b/java/com/android/dialer/calllogutils/PhoneNumberDisplayUtil.java
similarity index 95%
rename from java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java
rename to java/com/android/dialer/calllogutils/PhoneNumberDisplayUtil.java
index 410d4cc..9bebfac 100644
--- a/java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java
+++ b/java/com/android/dialer/calllogutils/PhoneNumberDisplayUtil.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.dialer.app.calllog;
+package com.android.dialer.calllogutils;
 
 import android.content.Context;
 import android.provider.CallLog.Calls;
@@ -22,15 +22,13 @@
 import android.text.TextDirectionHeuristics;
 import android.text.TextUtils;
 import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
-import com.android.dialer.app.R;
 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
 
 /** Helper for formatting and managing the display of phone numbers. */
 public class PhoneNumberDisplayUtil {
 
   /** Returns the string to display for the given phone number if there is no matching contact. */
-  /* package */
-  static CharSequence getDisplayName(
+  public static CharSequence getDisplayName(
       Context context, CharSequence number, int presentation, boolean isVoicemail) {
     if (presentation == Calls.PRESENTATION_UNKNOWN) {
       return context.getResources().getString(R.string.unknown);
@@ -42,7 +40,7 @@
       return context.getResources().getString(R.string.payphone);
     }
     if (isVoicemail) {
-      return context.getResources().getString(R.string.voicemail);
+      return context.getResources().getString(R.string.voicemail_string);
     }
     if (PhoneNumberHelper.isLegacyUnknownNumbers(number)) {
       return context.getResources().getString(R.string.unknown);
diff --git a/java/com/android/dialer/calllogutils/res/values/colors.xml b/java/com/android/dialer/calllogutils/res/values/colors.xml
new file mode 100644
index 0000000..dc4ec24
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/res/values/colors.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~      http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+  <!-- Color for missed call icons. -->
+  <color name="missed_call">#ff2e58</color>
+  <!-- Color for answered or outgoing call icons. -->
+  <color name="answered_call">#00c853</color>
+  <!-- Color for blocked call icons. -->
+  <color name="blocked_call">@color/dialer_secondary_text_color</color>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/calllogutils/res/values/dimens.xml b/java/com/android/dialer/calllogutils/res/values/dimens.xml
new file mode 100644
index 0000000..0935ac1
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/res/values/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~      http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+  <dimen name="call_type_icon_size">12dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/calllogutils/res/values/strings.xml b/java/com/android/dialer/calllogutils/res/values/strings.xml
new file mode 100644
index 0000000..6a6f101
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/res/values/strings.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~      http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+  <!-- Title for incoming call type. [CHAR LIMIT=40] -->
+  <string name="type_incoming">Incoming call</string>
+
+  <!-- Title for incoming call which was transferred to another device. [CHAR LIMIT=60] -->
+  <string name="type_incoming_pulled">Incoming call transferred to another device</string>
+
+  <!-- Title for outgoing call type. [CHAR LIMIT=40] -->
+  <string name="type_outgoing">Outgoing call</string>
+
+  <!-- Title for outgoing call which was transferred to another device. [CHAR LIMIT=60] -->
+  <string name="type_outgoing_pulled">Outgoing call transferred to another device</string>
+
+  <!-- Title for missed call type. [CHAR LIMIT=40] -->
+  <string name="type_missed">Missed call</string>
+
+  <!-- Title for incoming video call in call details screen [CHAR LIMIT=60] -->
+  <string name="type_incoming_video">Incoming video call</string>
+
+  <!-- Title for incoming video call in call details screen which was transferred to another device.
+       [CHAR LIMIT=60] -->
+  <string name="type_incoming_video_pulled">Incoming video call transferred to another device</string>
+
+  <!-- Title for outgoing video call in call details screen [CHAR LIMIT=60] -->
+  <string name="type_outgoing_video">Outgoing video call</string>
+
+  <!-- Title for outgoing video call in call details screen which was transferred to another device.
+       [CHAR LIMIT=60] -->
+  <string name="type_outgoing_video_pulled">Outgoing video call transferred to another device</string>
+
+  <!-- Title for missed video call in call details screen [CHAR LIMIT=60] -->
+  <string name="type_missed_video">Missed video call</string>
+
+  <!-- Title for voicemail details screen -->
+  <string name="type_voicemail">Voicemail</string>
+
+  <!-- Title for rejected call type. [CHAR LIMIT=40] -->
+  <string name="type_rejected">Declined call</string>
+
+  <!-- Title for blocked call type. [CHAR LIMIT=40] -->
+  <string name="type_blocked">Blocked call</string>
+
+  <!-- Title for "answered elsewhere" call type. This will happen if a call was ringing
+       simultaneously on multiple devices, and the user answered it on a device other than the
+       current device. [CHAR LIMIT=60] -->
+  <string name="type_answered_elsewhere">Call answered on another device</string>
+
+  <!-- String describing the phone account the call was made on or to. This string will be used
+         in description_incoming_missed_call, description_incoming_answered_call, and
+         description_outgoing_call.
+         Note: AccessibilityServices uses this attribute to announce what the view represents.
+         [CHAR LIMIT=NONE] -->
+  <string name="description_phone_account">on <xliff:g example="SIM 1" id="phoneAccount">^1</xliff:g></string>
+
+  <!-- String describing the secondary line number the call was received via.
+       Note: AccessibilityServices use this attribute to announce what the view represents.
+       [CHAR LIMIT=NONE]-->
+  <string name="description_via_number">via <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g></string>
+
+  <!-- String describing the PhoneAccount and via number that a call was received on, if both are
+       visible.
+       Note: AccessibilityServices use this attribute to announce what the view represents.
+       [CHAR LIMIT=NONE]-->
+  <string name="description_via_number_phone_account">on <xliff:g example="SIM 1" id="phoneAccount">%1$s</xliff:g>, via <xliff:g example="(555) 555-5555" id="number">%2$s</xliff:g></string>
+
+  <!-- String used for displaying calls to the voicemail number in the call log -->
+  <string name="voicemail_string">Voicemail</string>
+
+  <!-- A nicely formatted call duration displayed when viewing call details. For example "42 min 28 sec" -->
+  <string name="call_details_duration_format"><xliff:g example="42" id="minutes">%s</xliff:g> min <xliff:g example="28" id="seconds">%s</xliff:g> sec</string>
+
+  <!-- A nicely formatted call duration displayed when viewing call details for duration less than 1 minute. For example "28 sec" -->
+  <string name="call_details_short_duration_format"><xliff:g example="28" id="seconds">%s</xliff:g> sec</string>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/common/Assert.java b/java/com/android/dialer/common/Assert.java
index 00b4f25..943e1dd 100644
--- a/java/com/android/dialer/common/Assert.java
+++ b/java/com/android/dialer/common/Assert.java
@@ -19,6 +19,7 @@
 import android.os.Looper;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import javax.annotation.CheckReturnValue;
 
 /** Assertions which will result in program termination unless disabled by flags. */
 public class Assert {
@@ -33,7 +34,9 @@
    * Called when a truly exceptional case occurs.
    *
    * @throws AssertionError
+   * @deprecated Use throw Assert.create*FailException() instead.
    */
+  @Deprecated
   public static void fail() {
     throw new AssertionError("Fail");
   }
@@ -43,11 +46,38 @@
    *
    * @param reason the optional reason to supply as the exception message
    * @throws AssertionError
+   * @deprecated Use throw Assert.create*FailException() instead.
    */
+  @Deprecated
   public static void fail(String reason) {
     throw new AssertionError(reason);
   }
 
+  @CheckReturnValue
+  public static AssertionError createAssertionFailException(String msg) {
+    return new AssertionError(msg);
+  }
+
+  @CheckReturnValue
+  public static UnsupportedOperationException createUnsupportedOperationFailException() {
+    return new UnsupportedOperationException();
+  }
+
+  @CheckReturnValue
+  public static UnsupportedOperationException createUnsupportedOperationFailException(String msg) {
+    return new UnsupportedOperationException(msg);
+  }
+
+  @CheckReturnValue
+  public static IllegalStateException createIllegalStateFailException() {
+    return new IllegalStateException();
+  }
+
+  @CheckReturnValue
+  public static IllegalStateException createIllegalStateFailException(String msg) {
+    return new IllegalStateException(msg);
+  }
+
   /**
    * Ensures the truth of an expression involving one or more parameters to the calling method.
    *
diff --git a/java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java b/java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java
deleted file mode 100644
index f9d7cea..0000000
--- a/java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.common;
-
-import android.support.annotation.Nullable;
-import javax.annotation.Generated;
-
-
- final class AutoValue_FallibleAsyncTask_FallibleTaskResult<ResultT> extends FallibleAsyncTask.FallibleTaskResult<ResultT> {
-
-  private final Throwable throwable;
-  private final ResultT result;
-
-  AutoValue_FallibleAsyncTask_FallibleTaskResult(
-      @Nullable Throwable throwable,
-      @Nullable ResultT result) {
-    this.throwable = throwable;
-    this.result = result;
-  }
-
-  @Nullable
-  @Override
-  public Throwable getThrowable() {
-    return throwable;
-  }
-
-  @Nullable
-  @Override
-  public ResultT getResult() {
-    return result;
-  }
-
-  @Override
-  public String toString() {
-    return "FallibleTaskResult{"
-        + "throwable=" + throwable + ", "
-        + "result=" + result
-        + "}";
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o == this) {
-      return true;
-    }
-    if (o instanceof FallibleAsyncTask.FallibleTaskResult) {
-      FallibleAsyncTask.FallibleTaskResult<?> that = (FallibleAsyncTask.FallibleTaskResult<?>) o;
-      return ((this.throwable == null) ? (that.getThrowable() == null) : this.throwable.equals(that.getThrowable()))
-           && ((this.result == null) ? (that.getResult() == null) : this.result.equals(that.getResult()));
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    int h = 1;
-    h *= 1000003;
-    h ^= (throwable == null) ? 0 : this.throwable.hashCode();
-    h *= 1000003;
-    h ^= (result == null) ? 0 : this.result.hashCode();
-    return h;
-  }
-
-}
-
diff --git a/java/com/android/dialer/common/FallibleAsyncTask.java b/java/com/android/dialer/common/FallibleAsyncTask.java
index fbdbda7..f3abace 100644
--- a/java/com/android/dialer/common/FallibleAsyncTask.java
+++ b/java/com/android/dialer/common/FallibleAsyncTask.java
@@ -20,7 +20,7 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import com.android.dialer.common.FallibleAsyncTask.FallibleTaskResult;
-
+import com.google.auto.value.AutoValue;
 
 /**
  * A task that runs work in the background, passing Throwables from {@link
@@ -52,8 +52,8 @@
    *
    * @param <ResultT> the type of the result of the background computation
    */
-
-  protected abstract static class FallibleTaskResult<ResultT> {
+  @AutoValue
+  public abstract static class FallibleTaskResult<ResultT> {
 
     /** Creates an instance of FallibleTaskResult for the given throwable. */
     private static <ResultT> FallibleTaskResult<ResultT> createFailureResult(@NonNull Throwable t) {
diff --git a/java/com/android/dialer/common/PerAccountSharedPreferences.java b/java/com/android/dialer/common/PerAccountSharedPreferences.java
new file mode 100644
index 0000000..0ed1b03
--- /dev/null
+++ b/java/com/android/dialer/common/PerAccountSharedPreferences.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.dialer.common;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import java.util.Set;
+
+/**
+ * Class that helps us store dialer preferences that are phone account dependent. This is necessary
+ * for cases such as settings that are phone account dependent e.g endless vm. The logic is to
+ * essentially store the shared preference by appending the phone account id to the key.
+ */
+public class PerAccountSharedPreferences {
+  private final String sharedPrefsKeyPrefix;
+  private final SharedPreferences preferences;
+  private final PhoneAccountHandle phoneAccountHandle;
+
+  public PerAccountSharedPreferences(
+      Context context, PhoneAccountHandle handle, SharedPreferences prefs) {
+    preferences = prefs;
+    phoneAccountHandle = handle;
+    sharedPrefsKeyPrefix = "phone_account_dependent_";
+  }
+
+  /**
+   * Not to be used, currently only used by {@VisualVoicemailPreferences} for legacy reasons.
+   */
+  protected PerAccountSharedPreferences(
+      Context context, PhoneAccountHandle handle, SharedPreferences prefs, String prefix) {
+    Assert.checkArgument(prefix.equals("visual_voicemail_"));
+    preferences = prefs;
+    phoneAccountHandle = handle;
+    sharedPrefsKeyPrefix = prefix;
+  }
+
+  public class Editor {
+
+    private final SharedPreferences.Editor editor;
+
+    private Editor() {
+      editor = preferences.edit();
+    }
+
+    public void apply() {
+      editor.apply();
+    }
+
+    public Editor putBoolean(String key, boolean value) {
+      editor.putBoolean(getKey(key), value);
+      return this;
+    }
+
+    public Editor putFloat(String key, float value) {
+      editor.putFloat(getKey(key), value);
+      return this;
+    }
+
+    public Editor putInt(String key, int value) {
+      editor.putInt(getKey(key), value);
+      return this;
+    }
+
+    public Editor putLong(String key, long value) {
+      editor.putLong(getKey(key), value);
+      return this;
+    }
+
+    public Editor putString(String key, String value) {
+      editor.putString(getKey(key), value);
+      return this;
+    }
+
+    public Editor putStringSet(String key, Set<String> value) {
+      editor.putStringSet(getKey(key), value);
+      return this;
+    }
+  }
+
+  public Editor edit() {
+    return new Editor();
+  }
+
+  public boolean getBoolean(String key, boolean defValue) {
+    return getValue(key, defValue);
+  }
+
+  public float getFloat(String key, float defValue) {
+    return getValue(key, defValue);
+  }
+
+  public int getInt(String key, int defValue) {
+    return getValue(key, defValue);
+  }
+
+  public long getLong(String key, long defValue) {
+    return getValue(key, defValue);
+  }
+
+  public String getString(String key, String defValue) {
+    return getValue(key, defValue);
+  }
+
+  @Nullable
+  public String getString(String key) {
+    return getValue(key, null);
+  }
+
+  public Set<String> getStringSet(String key, Set<String> defValue) {
+    return getValue(key, defValue);
+  }
+
+  public boolean contains(String key) {
+    return preferences.contains(getKey(key));
+  }
+
+  private <T> T getValue(String key, T defValue) {
+    if (!contains(key)) {
+      return defValue;
+    }
+    Object object = preferences.getAll().get(getKey(key));
+    if (object == null) {
+      return defValue;
+    }
+    return (T) object;
+  }
+
+  private String getKey(String key) {
+    return sharedPrefsKeyPrefix + key + "_" + phoneAccountHandle.getId();
+  }
+}
diff --git a/java/com/android/dialer/common/proguard.flags b/java/com/android/dialer/common/proguard.flags
new file mode 100644
index 0000000..4b6b846
--- /dev/null
+++ b/java/com/android/dialer/common/proguard.flags
@@ -0,0 +1,4 @@
+-assumenosideeffects class com.android.dialer.common.LogUtil {
+    public static void v(...);
+    public static void d(...);
+}
diff --git a/java/com/android/dialer/common/res/values/config.xml b/java/com/android/dialer/common/res/values/config.xml
new file mode 100644
index 0000000..c4df279
--- /dev/null
+++ b/java/com/android/dialer/common/res/values/config.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <bool name="spring_hd_codec">false</bool>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/constants/Constants.java b/java/com/android/dialer/constants/Constants.java
index 7777301..d92c0bc 100644
--- a/java/com/android/dialer/constants/Constants.java
+++ b/java/com/android/dialer/constants/Constants.java
@@ -19,7 +19,6 @@
 import android.support.annotation.NonNull;
 import com.android.dialer.common.Assert;
 import com.android.dialer.proguard.UsedByReflection;
-import com.android.dialer.constants.ConstantsImpl;
 
 /**
  * Utility to access constants that are different across build variants (Google Dialer, AOSP,
@@ -29,11 +28,22 @@
  */
 @UsedByReflection(value = "Constants.java")
 public abstract class Constants {
-  private static Constants instance = new ConstantsImpl();
+  private static Constants instance;
   private static boolean didInitializeInstance;
 
   @NonNull
   public static synchronized Constants get() {
+    if (!didInitializeInstance) {
+      didInitializeInstance = true;
+      try {
+        Class<?> clazz = Class.forName(Constants.class.getName() + "Impl");
+        instance = (Constants) clazz.getConstructor().newInstance();
+      } catch (ReflectiveOperationException e) {
+        Assert.fail(
+            "Unable to create an instance of ConstantsImpl. To fix this error include one of the "
+                + "constants modules (googledialer, aosp etc...) in your target.");
+      }
+    }
     return instance;
   }
 
diff --git a/java/com/android/dialer/database/CallLogQueryHandler.java b/java/com/android/dialer/database/CallLogQueryHandler.java
index ffca69f..1f6bd5f 100644
--- a/java/com/android/dialer/database/CallLogQueryHandler.java
+++ b/java/com/android/dialer/database/CallLogQueryHandler.java
@@ -33,6 +33,7 @@
 import android.provider.CallLog.Calls;
 import android.provider.VoicemailContract.Status;
 import android.provider.VoicemailContract.Voicemails;
+import android.support.v4.os.BuildCompat;
 import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.compat.AppCompatConstants;
@@ -40,6 +41,7 @@
 import com.android.dialer.phonenumbercache.CallLogQuery;
 import com.android.dialer.telecom.TelecomUtil;
 import com.android.dialer.util.PermissionsUtil;
+import com.android.voicemail.VoicemailComponent;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.List;
@@ -126,13 +128,23 @@
   public void fetchVoicemailUnreadCount() {
     if (TelecomUtil.hasReadWriteVoicemailPermissions(mContext)) {
       // Only count voicemails that have not been read and have not been deleted.
+      StringBuilder where =
+          new StringBuilder(Voicemails.IS_READ + "=0" + " AND " + Voicemails.DELETED + "=0 ");
+      List<String> selectionArgs = new ArrayList<>();
+
+      if (BuildCompat.isAtLeastO()) {
+        VoicemailComponent.get(mContext)
+            .getVoicemailClient()
+            .appendOmtpVoicemailSelectionClause(mContext, where, selectionArgs);
+      }
+
       startQuery(
           QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN,
           null,
           Voicemails.CONTENT_URI,
           new String[] {Voicemails._ID},
-          Voicemails.IS_READ + "=0" + " AND " + Voicemails.DELETED + "=0",
-          null,
+          where.toString(),
+          selectionArgs.toArray(new String[selectionArgs.size()]),
           null);
     }
   }
@@ -168,6 +180,12 @@
       selectionArgs.add(Long.toString(newerThan));
     }
 
+    if (callType == Calls.VOICEMAIL_TYPE) {
+      VoicemailComponent.get(mContext)
+          .getVoicemailClient()
+          .appendOmtpVoicemailSelectionClause(mContext, where, selectionArgs);
+    }
+
     final int limit = (mLogLimit == -1) ? NUM_LOGS_TO_DISPLAY : mLogLimit;
     final String selection = where.length() > 0 ? where.toString() : null;
     Uri uri =
diff --git a/java/com/android/voicemailomtp/NeededForTesting.java b/java/com/android/dialer/debug/bindings/stub/DebugBindings.java
similarity index 67%
copy from java/com/android/voicemailomtp/NeededForTesting.java
copy to java/com/android/dialer/debug/bindings/stub/DebugBindings.java
index 20517fe..7df3834 100644
--- a/java/com/android/voicemailomtp/NeededForTesting.java
+++ b/java/com/android/dialer/debug/bindings/stub/DebugBindings.java
@@ -14,12 +14,14 @@
  * limitations under the License
  */
 
-package com.android.voicemailomtp;
+package com.android.dialer.debug.bindings;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
+import android.content.Context;
 
-@Retention(RetentionPolicy.SOURCE)
-public @interface NeededForTesting {
+/** Hooks into the debug module. */
+public class DebugBindings {
 
+  public static void registerConnectionService(Context context) {}
+
+  public static void addNewIncomingCall(Context context, String phoneNumber) {}
 }
diff --git a/java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java b/java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java
deleted file mode 100644
index 14299f9..0000000
--- a/java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.enrichedcall;
-
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_EnrichedCallCapabilities extends EnrichedCallCapabilities {
-
-  private final boolean supportsCallComposer;
-  private final boolean supportsPostCall;
-
-  AutoValue_EnrichedCallCapabilities(
-      boolean supportsCallComposer,
-      boolean supportsPostCall) {
-    this.supportsCallComposer = supportsCallComposer;
-    this.supportsPostCall = supportsPostCall;
-  }
-
-  @Override
-  public boolean supportsCallComposer() {
-    return supportsCallComposer;
-  }
-
-  @Override
-  public boolean supportsPostCall() {
-    return supportsPostCall;
-  }
-
-  @Override
-  public String toString() {
-    return "EnrichedCallCapabilities{"
-        + "supportsCallComposer=" + supportsCallComposer + ", "
-        + "supportsPostCall=" + supportsPostCall + ", "
-        + "}";
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o == this) {
-      return true;
-    }
-    if (o instanceof EnrichedCallCapabilities) {
-      EnrichedCallCapabilities that = (EnrichedCallCapabilities) o;
-      return (this.supportsCallComposer == that.supportsCallComposer())
-           && (this.supportsPostCall == that.supportsPostCall());
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    int h = 1;
-    h *= 1000003;
-    h ^= this.supportsCallComposer ? 1231 : 1237;
-    h *= 1000003;
-    h ^= this.supportsPostCall ? 1231 : 1237;
-    return h;
-  }
-
-}
-
diff --git a/java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java b/java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java
deleted file mode 100644
index edfefc4..0000000
--- a/java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.enrichedcall;
-
-import android.net.Uri;
-import android.support.annotation.Nullable;
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_OutgoingCallComposerData extends OutgoingCallComposerData {
-
-  private final String subject;
-  private final Uri imageUri;
-  private final String imageContentType;
-
-  private AutoValue_OutgoingCallComposerData(
-      @Nullable String subject,
-      @Nullable Uri imageUri,
-      @Nullable String imageContentType) {
-    this.subject = subject;
-    this.imageUri = imageUri;
-    this.imageContentType = imageContentType;
-  }
-
-  @Nullable
-  @Override
-  public String getSubject() {
-    return subject;
-  }
-
-  @Nullable
-  @Override
-  public Uri getImageUri() {
-    return imageUri;
-  }
-
-  @Nullable
-  @Override
-  public String getImageContentType() {
-    return imageContentType;
-  }
-
-  @Override
-  public String toString() {
-    return "OutgoingCallComposerData{"
-        + "subject=" + subject + ", "
-        + "imageUri=" + imageUri + ", "
-        + "imageContentType=" + imageContentType
-        + "}";
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o == this) {
-      return true;
-    }
-    if (o instanceof OutgoingCallComposerData) {
-      OutgoingCallComposerData that = (OutgoingCallComposerData) o;
-      return ((this.subject == null) ? (that.getSubject() == null) : this.subject.equals(that.getSubject()))
-           && ((this.imageUri == null) ? (that.getImageUri() == null) : this.imageUri.equals(that.getImageUri()))
-           && ((this.imageContentType == null) ? (that.getImageContentType() == null) : this.imageContentType.equals(that.getImageContentType()));
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    int h = 1;
-    h *= 1000003;
-    h ^= (subject == null) ? 0 : this.subject.hashCode();
-    h *= 1000003;
-    h ^= (imageUri == null) ? 0 : this.imageUri.hashCode();
-    h *= 1000003;
-    h ^= (imageContentType == null) ? 0 : this.imageContentType.hashCode();
-    return h;
-  }
-
-  static final class Builder extends OutgoingCallComposerData.Builder {
-    private String subject;
-    private Uri imageUri;
-    private String imageContentType;
-    Builder() {
-    }
-    private Builder(OutgoingCallComposerData source) {
-      this.subject = source.getSubject();
-      this.imageUri = source.getImageUri();
-      this.imageContentType = source.getImageContentType();
-    }
-    @Override
-    public OutgoingCallComposerData.Builder setSubject(@Nullable String subject) {
-      this.subject = subject;
-      return this;
-    }
-    @Override
-    OutgoingCallComposerData.Builder setImageUri(@Nullable Uri imageUri) {
-      this.imageUri = imageUri;
-      return this;
-    }
-    @Override
-    OutgoingCallComposerData.Builder setImageContentType(@Nullable String imageContentType) {
-      this.imageContentType = imageContentType;
-      return this;
-    }
-    @Override
-    OutgoingCallComposerData autoBuild() {
-      return new AutoValue_OutgoingCallComposerData(
-          this.subject,
-          this.imageUri,
-          this.imageContentType);
-    }
-  }
-
-}
diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java b/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java
index b7d7809..c3c78c9 100644
--- a/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java
+++ b/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java
@@ -16,21 +16,24 @@
 
 package com.android.dialer.enrichedcall;
 
-
+import com.google.auto.value.AutoValue;
 
 /** Value type holding enriched call capabilities. */
-
+@AutoValue
 public abstract class EnrichedCallCapabilities {
 
   public static final EnrichedCallCapabilities NO_CAPABILITIES =
-      EnrichedCallCapabilities.create(false, false);
+      EnrichedCallCapabilities.create(false, false, false);
 
   public static EnrichedCallCapabilities create(
-      boolean supportsCallComposer, boolean supportsPostCall) {
-    return new AutoValue_EnrichedCallCapabilities(supportsCallComposer, supportsPostCall);
+      boolean supportsCallComposer, boolean supportsPostCall, boolean supportsVideoCall) {
+    return new AutoValue_EnrichedCallCapabilities(
+        supportsCallComposer, supportsPostCall, supportsVideoCall);
   }
 
   public abstract boolean supportsCallComposer();
 
   public abstract boolean supportsPostCall();
+
+  public abstract boolean supportsVideoShare();
 }
diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallComponent.java b/java/com/android/dialer/enrichedcall/EnrichedCallComponent.java
new file mode 100644
index 0000000..5291e29
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/EnrichedCallComponent.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.enrichedcall;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import dagger.Subcomponent;
+import com.android.dialer.enrichedcall.stub.EnrichedCallManagerStub;
+
+/** Subcomponent that can be used to access the enriched call implementation. */
+public class EnrichedCallComponent {
+  private static EnrichedCallComponent instance;
+  private EnrichedCallManager enrichedCallManager;
+
+  @NonNull
+  public EnrichedCallManager getEnrichedCallManager() {
+    if (enrichedCallManager == null) {
+        enrichedCallManager = new EnrichedCallManagerStub();
+    }
+    return enrichedCallManager;
+  }
+
+  public static EnrichedCallComponent get(Context context) {
+    if (instance == null) {
+        instance = new EnrichedCallComponent();
+    }
+    return instance;
+  }
+
+  /** Used to refer to the root application component. */
+  public interface HasComponent {
+    EnrichedCallComponent enrichedCallComponent();
+  }
+}
diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallManager.java b/java/com/android/dialer/enrichedcall/EnrichedCallManager.java
index 6af8c40..a36b2cc 100644
--- a/java/com/android/dialer/enrichedcall/EnrichedCallManager.java
+++ b/java/com/android/dialer/enrichedcall/EnrichedCallManager.java
@@ -16,38 +16,25 @@
 
 package com.android.dialer.enrichedcall;
 
-import android.app.Application;
 import android.support.annotation.IntDef;
 import android.support.annotation.MainThread;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import com.android.dialer.common.Assert;
+import android.support.annotation.WorkerThread;
+import com.android.dialer.calldetails.nano.CallDetailsEntries;
+import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry;
+import com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult;
+import com.android.dialer.enrichedcall.videoshare.VideoShareListener;
 import com.android.dialer.multimedia.MultimediaData;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Map;
 
 /** Performs all enriched calling logic. */
 public interface EnrichedCallManager {
 
-  /** Factory for {@link EnrichedCallManager}. */
-  interface Factory {
-    EnrichedCallManager getEnrichedCallManager();
-  }
-
-  /** Accessor for {@link EnrichedCallManager}. */
-  class Accessor {
-
-    /**
-     * @throws IllegalArgumentException if application does not implement {@link
-     *     EnrichedCallManager.Factory}
-     */
-    @NonNull
-    public static EnrichedCallManager getInstance(@NonNull Application application) {
-      Assert.isNotNull(application);
-
-      return ((EnrichedCallManager.Factory) application).getEnrichedCallManager();
-    }
-  }
+  int POST_CALL_NOTE_MAX_CHAR = 60;
 
   /** Receives updates when enriched call capabilities are ready. */
   interface CapabilitiesListener {
@@ -148,6 +135,15 @@
   void endCallComposerSession(long sessionId);
 
   /**
+   * Sends a post call note to the given number.
+   *
+   * @throws IllegalArgumentException if message is longer than {@link #POST_CALL_NOTE_MAX_CHAR}
+   *     characters
+   */
+  @MainThread
+  void sendPostCallNote(@NonNull String number, @NonNull String message);
+
+  /**
    * Called once the capabilities are available for a corresponding call to {@link
    * #requestCapabilities(String)}.
    *
@@ -162,8 +158,8 @@
   interface StateChangedListener {
 
     /**
-     * Callback fired when state changes. Listeners should call {@link #getSession(String)} to
-     * retrieve the new state.
+     * Callback fired when state changes. Listeners should call {@link #getSession(long)} or {@link
+     * #getSession(String, String)} to retrieve the new state.
      */
     void onEnrichedCallStateChanged();
   }
@@ -177,10 +173,10 @@
   @MainThread
   void registerStateChangedListener(@NonNull StateChangedListener listener);
 
-  /** Returns the {@link Session} for the given number, or {@code null} if no session exists. */
+  /** Returns the {@link Session} for the given unique call id, falling back to the number. */
   @MainThread
   @Nullable
-  Session getSession(@NonNull String number);
+  Session getSession(@NonNull String uniqueCallId, @NonNull String number);
 
   /** Returns the {@link Session} for the given sessionId, or {@code null} if no session exists. */
   @MainThread
@@ -188,6 +184,18 @@
   Session getSession(long sessionId);
 
   /**
+   * Returns a mapping of enriched call data for all of the given {@link CallDetailsEntries}.
+   *
+   * <p>The mapping is created by finding the HistoryResults whose timestamps occurred during or
+   * close after a CallDetailsEntry. A CallDetailsEntry can have multiple HistoryResults in the
+   * event that both a CallComposer message and PostCall message were sent for the same call.
+   */
+  @WorkerThread
+  @NonNull
+  Map<CallDetailsEntry, List<HistoryResult>> getAllHistoricalData(
+      @NonNull String number, @NonNull CallDetailsEntries entries);
+
+  /**
    * Unregisters the given {@link StateChangedListener}.
    *
    * <p>As a result of this method, the listener will not receive updates when the state of enriched
@@ -222,4 +230,77 @@
    */
   @MainThread
   void onIncomingCallComposerData(long sessionId, @NonNull MultimediaData multimediaData);
+
+  /**
+   * Called when post call data arrives for the given session.
+   *
+   * @throws IllegalStateException if there's no session for the given id
+   */
+  @MainThread
+  void onIncomingPostCallData(long sessionId, @NonNull MultimediaData multimediaData);
+
+  /**
+   * Registers the given {@link VideoShareListener}.
+   *
+   * <p>As a result of this method, the listener will receive updates when any video share state
+   * changes.
+   */
+  @MainThread
+  void registerVideoShareListener(@NonNull VideoShareListener listener);
+
+  /**
+   * Unregisters the given {@link VideoShareListener}.
+   *
+   * <p>As a result of this method, the listener will not receive updates when any video share state
+   * changes.
+   */
+  @MainThread
+  void unregisterVideoShareListener(@NonNull VideoShareListener listener);
+
+  /** Called when an incoming video share invite is received. */
+  @MainThread
+  void onIncomingVideoShareInvite(long sessionId, @NonNull String number);
+
+  /**
+   * Starts a video share session with the given remote number.
+   *
+   * @param number the remote number in any format
+   * @return the id for the started session, or {@link Session#NO_SESSION_ID} if the session fails
+   */
+  @MainThread
+  long startVideoShareSession(@NonNull String number);
+
+  /**
+   * Accepts a video share session invite.
+   *
+   * @param sessionId the session to accept
+   * @return whether or not accepting the session succeeded
+   */
+  @MainThread
+  boolean acceptVideoShareSession(long sessionId);
+
+  /**
+   * Retrieve the session id for an incoming video share invite.
+   *
+   * @param number the remote number in any format
+   * @return the id for the session invite, or {@link Session#NO_SESSION_ID} if there is no invite
+   */
+  @MainThread
+  long getVideoShareInviteSessionId(@NonNull String number);
+
+  /**
+   * Ends the given video share session.
+   *
+   * @param sessionId the id of the session to end
+   */
+  @MainThread
+  void endVideoShareSession(long sessionId);
+
+  /**
+   * Returns the {@link VideoShareSession} for the given sessionId, or {@code null} if no session
+   * exists.
+   */
+  @MainThread
+  @Nullable
+  VideoShareSession getVideoShareSession(long sessionId);
 }
diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java b/java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java
deleted file mode 100644
index db9a799..0000000
--- a/java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.enrichedcall;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import com.android.dialer.multimedia.MultimediaData;
-
-/** Stub implementation of {@link EnrichedCallManager}. */
-public final class EnrichedCallManagerStub implements EnrichedCallManager {
-
-  @Override
-  public void registerCapabilitiesListener(@NonNull CapabilitiesListener listener) {}
-
-  @Override
-  public void requestCapabilities(@NonNull String number) {}
-
-  @Override
-  public void unregisterCapabilitiesListener(@NonNull CapabilitiesListener listener) {}
-
-  @Override
-  public EnrichedCallCapabilities getCapabilities(@NonNull String number) {
-    return null;
-  }
-
-  @Override
-  public void clearCachedData() {}
-
-  @Override
-  public long startCallComposerSession(@NonNull String number) {
-    return Session.NO_SESSION_ID;
-  }
-
-  @Override
-  public void sendCallComposerData(long sessionId, @NonNull MultimediaData data) {}
-
-  @Override
-  public void endCallComposerSession(long sessionId) {}
-
-  @Override
-  public void onCapabilitiesReceived(
-      @NonNull String number, @NonNull EnrichedCallCapabilities capabilities) {}
-
-  @Override
-  public void registerStateChangedListener(@NonNull StateChangedListener listener) {}
-
-  @Nullable
-  @Override
-  public Session getSession(@NonNull String number) {
-    return null;
-  }
-
-  @Nullable
-  @Override
-  public Session getSession(long sessionId) {
-    return null;
-  }
-
-  @Override
-  public void unregisterStateChangedListener(@NonNull StateChangedListener listener) {}
-
-  @Override
-  public void onSessionStatusUpdate(long sessionId, @NonNull String number, int state) {}
-
-  @Override
-  public void onMessageUpdate(long sessionId, @NonNull String messageId, int state) {}
-
-  @Override
-  public void onIncomingCallComposerData(long sessionId, @NonNull MultimediaData multimediaData) {}
-}
diff --git a/java/com/android/dialer/enrichedcall/FuzzyPhoneNumberMatcher.java b/java/com/android/dialer/enrichedcall/FuzzyPhoneNumberMatcher.java
new file mode 100644
index 0000000..f589f94
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/FuzzyPhoneNumberMatcher.java
@@ -0,0 +1,20 @@
+package com.android.dialer.enrichedcall;
+
+import android.support.annotation.NonNull;
+import com.android.dialer.common.Assert;
+
+/** Utility for comparing phone numbers. */
+public class FuzzyPhoneNumberMatcher {
+
+  /** Returns {@code true} if the given numbers can be interpreted to be the same. */
+  public static boolean matches(@NonNull String a, @NonNull String b) {
+    String aNormalized = Assert.isNotNull(a).replaceAll("[^0-9]", "");
+    String bNormalized = Assert.isNotNull(b).replaceAll("[^0-9]", "");
+    if (aNormalized.length() < 7 || bNormalized.length() < 7) {
+      return false;
+    }
+    String aMatchable = aNormalized.substring(aNormalized.length() - 7);
+    String bMatchable = bNormalized.substring(bNormalized.length() - 7);
+    return aMatchable.equals(bMatchable);
+  }
+}
diff --git a/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java b/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java
index a8ee49d..56145dd 100644
--- a/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java
+++ b/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java
@@ -20,7 +20,7 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import com.android.dialer.common.Assert;
-
+import com.google.auto.value.AutoValue;
 
 /**
  * Value type holding references to all data that could be provided for the call composer.
@@ -29,19 +29,19 @@
  *
  * <pre>
  *   OutgoingCallComposerData.builder.build(); // throws exception, no data set
- *   OutgoingCallComposerData
- *       .setSubject(subject)
+ *   OutgoingCallComposerData.builder
+ *       .setText(subject)
  *       .build(); // Success
- *   OutgoingCallComposerData
+ *   OutgoingCallComposerData.builder
  *       .setImageData(uri, contentType)
  *       .build(); // Success
- *   OutgoingCallComposerData
- *      .setSubject(subject)
+ *   OutgoingCallComposerData.builder
+ *      .setText(subject)
  *      .setImageData(uri, contentType)
  *      .build(); // Success
  * </pre>
  */
-
+@AutoValue
 public abstract class OutgoingCallComposerData {
 
   public static Builder builder() {
@@ -62,7 +62,7 @@
   public abstract String getImageContentType();
 
   /** Builds instances of {@link OutgoingCallComposerData}. */
-
+  @AutoValue.Builder
   public abstract static class Builder {
     public abstract Builder setSubject(String subject);
 
diff --git a/java/com/android/dialer/enrichedcall/Session.java b/java/com/android/dialer/enrichedcall/Session.java
index b0439fa..b3f2914 100644
--- a/java/com/android/dialer/enrichedcall/Session.java
+++ b/java/com/android/dialer/enrichedcall/Session.java
@@ -17,6 +17,7 @@
 package com.android.dialer.enrichedcall;
 
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import com.android.dialer.enrichedcall.EnrichedCallManager.State;
 import com.android.dialer.multimedia.MultimediaData;
 
@@ -38,6 +39,12 @@
    */
   long getSessionId();
 
+  /** Returns the id of the dialer call associated with this session, or null if there isn't one. */
+  @Nullable
+  String getUniqueDialerCallId();
+
+  void setUniqueDialerCallId(@NonNull String id);
+
   /** Returns the number associated with the remote end of this session. */
   @NonNull
   String getRemoteNumber();
diff --git a/java/com/android/voicemailomtp/NeededForTesting.java b/java/com/android/dialer/enrichedcall/VideoShareSession.java
similarity index 69%
copy from java/com/android/voicemailomtp/NeededForTesting.java
copy to java/com/android/dialer/enrichedcall/VideoShareSession.java
index 20517fe..07bc4ed 100644
--- a/java/com/android/voicemailomtp/NeededForTesting.java
+++ b/java/com/android/dialer/enrichedcall/VideoShareSession.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2017 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,12 +14,7 @@
  * limitations under the License
  */
 
-package com.android.voicemailomtp;
+package com.android.dialer.enrichedcall;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-@Retention(RetentionPolicy.SOURCE)
-public @interface NeededForTesting {
-
-}
+/** Holds state information and data about video share sessions. */
+public interface VideoShareSession {}
diff --git a/java/com/android/dialer/enrichedcall/historyquery/HistoryQuery.java b/java/com/android/dialer/enrichedcall/historyquery/HistoryQuery.java
new file mode 100644
index 0000000..b7593ce
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/historyquery/HistoryQuery.java
@@ -0,0 +1,31 @@
+package com.android.dialer.enrichedcall.historyquery;
+
+import android.support.annotation.NonNull;
+import com.android.dialer.common.LogUtil;
+import com.google.auto.value.AutoValue;
+
+/**
+ * Data object representing the pieces of information required to query for historical enriched call
+ * data.
+ */
+@AutoValue
+public abstract class HistoryQuery {
+
+  @NonNull
+  public static HistoryQuery create(@NonNull String number, long callStartTime, long callEndTime) {
+    return new AutoValue_HistoryQuery(number, callStartTime, callEndTime);
+  }
+
+  public abstract String getNumber();
+
+  public abstract long getCallStartTimestamp();
+
+  public abstract long getCallEndTimestamp();
+
+  @Override
+  public String toString() {
+    return String.format(
+        "HistoryQuery{number: %s, callStartTimestamp: %d, callEndTimestamp: %d}",
+        LogUtil.sanitizePhoneNumber(getNumber()), getCallStartTimestamp(), getCallEndTimestamp());
+  }
+}
diff --git a/java/com/android/dialer/enrichedcall/historyquery/nano/HistoryResult.java b/java/com/android/dialer/enrichedcall/historyquery/nano/HistoryResult.java
new file mode 100644
index 0000000..2fdc2da
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/historyquery/nano/HistoryResult.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Generated by the protocol buffer compiler.  DO NOT EDIT!
+
+package com.android.dialer.enrichedcall.historyquery.proto.nano;
+
+/** This file is autogenerated, but javadoc required. */
+@SuppressWarnings("hiding")
+public final class HistoryResult
+    extends com.google.protobuf.nano.ExtendableMessageNano<HistoryResult> {
+
+  /** This file is autogenerated, but javadoc required. */
+  // enum Type
+  public interface Type {
+    public static final int INCOMING_CALL_COMPOSER = 1;
+    public static final int OUTGOING_CALL_COMPOSER = 2;
+    public static final int INCOMING_POST_CALL = 3;
+    public static final int OUTGOING_POST_CALL = 4;
+  }
+
+  private static volatile HistoryResult[] _emptyArray;
+
+  public static HistoryResult[] emptyArray() {
+    // Lazily initializes the empty array
+    if (_emptyArray == null) {
+      synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+        if (_emptyArray == null) {
+          _emptyArray = new HistoryResult[0];
+        }
+      }
+    }
+    return _emptyArray;
+  }
+
+  // optional .com.android.dialer.enrichedcall.historyquery.proto.HistoryResult.Type type = 1;
+  public int type;
+
+  // optional string text = 2;
+  public java.lang.String text;
+
+  // optional string image_uri = 4;
+  public java.lang.String imageUri;
+
+  // optional string image_content_type = 5;
+  public java.lang.String imageContentType;
+
+  // optional int64 timestamp = 7;
+  public long timestamp;
+
+  // @@protoc_insertion_point(class_scope:com.android.dialer.enrichedcall.historyquery.proto.HistoryResult)
+
+  public HistoryResult() {
+    clear();
+  }
+
+  public HistoryResult clear() {
+    type =
+        com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type
+            .INCOMING_CALL_COMPOSER;
+    text = "";
+    imageUri = "";
+    imageContentType = "";
+    timestamp = 0L;
+    unknownFieldData = null;
+    cachedSize = -1;
+    return this;
+  }
+
+  @Override
+  public void writeTo(com.google.protobuf.nano.CodedOutputByteBufferNano output)
+      throws java.io.IOException {
+    if (this.type
+        != com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type
+            .INCOMING_CALL_COMPOSER) {
+      output.writeInt32(1, this.type);
+    }
+    if (this.text != null && !this.text.equals("")) {
+      output.writeString(2, this.text);
+    }
+    if (this.imageUri != null && !this.imageUri.equals("")) {
+      output.writeString(4, this.imageUri);
+    }
+    if (this.imageContentType != null && !this.imageContentType.equals("")) {
+      output.writeString(5, this.imageContentType);
+    }
+    if (this.timestamp != 0L) {
+      output.writeInt64(7, this.timestamp);
+    }
+    super.writeTo(output);
+  }
+
+  @Override
+  protected int computeSerializedSize() {
+    int size = super.computeSerializedSize();
+    if (this.type
+        != com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type
+            .INCOMING_CALL_COMPOSER) {
+      size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt32Size(1, this.type);
+    }
+    if (this.text != null && !this.text.equals("")) {
+      size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(2, this.text);
+    }
+    if (this.imageUri != null && !this.imageUri.equals("")) {
+      size +=
+          com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(4, this.imageUri);
+    }
+    if (this.imageContentType != null && !this.imageContentType.equals("")) {
+      size +=
+          com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+              5, this.imageContentType);
+    }
+    if (this.timestamp != 0L) {
+      size +=
+          com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt64Size(7, this.timestamp);
+    }
+    return size;
+  }
+
+  @Override
+  public HistoryResult mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+      throws java.io.IOException {
+    while (true) {
+      int tag = input.readTag();
+      switch (tag) {
+        case 0:
+          return this;
+        default:
+          {
+            if (!super.storeUnknownField(input, tag)) {
+              return this;
+            }
+            break;
+          }
+        case 8:
+          {
+            int initialPos = input.getPosition();
+            int value = input.readInt32();
+            switch (value) {
+              case com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type
+                  .INCOMING_CALL_COMPOSER:
+              case com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type
+                  .OUTGOING_CALL_COMPOSER:
+              case com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type
+                  .INCOMING_POST_CALL:
+              case com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type
+                  .OUTGOING_POST_CALL:
+                this.type = value;
+                break;
+              default:
+                input.rewindToPosition(initialPos);
+                storeUnknownField(input, tag);
+                break;
+            }
+            break;
+          }
+        case 18:
+          {
+            this.text = input.readString();
+            break;
+          }
+        case 34:
+          {
+            this.imageUri = input.readString();
+            break;
+          }
+        case 42:
+          {
+            this.imageContentType = input.readString();
+            break;
+          }
+        case 56:
+          {
+            this.timestamp = input.readInt64();
+            break;
+          }
+      }
+    }
+  }
+
+  public static HistoryResult parseFrom(byte[] data)
+      throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+    return com.google.protobuf.nano.MessageNano.mergeFrom(new HistoryResult(), data);
+  }
+
+  public static HistoryResult parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+      throws java.io.IOException {
+    return new HistoryResult().mergeFrom(input);
+  }
+}
diff --git a/java/com/android/dialer/enrichedcall/stub/EnrichedCallManagerStub.java b/java/com/android/dialer/enrichedcall/stub/EnrichedCallManagerStub.java
new file mode 100644
index 0000000..01d1f2a
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/stub/EnrichedCallManagerStub.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.enrichedcall.stub;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.util.ArrayMap;
+import com.android.dialer.calldetails.nano.CallDetailsEntries;
+import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry;
+import com.android.dialer.common.Assert;
+import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.enrichedcall.Session;
+import com.android.dialer.enrichedcall.VideoShareSession;
+import com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult;
+import com.android.dialer.enrichedcall.videoshare.VideoShareListener;
+import com.android.dialer.multimedia.MultimediaData;
+import java.util.List;
+import java.util.Map;
+
+/** Stub implementation of {@link EnrichedCallManager}. */
+public final class EnrichedCallManagerStub implements EnrichedCallManager {
+
+  @Override
+  public void registerCapabilitiesListener(@NonNull CapabilitiesListener listener) {}
+
+  @Override
+  public void requestCapabilities(@NonNull String number) {}
+
+  @Override
+  public void unregisterCapabilitiesListener(@NonNull CapabilitiesListener listener) {}
+
+  @Override
+  public EnrichedCallCapabilities getCapabilities(@NonNull String number) {
+    return null;
+  }
+
+  @Override
+  public void clearCachedData() {}
+
+  @Override
+  public long startCallComposerSession(@NonNull String number) {
+    return Session.NO_SESSION_ID;
+  }
+
+  @Override
+  public void sendCallComposerData(long sessionId, @NonNull MultimediaData data) {}
+
+  @Override
+  public void endCallComposerSession(long sessionId) {}
+
+  @Override
+  public void sendPostCallNote(@NonNull String number, @NonNull String message) {}
+
+  @Override
+  public void onCapabilitiesReceived(
+      @NonNull String number, @NonNull EnrichedCallCapabilities capabilities) {}
+
+  @Override
+  public void registerStateChangedListener(@NonNull StateChangedListener listener) {}
+
+  @Nullable
+  @Override
+  public Session getSession(@NonNull String uniqueCallId, @NonNull String number) {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public Session getSession(long sessionId) {
+    return null;
+  }
+
+  @NonNull
+  @Override
+  @WorkerThread
+  public Map<CallDetailsEntry, List<HistoryResult>> getAllHistoricalData(
+      @NonNull String number, @NonNull CallDetailsEntries entries) {
+    Assert.isWorkerThread();
+    return new ArrayMap<>();
+  }
+
+  @Override
+  public void unregisterStateChangedListener(@NonNull StateChangedListener listener) {}
+
+  @Override
+  public void onSessionStatusUpdate(long sessionId, @NonNull String number, int state) {}
+
+  @Override
+  public void onMessageUpdate(long sessionId, @NonNull String messageId, int state) {}
+
+  @Override
+  public void onIncomingCallComposerData(long sessionId, @NonNull MultimediaData multimediaData) {}
+
+  @Override
+  public void onIncomingPostCallData(long sessionId, @NonNull MultimediaData multimediaData) {}
+
+  @Override
+  public void registerVideoShareListener(@NonNull VideoShareListener listener) {}
+
+  @Override
+  public void unregisterVideoShareListener(@NonNull VideoShareListener listener) {}
+
+  @Override
+  public void onIncomingVideoShareInvite(long sessionId, @NonNull String number) {}
+
+  @Override
+  public long startVideoShareSession(String number) {
+    return Session.NO_SESSION_ID;
+  }
+
+  @Override
+  public boolean acceptVideoShareSession(long sessionId) {
+    return false;
+  }
+
+  @Override
+  public long getVideoShareInviteSessionId(@NonNull String number) {
+    return Session.NO_SESSION_ID;
+  }
+
+  @Override
+  public void endVideoShareSession(long sessionId) {}
+
+  @Nullable
+  @Override
+  public VideoShareSession getVideoShareSession(long sessionId) {
+    return null;
+  }
+}
diff --git a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java b/java/com/android/dialer/enrichedcall/stub/StubEnrichedCallModule.java
similarity index 86%
rename from java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
rename to java/com/android/dialer/enrichedcall/stub/StubEnrichedCallModule.java
index 39c55d0..0ec7211 100644
--- a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
+++ b/java/com/android/dialer/enrichedcall/stub/StubEnrichedCallModule.java
@@ -14,8 +14,9 @@
  * limitations under the License
  */
 
-package com.android.dialer.enrichedcall;
+package com.android.dialer.enrichedcall.stub;
 
+import com.android.dialer.enrichedcall.EnrichedCallManager;
 import dagger.Module;
 import dagger.Provides;
 import javax.inject.Singleton;
@@ -29,4 +30,6 @@
   static EnrichedCallManager provideEnrichedCallManager() {
     return new EnrichedCallManagerStub();
   }
+
+  private StubEnrichedCallModule() {}
 }
diff --git a/java/com/android/dialer/enrichedcall/videoshare/VideoShareListener.java b/java/com/android/dialer/enrichedcall/videoshare/VideoShareListener.java
new file mode 100644
index 0000000..bcc387a
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/videoshare/VideoShareListener.java
@@ -0,0 +1,14 @@
+package com.android.dialer.enrichedcall.videoshare;
+
+import android.support.annotation.MainThread;
+
+/** Receives updates when video share status has changed. */
+public interface VideoShareListener {
+
+  /**
+   * Callback fired when video share has changed (service connected / disconnected, video share
+   * invite received or canceled, or when a session changes).
+   */
+  @MainThread
+  void onVideoShareChanged();
+}
diff --git a/java/com/android/dialer/inject/ApplicationModule.java b/java/com/android/dialer/inject/ContextModule.java
similarity index 65%
rename from java/com/android/dialer/inject/ApplicationModule.java
rename to java/com/android/dialer/inject/ContextModule.java
index 99e5296..aa83f01 100644
--- a/java/com/android/dialer/inject/ApplicationModule.java
+++ b/java/com/android/dialer/inject/ContextModule.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2017 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,24 +16,24 @@
 
 package com.android.dialer.inject;
 
-import android.app.Application;
+import android.content.Context;
 import android.support.annotation.NonNull;
 import com.android.dialer.common.Assert;
 import dagger.Module;
 import dagger.Provides;
 
-/** Provides the singleton application object. */
+/** Provides the singleton context object. */
 @Module
-public final class ApplicationModule {
+public final class ContextModule {
 
-  @NonNull private final Application application;
+  @NonNull private final Context context;
 
-  public ApplicationModule(@NonNull Application application) {
-    this.application = Assert.isNotNull(application);
+  public ContextModule(@NonNull Context context) {
+    this.context = Assert.isNotNull(context);
   }
 
   @Provides
-  Application provideApplication() {
-    return application;
+  Context provideContext() {
+    return context;
   }
 }
diff --git a/java/com/android/dialer/inject/DialerAppComponent.java b/java/com/android/dialer/inject/DialerAppComponent.java
deleted file mode 100644
index 9832ce8..0000000
--- a/java/com/android/dialer/inject/DialerAppComponent.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.inject;
-
-import com.android.dialer.enrichedcall.EnrichedCallManager;
-import com.android.dialer.enrichedcall.StubEnrichedCallModule;
-import dagger.Component;
-import javax.inject.Singleton;
-
-/** Core application-wide {@link Component} for the open source dialer app. */
-@Singleton
-@Component(modules = {ApplicationModule.class, StubEnrichedCallModule.class})
-public interface DialerAppComponent {
-  EnrichedCallManager enrichedCallManager();
-}
diff --git a/java/com/android/voicemailomtp/NeededForTesting.java b/java/com/android/dialer/inject/HasRootComponent.java
similarity index 67%
copy from java/com/android/voicemailomtp/NeededForTesting.java
copy to java/com/android/dialer/inject/HasRootComponent.java
index 20517fe..0802b80 100644
--- a/java/com/android/voicemailomtp/NeededForTesting.java
+++ b/java/com/android/dialer/inject/HasRootComponent.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2017 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,12 +14,12 @@
  * limitations under the License
  */
 
-package com.android.voicemailomtp;
+package com.android.dialer.inject;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-@Retention(RetentionPolicy.SOURCE)
-public @interface NeededForTesting {
-
+/**
+ * Used by packages to access the root component from the Application without creating a dependency
+ * cycle.
+ */
+public interface HasRootComponent {
+  Object component();
 }
diff --git a/java/com/android/dialer/interactions/PhoneNumberInteraction.java b/java/com/android/dialer/interactions/PhoneNumberInteraction.java
index f36e531..b797629 100644
--- a/java/com/android/dialer/interactions/PhoneNumberInteraction.java
+++ b/java/com/android/dialer/interactions/PhoneNumberInteraction.java
@@ -81,8 +81,10 @@
   private static final String TAG = PhoneNumberInteraction.class.getSimpleName();
   /** The identifier for a permissions request if one is generated. */
   public static final int REQUEST_READ_CONTACTS = 1;
+  public static final int REQUEST_CALL_PHONE = 2;
 
-  private static final String[] PHONE_NUMBER_PROJECTION =
+  @VisibleForTesting
+  public static final String[] PHONE_NUMBER_PROJECTION =
       new String[] {
         Phone._ID,
         Phone.NUMBER,
@@ -191,13 +193,14 @@
    *     numbers have been queried for. The activity must implement {@link InteractionErrorListener}
    *     and {@link DisambigDialogDismissedListener}.
    * @param isVideoCall {@code true} if the call is a video call, {@code false} otherwise.
+   * @return true if the necessary permissions were found to start the interaction, false otherwise
    */
-  public static void startInteractionForPhoneCall(
+  public static boolean startInteractionForPhoneCall(
       TransactionSafeActivity activity,
       Uri uri,
       boolean isVideoCall,
       CallSpecificAppData callSpecificAppData) {
-    new PhoneNumberInteraction(
+    return new PhoneNumberInteraction(
             activity, ContactDisplayUtils.INTERACTION_CALL, isVideoCall, callSpecificAppData)
         .startInteraction(uri);
   }
@@ -211,11 +214,19 @@
    * Initiates the interaction to result in either a phone call or sms message for a contact.
    *
    * @param uri Contact Uri
+   * @return true if the necessary permissions were found to start the interaction, false otherwise
    */
-  private void startInteraction(Uri uri) {
-    // It's possible for a shortcut to have been created, and then Contacts permissions revoked. To
-    // avoid a crash when the user tries to use such a shortcut, check for this condition and ask
-    // the user for the permission.
+  private boolean startInteraction(Uri uri) {
+    // It's possible for a shortcut to have been created, and then permissions revoked. To avoid a
+    // crash when the user tries to use such a shortcut, check for this condition and ask the user
+    // for the permission.
+    if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.CALL_PHONE)
+        != PackageManager.PERMISSION_GRANTED) {
+      LogUtil.i("PhoneNumberInteraction.startInteraction", "No phone permissions");
+      ActivityCompat.requestPermissions(
+          (Activity) mContext, new String[] {Manifest.permission.CALL_PHONE}, REQUEST_CALL_PHONE);
+      return false;
+    }
     if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.READ_CONTACTS)
         != PackageManager.PERMISSION_GRANTED) {
       LogUtil.i("PhoneNumberInteraction.startInteraction", "No contact permissions");
@@ -223,7 +234,7 @@
           (Activity) mContext,
           new String[] {Manifest.permission.READ_CONTACTS},
           REQUEST_READ_CONTACTS);
-      return;
+      return false;
     }
 
     if (mLoader != null) {
@@ -249,6 +260,7 @@
             mContext, queryUri, PHONE_NUMBER_PROJECTION, PHONE_NUMBER_SELECTION, null, null);
     mLoader.registerListener(0, this);
     mLoader.startLoading();
+    return true;
   }
 
   @Override
@@ -457,8 +469,8 @@
    * will be chosen to make a call or initiate an sms message.
    *
    * <p>It is recommended to use {@link #startInteractionForPhoneCall(TransactionSafeActivity, Uri,
-   * boolean, int)} instead of directly using this class, as those methods handle one or multiple
-   * data cases appropriately.
+   * boolean, CallSpecificAppData)} instead of directly using this class, as those methods handle
+   * one or multiple data cases appropriately.
    *
    * <p>This fragment may only be attached to activities which implement {@link
    * DisambigDialogDismissedListener}.
diff --git a/java/com/android/dialer/logging/nano/ContactLookupResult.java b/java/com/android/dialer/logging/nano/ContactLookupResult.java
index 8960560..93f5f01 100644
--- a/java/com/android/dialer/logging/nano/ContactLookupResult.java
+++ b/java/com/android/dialer/logging/nano/ContactLookupResult.java
@@ -11,17 +11,19 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
  */
 
 // Generated by the protocol buffer compiler.  DO NOT EDIT!
 
 package com.android.dialer.logging.nano;
 
+/** This file is autogenerated, but javadoc required. */
 @SuppressWarnings("hiding")
-public final class ContactLookupResult extends
-    com.google.protobuf.nano.ExtendableMessageNano<ContactLookupResult> {
+public final class ContactLookupResult
+    extends com.google.protobuf.nano.ExtendableMessageNano<ContactLookupResult> {
 
+  /** This file is autogenerated, but javadoc required. */
   // enum Type
   public interface Type {
     public static final int UNKNOWN_LOOKUP_RESULT_TYPE = 0;
@@ -34,11 +36,11 @@
   }
 
   private static volatile ContactLookupResult[] _emptyArray;
+
   public static ContactLookupResult[] emptyArray() {
     // Lazily initializes the empty array
     if (_emptyArray == null) {
-      synchronized (
-          com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+      synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
         if (_emptyArray == null) {
           _emptyArray = new ContactLookupResult[0];
         }
@@ -60,20 +62,20 @@
   }
 
   @Override
-  public ContactLookupResult mergeFrom(
-          com.google.protobuf.nano.CodedInputByteBufferNano input)
+  public ContactLookupResult mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
       throws java.io.IOException {
     while (true) {
       int tag = input.readTag();
       switch (tag) {
         case 0:
           return this;
-        default: {
-          if (!super.storeUnknownField(input, tag)) {
-            return this;
+        default:
+          {
+            if (!super.storeUnknownField(input, tag)) {
+              return this;
+            }
+            break;
           }
-          break;
-        }
       }
     }
   }
@@ -84,8 +86,7 @@
   }
 
   public static ContactLookupResult parseFrom(
-          com.google.protobuf.nano.CodedInputByteBufferNano input)
-      throws java.io.IOException {
+      com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException {
     return new ContactLookupResult().mergeFrom(input);
   }
 }
diff --git a/java/com/android/dialer/logging/nano/ContactSource.java b/java/com/android/dialer/logging/nano/ContactSource.java
index 35d8b8c..dbe40cd 100644
--- a/java/com/android/dialer/logging/nano/ContactSource.java
+++ b/java/com/android/dialer/logging/nano/ContactSource.java
@@ -11,17 +11,19 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
  */
 
 // Generated by the protocol buffer compiler.  DO NOT EDIT!
 
 package com.android.dialer.logging.nano;
 
+/** This file is autogenerated, but javadoc required. */
 @SuppressWarnings("hiding")
-public final class ContactSource extends
-    com.google.protobuf.nano.ExtendableMessageNano<ContactSource> {
+public final class ContactSource
+    extends com.google.protobuf.nano.ExtendableMessageNano<ContactSource> {
 
+  /** This file is autogenerated, but javadoc required. */
   // enum Type
   public interface Type {
     public static final int UNKNOWN_SOURCE_TYPE = 0;
@@ -33,11 +35,11 @@
   }
 
   private static volatile ContactSource[] _emptyArray;
+
   public static ContactSource[] emptyArray() {
     // Lazily initializes the empty array
     if (_emptyArray == null) {
-      synchronized (
-          com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+      synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
         if (_emptyArray == null) {
           _emptyArray = new ContactSource[0];
         }
@@ -59,20 +61,20 @@
   }
 
   @Override
-  public ContactSource mergeFrom(
-          com.google.protobuf.nano.CodedInputByteBufferNano input)
+  public ContactSource mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
       throws java.io.IOException {
     while (true) {
       int tag = input.readTag();
       switch (tag) {
         case 0:
           return this;
-        default: {
-          if (!super.storeUnknownField(input, tag)) {
-            return this;
+        default:
+          {
+            if (!super.storeUnknownField(input, tag)) {
+              return this;
+            }
+            break;
           }
-          break;
-        }
       }
     }
   }
@@ -82,8 +84,7 @@
     return com.google.protobuf.nano.MessageNano.mergeFrom(new ContactSource(), data);
   }
 
-  public static ContactSource parseFrom(
-          com.google.protobuf.nano.CodedInputByteBufferNano input)
+  public static ContactSource parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
       throws java.io.IOException {
     return new ContactSource().mergeFrom(input);
   }
diff --git a/java/com/android/dialer/logging/nano/DialerImpression.java b/java/com/android/dialer/logging/nano/DialerImpression.java
index 6bb5675..80a006b 100644
--- a/java/com/android/dialer/logging/nano/DialerImpression.java
+++ b/java/com/android/dialer/logging/nano/DialerImpression.java
@@ -14,12 +14,16 @@
  * limitations under the License.
  */
 
+// Generated by the protocol buffer compiler.  DO NOT EDIT!
+
 package com.android.dialer.logging.nano;
 
+/** This file is autogenerated, but javadoc required. */
 @SuppressWarnings("hiding")
-public final class DialerImpression extends
-    com.google.protobuf.nano.ExtendableMessageNano<DialerImpression> {
+public final class DialerImpression
+    extends com.google.protobuf.nano.ExtendableMessageNano<DialerImpression> {
 
+  /** This file is autogenerated, but javadoc required. */
   // enum Type
   public interface Type {
     public static final int UNKNOWN_AOSP_EVENT_TYPE = 1000;
@@ -33,7 +37,8 @@
     public static final int DIALOG_ACTION_CONFIRM_NUMBER_NOT_SPAM = 1008;
     public static final int REPORT_AS_NOT_SPAM_VIA_UNBLOCK_NUMBER = 1009;
     public static final int DIALOG_ACTION_CONFIRM_NUMBER_SPAM_INDIRECTLY_VIA_BLOCK_NUMBER = 1010;
-    public static final int REPORT_CALL_AS_SPAM_VIA_CALL_LOG_BLOCK_REPORT_SPAM_SENT_VIA_BLOCK_NUMBER_DIALOG = 1011;
+    public static final int
+        REPORT_CALL_AS_SPAM_VIA_CALL_LOG_BLOCK_REPORT_SPAM_SENT_VIA_BLOCK_NUMBER_DIALOG = 1011;
     public static final int USER_ACTION_BLOCKED_NUMBER = 1012;
     public static final int USER_ACTION_UNBLOCKED_NUMBER = 1013;
     public static final int SPAM_AFTER_CALL_NOTIFICATION_BLOCK_NUMBER = 1014;
@@ -41,7 +46,8 @@
     public static final int SPAM_AFTER_CALL_NOTIFICATION_SHOW_NON_SPAM_DIALOG = 1016;
     public static final int SPAM_AFTER_CALL_NOTIFICATION_ADD_TO_CONTACTS = 1019;
     public static final int SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_SPAM = 1020;
-    public static final int SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_NOT_SPAM_AND_BLOCKED = 1021;
+    public static final int SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_NOT_SPAM_AND_BLOCKED =
+        1021;
     public static final int SPAM_AFTER_CALL_NOTIFICATION_REPORT_NUMBER_AS_NOT_SPAM = 1022;
     public static final int SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_SPAM_DIALOG = 1024;
     public static final int SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_NON_SPAM_DIALOG = 1025;
@@ -89,7 +95,8 @@
     public static final int STORAGE_PERMISSION_DENIED = 1073;
     public static final int CAMERA_PERMISSION_DENIED = 1078;
     public static final int VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_ACTIVITY = 1079;
-    public static final int VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_NOTIFICATION = 1080;
+    public static final int VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_NOTIFICATION =
+        1080;
     public static final int BACKUP_ON_BACKUP = 1081;
     public static final int BACKUP_ON_FULL_BACKUP = 1082;
     public static final int BACKUP_ON_BACKUP_DISABLED = 1083;
@@ -116,15 +123,43 @@
     public static final int BACKUP_ON_RESTORE_VM_DUPLICATE_NOT_RESTORING = 1104;
     public static final int CALL_LOG_SHARE_AND_CALL = 1105;
     public static final int CALL_COMPOSER_ACTIVITY_PLACE_RCS_CALL = 1106;
-    public static final int CALL_COMPOSER_ACTIVITY_SEND_AND_CALL_PRESSED_WHEN_SESSION_NOT_READY = 1107;
+    public static final int CALL_COMPOSER_ACTIVITY_SEND_AND_CALL_PRESSED_WHEN_SESSION_NOT_READY =
+        1107;
+    public static final int POST_CALL_PROMPT_USER_TO_SEND_MESSAGE_CLICKED = 1108;
+    public static final int POST_CALL_PROMPT_USER_TO_SEND_MESSAGE = 1109;
+    public static final int POST_CALL_PROMPT_USER_TO_VIEW_SENT_MESSAGE = 1110;
+    public static final int POST_CALL_PROMPT_USER_TO_VIEW_SENT_MESSAGE_CLICKED = 1111;
+    public static final int IN_CALL_SCREEN_TURN_ON_MUTE = 1112;
+    public static final int IN_CALL_SCREEN_TURN_OFF_MUTE = 1113;
+    public static final int IN_CALL_SCREEN_SWAP_CAMERA = 1114;
+    public static final int IN_CALL_SCREEN_TURN_ON_VIDEO = 1115;
+    public static final int IN_CALL_SCREEN_TURN_OFF_VIDEO = 1116;
+    public static final int VIDEO_CALL_WITH_INCOMING_VOICE_CALL = 1117;
+    public static final int VIDEO_CALL_WITH_INCOMING_VIDEO_CALL = 1118;
+    public static final int VOICE_CALL_WITH_INCOMING_VOICE_CALL = 1119;
+    public static final int VOICE_CALL_WITH_INCOMING_VIDEO_CALL = 1120;
+    public static final int CALL_DETAILS_COPY_NUMBER = 1121;
+    public static final int CALL_DETAILS_EDIT_BEFORE_CALL = 1122;
+    public static final int CALL_DETAILS_CALL_BACK = 1123;
+    public static final int VVM_USER_DISMISSED_VM_ALMOST_FULL_PROMO = 1124;
+    public static final int VVM_USER_DISMISSED_VM_FULL_PROMO = 1125;
+    public static final int VVM_USER_ENABLED_ARCHIVE_FROM_VM_ALMOST_FULL_PROMO = 1126;
+    public static final int VVM_USER_ENABLED_ARCHIVE_FROM_VM_FULL_PROMO = 1127;
+    public static final int VVM_USER_SHOWN_VM_ALMOST_FULL_PROMO = 1128;
+    public static final int VVM_USER_SHOWN_VM_FULL_PROMO = 1129;
+    public static final int VVM_USER_SHOWN_VM_ALMOST_FULL_ERROR_MESSAGE = 1130;
+    public static final int VVM_USER_SHOWN_VM_FULL_ERROR_MESSAGE = 1131;
+    public static final int VVM_USER_TURNED_ARCHIVE_ON_FROM_SETTINGS = 1132;
+    public static final int VVM_USER_TURNED_ARCHIVE_OFF_FROM_SETTINGS = 1133;
+    public static final int VVM_ARCHIVE_AUTO_DELETED_VM_FROM_SERVER = 1134;
+    public static final int VVM_ARCHIVE_AUTO_DELETE_TURNED_OFF = 1135;
   }
 
   private static volatile DialerImpression[] _emptyArray;
   public static DialerImpression[] emptyArray() {
     // Lazily initializes the empty array
     if (_emptyArray == null) {
-      synchronized (
-          com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+      synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
         if (_emptyArray == null) {
           _emptyArray = new DialerImpression[0];
         }
@@ -146,20 +181,20 @@
   }
 
   @Override
-  public DialerImpression mergeFrom(
-          com.google.protobuf.nano.CodedInputByteBufferNano input)
+  public DialerImpression mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
       throws java.io.IOException {
     while (true) {
       int tag = input.readTag();
       switch (tag) {
         case 0:
           return this;
-        default: {
-          if (!super.storeUnknownField(input, tag)) {
-            return this;
+        default:
+          {
+            if (!super.storeUnknownField(input, tag)) {
+              return this;
+            }
+            break;
           }
-          break;
-        }
       }
     }
   }
@@ -169,10 +204,8 @@
     return com.google.protobuf.nano.MessageNano.mergeFrom(new DialerImpression(), data);
   }
 
-  public static DialerImpression parseFrom(
-          com.google.protobuf.nano.CodedInputByteBufferNano input)
+  public static DialerImpression parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
       throws java.io.IOException {
     return new DialerImpression().mergeFrom(input);
   }
 }
-
diff --git a/java/com/android/dialer/logging/nano/InteractionEvent.java b/java/com/android/dialer/logging/nano/InteractionEvent.java
index 8d9430b..7ca95fa 100644
--- a/java/com/android/dialer/logging/nano/InteractionEvent.java
+++ b/java/com/android/dialer/logging/nano/InteractionEvent.java
@@ -23,8 +23,8 @@
 public final class InteractionEvent
     extends com.google.protobuf.nano.ExtendableMessageNano<InteractionEvent> {
 
-  // enum Type
   /** This file is autogenerated, but javadoc required. */
+  // enum Type
   public interface Type {
     public static final int UNKNOWN = 0;
     public static final int CALL_BLOCKED = 15;
diff --git a/java/com/android/dialer/logging/nano/ReportingLocation.java b/java/com/android/dialer/logging/nano/ReportingLocation.java
index 1f05ce4..08ee04e 100644
--- a/java/com/android/dialer/logging/nano/ReportingLocation.java
+++ b/java/com/android/dialer/logging/nano/ReportingLocation.java
@@ -11,17 +11,19 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
  */
 
 // Generated by the protocol buffer compiler.  DO NOT EDIT!
 
 package com.android.dialer.logging.nano;
 
+/** This file is autogenerated, but javadoc required. */
 @SuppressWarnings("hiding")
-public final class ReportingLocation extends
-    com.google.protobuf.nano.ExtendableMessageNano<ReportingLocation> {
+public final class ReportingLocation
+    extends com.google.protobuf.nano.ExtendableMessageNano<ReportingLocation> {
 
+  /** This file is autogenerated, but javadoc required. */
   // enum Type
   public interface Type {
     public static final int UNKNOWN_REPORTING_LOCATION = 0;
@@ -30,11 +32,11 @@
   }
 
   private static volatile ReportingLocation[] _emptyArray;
+
   public static ReportingLocation[] emptyArray() {
     // Lazily initializes the empty array
     if (_emptyArray == null) {
-      synchronized (
-          com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+      synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
         if (_emptyArray == null) {
           _emptyArray = new ReportingLocation[0];
         }
@@ -56,20 +58,20 @@
   }
 
   @Override
-  public ReportingLocation mergeFrom(
-          com.google.protobuf.nano.CodedInputByteBufferNano input)
+  public ReportingLocation mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
       throws java.io.IOException {
     while (true) {
       int tag = input.readTag();
       switch (tag) {
         case 0:
           return this;
-        default: {
-          if (!super.storeUnknownField(input, tag)) {
-            return this;
+        default:
+          {
+            if (!super.storeUnknownField(input, tag)) {
+              return this;
+            }
+            break;
           }
-          break;
-        }
       }
     }
   }
@@ -79,8 +81,7 @@
     return com.google.protobuf.nano.MessageNano.mergeFrom(new ReportingLocation(), data);
   }
 
-  public static ReportingLocation parseFrom(
-          com.google.protobuf.nano.CodedInputByteBufferNano input)
+  public static ReportingLocation parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
       throws java.io.IOException {
     return new ReportingLocation().mergeFrom(input);
   }
diff --git a/java/com/android/dialer/logging/nano/ScreenEvent.java b/java/com/android/dialer/logging/nano/ScreenEvent.java
index be4e5eb..bd5b817 100644
--- a/java/com/android/dialer/logging/nano/ScreenEvent.java
+++ b/java/com/android/dialer/logging/nano/ScreenEvent.java
@@ -22,8 +22,8 @@
 @SuppressWarnings("hiding")
 public final class ScreenEvent extends com.google.protobuf.nano.ExtendableMessageNano<ScreenEvent> {
 
-  // enum Type
   /** This file is autogenerated, but javadoc required. */
+  // enum Type
   public interface Type {
     public static final int UNKNOWN = 0;
     public static final int DIALPAD = 1;
diff --git a/java/com/android/dialer/multimedia/AutoValue_MultimediaData.java b/java/com/android/dialer/multimedia/AutoValue_MultimediaData.java
deleted file mode 100644
index cc68150..0000000
--- a/java/com/android/dialer/multimedia/AutoValue_MultimediaData.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.multimedia;
-
-import android.location.Location;
-import android.net.Uri;
-import android.support.annotation.Nullable;
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_MultimediaData extends MultimediaData {
-
-  private final String subject;
-  private final Location location;
-  private final Uri imageUri;
-  private final String imageContentType;
-  private final boolean important;
-
-  private AutoValue_MultimediaData(
-      @Nullable String subject,
-      @Nullable Location location,
-      @Nullable Uri imageUri,
-      @Nullable String imageContentType,
-      boolean important) {
-    this.subject = subject;
-    this.location = location;
-    this.imageUri = imageUri;
-    this.imageContentType = imageContentType;
-    this.important = important;
-  }
-
-  @Nullable
-  @Override
-  public String getSubject() {
-    return subject;
-  }
-
-  @Nullable
-  @Override
-  public Location getLocation() {
-    return location;
-  }
-
-  @Nullable
-  @Override
-  public Uri getImageUri() {
-    return imageUri;
-  }
-
-  @Nullable
-  @Override
-  public String getImageContentType() {
-    return imageContentType;
-  }
-
-  @Override
-  public boolean isImportant() {
-    return important;
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o == this) {
-      return true;
-    }
-    if (o instanceof MultimediaData) {
-      MultimediaData that = (MultimediaData) o;
-      return ((this.subject == null) ? (that.getSubject() == null) : this.subject.equals(that.getSubject()))
-           && ((this.location == null) ? (that.getLocation() == null) : this.location.equals(that.getLocation()))
-           && ((this.imageUri == null) ? (that.getImageUri() == null) : this.imageUri.equals(that.getImageUri()))
-           && ((this.imageContentType == null) ? (that.getImageContentType() == null) : this.imageContentType.equals(that.getImageContentType()))
-           && (this.important == that.isImportant());
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    int h = 1;
-    h *= 1000003;
-    h ^= (subject == null) ? 0 : this.subject.hashCode();
-    h *= 1000003;
-    h ^= (location == null) ? 0 : this.location.hashCode();
-    h *= 1000003;
-    h ^= (imageUri == null) ? 0 : this.imageUri.hashCode();
-    h *= 1000003;
-    h ^= (imageContentType == null) ? 0 : this.imageContentType.hashCode();
-    h *= 1000003;
-    h ^= this.important ? 1231 : 1237;
-    return h;
-  }
-
-  static final class Builder extends MultimediaData.Builder {
-    private String subject;
-    private Location location;
-    private Uri imageUri;
-    private String imageContentType;
-    private Boolean important;
-    Builder() {
-    }
-    private Builder(MultimediaData source) {
-      this.subject = source.getSubject();
-      this.location = source.getLocation();
-      this.imageUri = source.getImageUri();
-      this.imageContentType = source.getImageContentType();
-      this.important = source.isImportant();
-    }
-    @Override
-    public MultimediaData.Builder setSubject(@Nullable String subject) {
-      this.subject = subject;
-      return this;
-    }
-    @Override
-    public MultimediaData.Builder setLocation(@Nullable Location location) {
-      this.location = location;
-      return this;
-    }
-    @Override
-    MultimediaData.Builder setImageUri(@Nullable Uri imageUri) {
-      this.imageUri = imageUri;
-      return this;
-    }
-    @Override
-    MultimediaData.Builder setImageContentType(@Nullable String imageContentType) {
-      this.imageContentType = imageContentType;
-      return this;
-    }
-    @Override
-    public MultimediaData.Builder setImportant(boolean important) {
-      this.important = important;
-      return this;
-    }
-    @Override
-    public MultimediaData build() {
-      String missing = "";
-      if (this.important == null) {
-        missing += " important";
-      }
-      if (!missing.isEmpty()) {
-        throw new IllegalStateException("Missing required properties:" + missing);
-      }
-      return new AutoValue_MultimediaData(
-          this.subject,
-          this.location,
-          this.imageUri,
-          this.imageContentType,
-          this.important);
-    }
-  }
-
-}
diff --git a/java/com/android/dialer/multimedia/MultimediaData.java b/java/com/android/dialer/multimedia/MultimediaData.java
index ebd41a9..22bb764 100644
--- a/java/com/android/dialer/multimedia/MultimediaData.java
+++ b/java/com/android/dialer/multimedia/MultimediaData.java
@@ -21,10 +21,10 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import com.android.dialer.common.LogUtil;
+import com.google.auto.value.AutoValue;
 
-
-/** Holds the data associated with an enriched call session. */
-
+/** Holds data associated with a call. */
+@AutoValue
 public abstract class MultimediaData {
 
   public static final MultimediaData EMPTY = builder().build();
@@ -34,32 +34,33 @@
     return new AutoValue_MultimediaData.Builder().setImportant(false);
   }
 
-  /** Returns the call composer subject if set, or null if this isn't a call composer session. */
+  /**
+   * Returns the text part of this data.
+   *
+   * <p>This field is used for both the call composer session and the post call note.
+   */
   @Nullable
-  public abstract String getSubject();
+  public abstract String getText();
 
-  /** Returns the call composer location if set, or null if this isn't a call composer session. */
+  /** Returns the location part of this data. */
   @Nullable
   public abstract Location getLocation();
 
-  /** Returns {@code true} if this session contains image data. */
+  /** Returns {@code true} if this object contains image data. */
   public boolean hasImageData() {
     // imageUri and content are always either both null or nonnull
     return getImageUri() != null && getImageContentType() != null;
   }
 
-  /** Returns the call composer photo if set, or null if this isn't a call composer session. */
+  /** Returns the image uri part of this object's image. */
   @Nullable
   public abstract Uri getImageUri();
 
-  /**
-   * Returns the content type of the image, either image/png or image/jpeg, if set, or null if this
-   * isn't a call composer session.
-   */
+  /** Returns the content type part of this object's image, either image/png or image/jpeg. */
   @Nullable
   public abstract String getImageContentType();
 
-  /** Returns {@code true} if this is a call composer session that's marked as important. */
+  /** Returns {@code true} if this data is marked as important. */
   public abstract boolean isImportant();
 
   /** Returns the string form of this MultimediaData with no PII. */
@@ -68,7 +69,7 @@
     return String.format(
         "MultimediaData{subject: %s, location: %s, imageUrl: %s, imageContentType: %s, "
             + "important: %b}",
-        LogUtil.sanitizePii(getSubject()),
+        LogUtil.sanitizePii(getText()),
         LogUtil.sanitizePii(getLocation()),
         LogUtil.sanitizePii(getImageUri()),
         getImageContentType(),
@@ -76,10 +77,10 @@
   }
 
   /** Creates instances of {@link MultimediaData}. */
-
+  @AutoValue.Builder
   public abstract static class Builder {
 
-    public abstract Builder setSubject(@NonNull String subject);
+    public abstract Builder setText(@NonNull String subject);
 
     public abstract Builder setLocation(@NonNull Location location);
 
diff --git a/java/com/android/dialer/notification/AndroidManifest.xml b/java/com/android/dialer/notification/AndroidManifest.xml
new file mode 100644
index 0000000..c5484f2
--- /dev/null
+++ b/java/com/android/dialer/notification/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<manifest
+  package="com.android.dialer.notification"
+  xmlns:android="http://schemas.android.com/apk/res/android">
+  <uses-sdk android:minSdkVersion="23" />
+</manifest>
diff --git a/java/com/android/dialer/notification/GroupedNotificationUtil.java b/java/com/android/dialer/notification/GroupedNotificationUtil.java
new file mode 100644
index 0000000..63ea517
--- /dev/null
+++ b/java/com/android/dialer/notification/GroupedNotificationUtil.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.notification;
+
+import android.app.NotificationManager;
+import android.service.notification.StatusBarNotification;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import java.util.Objects;
+
+/** Utilities for dealing with grouped notifications */
+public final class GroupedNotificationUtil {
+
+  /**
+   * Remove notification(s) that were added as part of a group. Will ensure that if this is the last
+   * notification in the group the summary will be removed.
+   *
+   * @param tag String tag as included in {@link NotificationManager#notify(String, int,
+   *     android.app.Notification)}. If null will remove all notifications under id
+   * @param id notification id as included with {@link NotificationManager#notify(String, int,
+   *     android.app.Notification)}.
+   * @param summaryTag String tag of the summary notification
+   */
+  public static void removeNotification(
+      @NonNull NotificationManager notificationManager,
+      @Nullable String tag,
+      int id,
+      @NonNull String summaryTag) {
+    if (tag == null) {
+      // Clear all missed call notifications
+      for (StatusBarNotification notification : notificationManager.getActiveNotifications()) {
+        if (notification.getId() == id) {
+          notificationManager.cancel(notification.getTag(), id);
+        }
+      }
+    } else {
+      notificationManager.cancel(tag, id);
+
+      // See if other non-summary missed call notifications exist, and if not then clear the summary
+      boolean clearSummary = true;
+      for (StatusBarNotification notification : notificationManager.getActiveNotifications()) {
+        if (notification.getId() == id && !Objects.equals(summaryTag, notification.getTag())) {
+          clearSummary = false;
+          break;
+        }
+      }
+      if (clearSummary) {
+        notificationManager.cancel(summaryTag, id);
+      }
+    }
+  }
+}
diff --git a/java/com/android/dialer/notification/NotificationChannelManager.java b/java/com/android/dialer/notification/NotificationChannelManager.java
new file mode 100644
index 0000000..9ff5732
--- /dev/null
+++ b/java/com/android/dialer/notification/NotificationChannelManager.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.notification;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringDef;
+import android.support.v4.os.BuildCompat;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.dialer.buildtype.BuildType;
+import com.android.dialer.common.LogUtil;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Contains info on how to create {@link NotificationChannel NotificationChannels} */
+public class NotificationChannelManager {
+
+  private static NotificationChannelManager instance;
+
+  public static NotificationChannelManager getInstance() {
+    if (instance == null) {
+      instance = new NotificationChannelManager();
+    }
+    return instance;
+  }
+
+  /**
+   * Set the channel of notification appropriately. Will create the channel if it does not already
+   * exist. Safe to call pre-O (will no-op).
+   *
+   * <p>phoneAccount should only be null if channelName is {@link Channel#MISC}.
+   */
+  public static void applyChannel(
+      @NonNull Notification.Builder notification,
+      @NonNull Context context,
+      @Channel String channelName,
+      @Nullable PhoneAccountHandle phoneAccount) {
+    if (phoneAccount == null) {
+      if (!Channel.MISC.equals(channelName)) {
+        IllegalArgumentException exception =
+            new IllegalArgumentException(
+                "Phone account handle must not be null unless on Channel.MISC");
+        if (BuildType.get() >= BuildType.RELEASE) {
+          LogUtil.e("NotificationChannelManager.applyChannel", null, exception);
+        } else {
+          throw exception;
+        }
+      }
+    }
+
+    if (BuildCompat.isAtLeastO()) {
+      NotificationChannel channel =
+          NotificationChannelManager.getInstance().getChannel(context, channelName, phoneAccount);
+      notification.setChannel(channel.getId());
+    }
+  }
+
+  /** The base Channel IDs for {@link NotificationChannel} */
+  @Retention(RetentionPolicy.SOURCE)
+  @StringDef({
+    Channel.INCOMING_CALL,
+    Channel.ONGOING_CALL,
+    Channel.MISSED_CALL,
+    Channel.VOICEMAIL,
+    Channel.EXTERNAL_CALL,
+    Channel.MISC
+  })
+  public @interface Channel {
+    String INCOMING_CALL = "incomingCall";
+    String ONGOING_CALL = "ongoingCall";
+    String MISSED_CALL = "missedCall";
+    String VOICEMAIL = "voicemail";
+    String EXTERNAL_CALL = "externalCall";
+    String MISC = "miscellaneous";
+  }
+
+  private NotificationChannelManager() {}
+
+  private NotificationChannel getChannel(
+      @NonNull Context context,
+      @Channel String channelName,
+      @Nullable PhoneAccountHandle phoneAccount) {
+    String channelId = channelNameToId(channelName, phoneAccount);
+    NotificationChannel channel = getNotificationManager(context).getNotificationChannel(channelId);
+    if (channel == null) {
+      channel = createChannel(context, channelName, phoneAccount);
+    }
+    return channel;
+  }
+
+  private static String channelNameToId(
+      @Channel String name, @Nullable PhoneAccountHandle phoneAccountHandle) {
+    if (phoneAccountHandle == null) {
+      return name;
+    } else {
+      return name + ":" + phoneAccountHandle.getId();
+    }
+  }
+
+  private NotificationChannel createChannel(
+      Context context,
+      @Channel String channelName,
+      @Nullable PhoneAccountHandle phoneAccountHandle) {
+    String channelId = channelNameToId(channelName, phoneAccountHandle);
+
+    if (phoneAccountHandle != null) {
+      PhoneAccount account = getTelecomManager(context).getPhoneAccount(phoneAccountHandle);
+      NotificationChannelGroup group =
+          new NotificationChannelGroup(
+              phoneAccountHandle.getId(),
+              (account == null) ? phoneAccountHandle.getId() : account.getLabel().toString());
+      getNotificationManager(context)
+          .createNotificationChannelGroup(group); // No-op if already exists
+    } else if (!Channel.MISC.equals(channelName)) {
+      LogUtil.w(
+          "NotificationChannelManager.createChannel",
+          "Null PhoneAccountHandle with channel " + channelName);
+    }
+
+    Uri silentRingtone = Uri.parse("");
+
+    CharSequence name;
+    int importance;
+    boolean canShowBadge;
+    boolean lights;
+    boolean vibration;
+    Uri sound;
+    switch (channelName) {
+      case Channel.INCOMING_CALL:
+        name = context.getText(R.string.notification_channel_incoming_call);
+        importance = NotificationManager.IMPORTANCE_MAX;
+        canShowBadge = false;
+        lights = true;
+        vibration = false;
+        sound = silentRingtone;
+        break;
+      case Channel.MISSED_CALL:
+        name = context.getText(R.string.notification_channel_missed_call);
+        importance = NotificationManager.IMPORTANCE_DEFAULT;
+        canShowBadge = true;
+        lights = true;
+        vibration = true;
+        sound = silentRingtone;
+        break;
+      case Channel.ONGOING_CALL:
+        name = context.getText(R.string.notification_channel_ongoing_call);
+        importance = NotificationManager.IMPORTANCE_DEFAULT;
+        canShowBadge = false;
+        lights = false;
+        vibration = false;
+        sound = null;
+        break;
+      case Channel.VOICEMAIL:
+        name = context.getText(R.string.notification_channel_voicemail);
+        importance = NotificationManager.IMPORTANCE_DEFAULT;
+        canShowBadge = true;
+        lights = true;
+        vibration = true;
+        sound =
+            TelephonyManagerCompat.getVoicemailRingtoneUri(
+                getTelephonyManager(context), phoneAccountHandle);
+        break;
+      case Channel.EXTERNAL_CALL:
+        name = context.getText(R.string.notification_channel_external_call);
+        importance = NotificationManager.IMPORTANCE_HIGH;
+        canShowBadge = false;
+        lights = true;
+        vibration = true;
+        sound = null;
+        break;
+      case Channel.MISC:
+        name = context.getText(R.string.notification_channel_misc);
+        importance = NotificationManager.IMPORTANCE_DEFAULT;
+        canShowBadge = false;
+        lights = true;
+        vibration = true;
+        sound = null;
+        break;
+      default:
+        throw new IllegalArgumentException("Unknown channel: " + channelName);
+    }
+
+    NotificationChannel channel = new NotificationChannel(channelId, name, importance);
+    channel.setShowBadge(canShowBadge);
+    if (sound != null) {
+      channel.setSound(
+          sound,
+          new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build());
+    }
+    channel.enableLights(lights);
+    channel.enableVibration(vibration);
+    getNotificationManager(context).createNotificationChannel(channel);
+    return channel;
+  }
+
+  private static NotificationManager getNotificationManager(@NonNull Context context) {
+    return context.getSystemService(NotificationManager.class);
+  }
+
+  private static TelephonyManager getTelephonyManager(@NonNull Context context) {
+    return context.getSystemService(TelephonyManager.class);
+  }
+
+  private static TelecomManager getTelecomManager(@NonNull Context context) {
+    return context.getSystemService(TelecomManager.class);
+  }
+}
diff --git a/java/com/android/dialer/notification/res/values/ids.xml b/java/com/android/dialer/notification/res/values/ids.xml
new file mode 100644
index 0000000..6bdb489
--- /dev/null
+++ b/java/com/android/dialer/notification/res/values/ids.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<resources>
+  <item name="notification_incoming_call" type="id"/>
+  <item name="notification_ongoing_call" type="id"/>
+  <item name="notification_missed_call" type="id"/>
+  <item name="notification_voicemail" type="id"/>
+  <item name="notification_external_call" type="id"/>
+  <item name="notification_call_blocking_disabled_by_emergency_call" type="id"/>
+  <item name="notification_spam_call" type="id"/>
+  <item name="notification_feedback" type="id"/>
+</resources>
diff --git a/java/com/android/dialer/notification/res/values/strings.xml b/java/com/android/dialer/notification/res/values/strings.xml
new file mode 100644
index 0000000..2fc4962
--- /dev/null
+++ b/java/com/android/dialer/notification/res/values/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<resources>
+  <string name="notification_channel_incoming_call">Incoming calls</string>
+  <string name="notification_channel_ongoing_call">Ongoing calls</string>
+  <string name="notification_channel_missed_call">Missed calls</string>
+  <string name="notification_channel_voicemail">Voicemails</string>
+  <string name="notification_channel_external_call">External calls</string>
+  <string name="notification_channel_misc">Miscellaneous</string>
+</resources>
diff --git a/java/com/android/dialer/oem/AndroidManifest.xml b/java/com/android/dialer/oem/AndroidManifest.xml
new file mode 100644
index 0000000..e161a6d
--- /dev/null
+++ b/java/com/android/dialer/oem/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+  package="com.android.dialer.oem">
+</manifest>
\ No newline at end of file
diff --git a/java/com/android/dialer/oem/MotorolaHiddenMenuKeySequence.java b/java/com/android/dialer/oem/MotorolaHiddenMenuKeySequence.java
new file mode 100644
index 0000000..18f621e
--- /dev/null
+++ b/java/com/android/dialer/oem/MotorolaHiddenMenuKeySequence.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * This file is derived in part from code issued under the following license.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.android.dialer.oem;
+
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import com.android.dialer.common.LogUtil;
+import java.util.regex.Pattern;
+
+/**
+ * Util class to handle special char sequence and launch corresponding intent based the sequence.
+ */
+public class MotorolaHiddenMenuKeySequence {
+  private static final String EXTRA_HIDDEN_MENU_CODE = "HiddenMenuCode";
+  private static MotorolaHiddenMenuKeySequence instance = null;
+
+  private static String[] hiddenKeySequenceArray = null;
+  private static String[] hiddenKeySequenceIntentArray = null;
+  private static String[] hiddenKeyPatternArray = null;
+  private static String[] hiddenKeyPatternIntentArray = null;
+  private static boolean featureHiddenMenuEnabled = false;
+
+  /**
+   * Handle input char sequence.
+   *
+   * @param context context
+   * @param input input sequence
+   * @return true if the input matches any pattern
+   */
+  static boolean handleCharSequence(Context context, String input) {
+    getInstance(context);
+    if (!featureHiddenMenuEnabled) {
+      return false;
+    }
+    return handleKeySequence(context, input) || handleKeyPattern(context, input);
+  }
+
+  /**
+   * Public interface to return the Singleton instance
+   *
+   * @param context the Context
+   * @return the MotorolaHiddenMenuKeySequence singleton instance
+   */
+  private static synchronized MotorolaHiddenMenuKeySequence getInstance(Context context) {
+    if (null == instance) {
+      instance = new MotorolaHiddenMenuKeySequence(context);
+    }
+    return instance;
+  }
+
+  private MotorolaHiddenMenuKeySequence(Context context) {
+    featureHiddenMenuEnabled =
+        context.getResources().getBoolean(R.bool.motorola_feature_hidden_menu);
+    // In case we do have a SPN from resource we need to match from service; otherwise we are
+    // free to go
+    if (featureHiddenMenuEnabled) {
+
+      hiddenKeySequenceArray =
+          context.getResources().getStringArray(R.array.motorola_hidden_menu_key_sequence);
+      hiddenKeySequenceIntentArray =
+          context.getResources().getStringArray(R.array.motorola_hidden_menu_key_sequence_intents);
+      hiddenKeyPatternArray =
+          context.getResources().getStringArray(R.array.motorola_hidden_menu_key_pattern);
+      hiddenKeyPatternIntentArray =
+          context.getResources().getStringArray(R.array.motorola_hidden_menu_key_pattern_intents);
+
+      if (hiddenKeySequenceArray.length != hiddenKeySequenceIntentArray.length
+          || hiddenKeyPatternArray.length != hiddenKeyPatternIntentArray.length
+          || (hiddenKeySequenceArray.length == 0 && hiddenKeyPatternArray.length == 0)) {
+        LogUtil.e(
+            "MotorolaHiddenMenuKeySequence",
+            "the key sequence array is not matching, turn off feature."
+                + "key sequence: %d != %d, key pattern %d != %d",
+            hiddenKeySequenceArray.length,
+            hiddenKeySequenceIntentArray.length,
+            hiddenKeyPatternArray.length,
+            hiddenKeyPatternIntentArray.length);
+        featureHiddenMenuEnabled = false;
+      }
+    }
+  }
+
+  private static boolean handleKeyPattern(Context context, String input) {
+    int len = input.length();
+    if (len <= 3 || hiddenKeyPatternArray == null || hiddenKeyPatternIntentArray == null) {
+      return false;
+    }
+
+    for (int i = 0; i < hiddenKeyPatternArray.length; i++) {
+      if ((Pattern.compile(hiddenKeyPatternArray[i])).matcher(input).matches()) {
+        return sendIntent(context, input, hiddenKeyPatternIntentArray[i]);
+      }
+    }
+    return false;
+  }
+
+  private static boolean handleKeySequence(Context context, String input) {
+    int len = input.length();
+    if (len <= 3 || hiddenKeySequenceArray == null || hiddenKeySequenceIntentArray == null) {
+      return false;
+    }
+
+    for (int i = 0; i < hiddenKeySequenceArray.length; i++) {
+      if (hiddenKeySequenceArray[i].equals(input)) {
+        return sendIntent(context, input, hiddenKeySequenceIntentArray[i]);
+      }
+    }
+    return false;
+  }
+
+  private static boolean sendIntent(
+      final Context context, final String input, final String action) {
+    LogUtil.d("MotorolaHiddenMenuKeySequence.sendIntent", "input: %s", input);
+    try {
+      Intent intent = new Intent(action);
+      intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
+      intent.putExtra(EXTRA_HIDDEN_MENU_CODE, input);
+
+      ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent, 0);
+
+      if (resolveInfo != null
+          && resolveInfo.activityInfo != null
+          && resolveInfo.activityInfo.enabled) {
+        context.startActivity(intent);
+        return true;
+      } else {
+        LogUtil.w("MotorolaHiddenMenuKeySequence.sendIntent", "not able to resolve the intent");
+      }
+    } catch (ActivityNotFoundException e) {
+      LogUtil.e(
+          "MotorolaHiddenMenuKeySequence.sendIntent", "handleHiddenMenu Key Pattern Exception", e);
+    }
+    return false;
+  }
+}
diff --git a/java/com/android/dialer/oem/MotorolaUtils.java b/java/com/android/dialer/oem/MotorolaUtils.java
new file mode 100644
index 0000000..29bf0b2
--- /dev/null
+++ b/java/com/android/dialer/oem/MotorolaUtils.java
@@ -0,0 +1,51 @@
+package com.android.dialer.oem;
+
+import android.content.Context;
+import com.android.dialer.common.ConfigProviderBindings;
+
+/** Util class for Motorola OEM devices. */
+public class MotorolaUtils {
+
+  private static final String CONFIG_HD_CODEC_BLINKING_ICON_WHEN_CONNECTING_CALL_ENABLED =
+      "hd_codec_blinking_icon_when_connecting_enabled";
+  private static final String CONFIG_HD_CODEC_SHOW_ICON_IN_CALL_LOG_ENABLED =
+      "hd_codec_show_icon_in_call_log_enabled";
+
+  // This is used to check if a Motorola device supports HD voice call feature, which comes from
+  // system feature setting.
+  private static final String HD_CALL_FEATRURE = "com.motorola.software.sprint.hd_call";
+
+  // Feature flag indicates it's a HD call, currently this is only used by Motorola system build.
+  // TODO(b/35359461): Upstream and move it to android.provider.CallLog.
+  private static final int FEATURES_HD_CALL = 0x10000000;
+
+  public static boolean shouldBlinkHdIconWhenConnectingCall(Context context) {
+    return ConfigProviderBindings.get(context)
+            .getBoolean(CONFIG_HD_CODEC_BLINKING_ICON_WHEN_CONNECTING_CALL_ENABLED, true)
+        && isSupportingSprintHdCodec(context);
+  }
+
+  public static boolean shouldShowHdIconInCallLog(Context context, int features) {
+    return ConfigProviderBindings.get(context)
+            .getBoolean(CONFIG_HD_CODEC_SHOW_ICON_IN_CALL_LOG_ENABLED, true)
+        && isSupportingSprintHdCodec(context)
+        && (features & FEATURES_HD_CALL) == FEATURES_HD_CALL;
+  }
+
+  /**
+   * Handle special char sequence entered in dialpad. This may launch special intent based on input.
+   *
+   * @param context context
+   * @param input input string
+   * @return true if the input is consumed and the intent is launched
+   */
+  public static boolean handleSpecialCharSequence(Context context, String input) {
+    // TODO(b/35395377): Add check for Motorola devices.
+    return MotorolaHiddenMenuKeySequence.handleCharSequence(context, input);
+  }
+
+  private static boolean isSupportingSprintHdCodec(Context context) {
+    return context.getPackageManager().hasSystemFeature(HD_CALL_FEATRURE)
+        && context.getResources().getBoolean(R.bool.motorola_sprint_hd_codec);
+  }
+}
diff --git a/java/com/android/dialer/oem/res/values/motorola_config.xml b/java/com/android/dialer/oem/res/values/motorola_config.xml
new file mode 100644
index 0000000..f875d57
--- /dev/null
+++ b/java/com/android/dialer/oem/res/values/motorola_config.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <!-- Flag to control if HD codec is supported by Sprint. -->
+  <bool name="motorola_sprint_hd_codec">false</bool>
+
+  <!-- Hidden menu configuration for Motorola. -->
+  <!-- Flag to control if the Hidden Menu sequence will be supported by Sprint. -->
+  <bool name="motorola_feature_hidden_menu">false</bool>
+
+  <!-- This defines the specific key seuquence that will be catched in the SpecialCharSequenceMgr
+       such as, ##OMADM# -->
+  <string-array name="motorola_hidden_menu_key_sequence">
+    <item>##66236#</item>   <!--##OMADM#-->
+    <item>##2539#</item>    <!--##AKEY#-->
+    <item>##786#</item>     <!--##RTN#-->
+    <item>##72786#</item>   <!--##SCRTN#-->
+    <item>##3282#</item>    <!--##DATA#-->
+    <item>##33284#</item>   <!--##DEBUG#-->
+    <item>##3424#</item>    <!--##DIAG#-->
+    <item>##564#</item>     <!--##LOG#-->
+    <item>##4567257#</item> <!--##GLMSCLR#-->
+    <item>##873283#</item>  <!--##UPDATE#-->
+    <item>##6343#</item>    <!--##MEID#-->
+    <item>##27263#</item>   <!--##BRAND#-->
+    <item>##258#</item>     <!--##BLV#-->
+    <item>##8422#</item>    <!--##UICC#-->
+    <item>##4382#</item>    <!--CMAS/WEA-->
+  </string-array>
+
+  <string name="motorola_hidden_menu_intent">com.motorola.intent.action.LAUNCH_HIDDEN_MENU</string>
+
+  <!-- This defines the intents that will be send out when the key quence is matched, this must be
+       in the same order with he KeySequence array. -->
+  <string-array name="motorola_hidden_menu_key_sequence_intents">
+    <item>@string/motorola_hidden_menu_intent</item>
+    <item>@string/motorola_hidden_menu_intent</item>
+    <item>@string/motorola_hidden_menu_intent</item>
+    <item>@string/motorola_hidden_menu_intent</item>
+    <item>@string/motorola_hidden_menu_intent</item>
+    <item>@string/motorola_hidden_menu_intent</item>
+    <item>@string/motorola_hidden_menu_intent</item>
+    <item>@string/motorola_hidden_menu_intent</item>
+    <item>@string/motorola_hidden_menu_intent</item>
+    <item>com.motorola.android.intent.action.omadm.sprint.hfa</item>
+    <item>@string/motorola_hidden_menu_intent</item>
+    <item>@string/motorola_hidden_menu_intent</item>
+    <item>@string/motorola_hidden_menu_intent</item>
+    <item>@string/motorola_hidden_menu_intent</item>
+    <item>@string/motorola_hidden_menu_intent</item>
+  </string-array>
+
+  <!-- This defines the specific key patterns that will be catched in the SpecialCharSequenceMgr
+       such as, ##[0-9]{3,7}# -->
+  <string-array name="motorola_hidden_menu_key_pattern">
+    <!--##MSL#, here MSL is 6 digits SPC code, ##OTKSL#, OTKSL is also digits code -->
+    <item>##[0-9]{6}#</item>
+  </string-array>
+
+  <!-- This defines the intents that will be send out when the key quence is matched, this must be
+       in the same order with he KeyPattern array. -->
+  <string-array name="motorola_hidden_menu_key_pattern_intents">
+    <item>@string/motorola_hidden_menu_intent</item>
+  </string-array>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/p13n/inference/P13nRanking.java b/java/com/android/dialer/p13n/inference/P13nRanking.java
index 6bfc035..0682e85 100644
--- a/java/com/android/dialer/p13n/inference/P13nRanking.java
+++ b/java/com/android/dialer/p13n/inference/P13nRanking.java
@@ -22,6 +22,7 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
 import com.android.dialer.p13n.inference.protocol.P13nRanker;
 import com.android.dialer.p13n.inference.protocol.P13nRankerFactory;
 import java.util.List;
@@ -38,37 +39,51 @@
   public static P13nRanker get(@NonNull Context context) {
     Assert.isNotNull(context);
     Assert.isMainThread();
+
     if (ranker != null) {
       return ranker;
     }
 
+    if (!ConfigProviderBindings.get(context).getBoolean("p13n_ranker_should_enable", false)) {
+      setToIdentityRanker();
+      return ranker;
+    }
+
     Context application = context.getApplicationContext();
     if (application instanceof P13nRankerFactory) {
       ranker = ((P13nRankerFactory) application).newP13nRanker();
     }
 
     if (ranker == null) {
-      ranker =
-          new P13nRanker() {
-            @Override
-            public void refresh(@Nullable P13nRefreshCompleteListener listener) {}
-
-            @Override
-            public List<String> rankList(List<String> phoneNumbers) {
-              return phoneNumbers;
-            }
-
-            @NonNull
-            @Override
-            public Cursor rankCursor(
-                @NonNull Cursor phoneQueryResults, int phoneNumberColumnIndex) {
-              return phoneQueryResults;
-            }
-          };
+      setToIdentityRanker();
     }
     return ranker;
   }
 
+  private static void setToIdentityRanker() {
+    ranker =
+        new P13nRanker() {
+          @Override
+          public void refresh(@Nullable P13nRefreshCompleteListener listener) {}
+
+          @Override
+          public List<String> rankList(List<String> phoneNumbers) {
+            return phoneNumbers;
+          }
+
+          @NonNull
+          @Override
+          public Cursor rankCursor(@NonNull Cursor phoneQueryResults, int queryLength) {
+            return phoneQueryResults;
+          }
+
+          @Override
+          public boolean shouldShowEmptyListForNullQuery() {
+            return true;
+          }
+        };
+  }
+
   public static void setForTesting(@NonNull P13nRanker ranker) {
     P13nRanking.ranker = ranker;
   }
diff --git a/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java b/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java
index 9a859a6..41f1de4 100644
--- a/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java
+++ b/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java
@@ -41,13 +41,15 @@
    * input cursor is closed or invalid, or if any other error occurs in the ranking process.
    *
    * @param phoneQueryResults cursor of results of a Dialer search query
-   * @param phoneNumberColumnIndex column index of the phone number in the cursor data
+   * @param queryLength length of the search query that resulted in the cursor data, if below 0,
+   *     assumes no length is specified, thus applies the default behavior which is same as when
+   *     queryLength is greater than zero.
    * @return new cursor of data reordered by ranking (or reference to input cursor if order
    *     unchanged)
    */
   @NonNull
   @MainThread
-  Cursor rankCursor(@NonNull Cursor phoneQueryResults, int phoneNumberColumnIndex);
+  Cursor rankCursor(@NonNull Cursor phoneQueryResults, int queryLength);
 
   /**
    * Refreshes ranking cache (pulls fresh contextual features, pre-caches inference results, etc.).
@@ -61,6 +63,10 @@
   @MainThread
   void refresh(@Nullable P13nRefreshCompleteListener listener);
 
+  /** Decides if results should be displayed for no-query search. */
+  @MainThread
+  boolean shouldShowEmptyListForNullQuery();
+
   /**
    * Callback class for when ranking refresh has completed.
    *
diff --git a/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java b/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java
index 03b77b9..f443d56 100644
--- a/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java
+++ b/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java
@@ -18,7 +18,9 @@
 
 import android.content.Context;
 import android.net.Uri;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
 import java.io.InputStream;
 
 public interface CachedNumberLookupService {
@@ -35,6 +37,7 @@
    *     found in the cache, {@link ContactInfo#EMPTY} if the phone number was not found in the
    *     cache, and null if there was an error when querying the cache.
    */
+  @WorkerThread
   CachedContactInfo lookupCachedContactFromNumber(Context context, String number);
 
   void addContact(Context context, CachedContactInfo info);
@@ -64,6 +67,7 @@
     int SOURCE_TYPE_PROFILE = 4;
     int SOURCE_TYPE_CNAP = 5;
 
+    @NonNull
     ContactInfo getContactInfo();
 
     void setSource(int sourceType, String name, long directoryId);
diff --git a/java/com/android/dialer/postcall/AndroidManifest.xml b/java/com/android/dialer/postcall/AndroidManifest.xml
new file mode 100644
index 0000000..2bf07bc
--- /dev/null
+++ b/java/com/android/dialer/postcall/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~      http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<manifest
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.android.dialer.callcomposer">
+
+  <application>
+    <activity
+      android:name="com.android.dialer.postcall.PostCallActivity"
+      android:exported="false"
+      android:theme="@style/Theme.AppCompat.NoActionBar"
+      android:windowSoftInputMode="adjustResize"
+      android:screenOrientation="portrait"/>
+  </application>
+</manifest>
diff --git a/java/com/android/dialer/postcall/PostCall.java b/java/com/android/dialer/postcall/PostCall.java
new file mode 100644
index 0000000..cfe7c86
--- /dev/null
+++ b/java/com/android/dialer/postcall/PostCall.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.postcall;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.support.design.widget.BaseTransientBottomBar.BaseCallback;
+import android.support.design.widget.Snackbar;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.dialer.buildtype.BuildType;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProvider;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.IntentUtil;
+
+/** Helper class to handle all post call actions. */
+public class PostCall {
+
+  private static final String KEY_POST_CALL_CALL_CONNECT_TIME = "post_call_call_connect_time";
+  private static final String KEY_POST_CALL_CALL_DISCONNECT_TIME = "post_call_call_disconnect_time";
+  private static final String KEY_POST_CALL_CALL_NUMBER = "post_call_call_number";
+  private static final String KEY_POST_CALL_MESSAGE_SENT = "post_call_message_sent";
+
+  public static void promptUserForMessageIfNecessary(Activity activity, View rootView) {
+    if (isEnabled(activity)) {
+      if (shouldPromptUserToViewSentMessage(activity)) {
+        promptUserToViewSentMessage(activity, rootView);
+      } else if (shouldPromptUserToSendMessage(activity)) {
+        promptUserToSendMessage(activity, rootView);
+      }
+    }
+  }
+
+  private static void promptUserToSendMessage(Activity activity, View rootView) {
+    LogUtil.i("PostCall.promptUserToSendMessage", "returned from call, showing post call SnackBar");
+    String message = activity.getString(R.string.post_call_message);
+    String addMessage = activity.getString(R.string.post_call_add_message);
+    OnClickListener onClickListener =
+        v -> {
+          Logger.get(activity)
+              .logImpression(DialerImpression.Type.POST_CALL_PROMPT_USER_TO_SEND_MESSAGE_CLICKED);
+          activity.startActivity(PostCallActivity.newIntent(activity, getPhoneNumber(activity)));
+        };
+
+    Snackbar.make(rootView, message, Snackbar.LENGTH_INDEFINITE)
+        .setAction(addMessage, onClickListener)
+        .setActionTextColor(
+            activity.getResources().getColor(R.color.dialer_snackbar_action_text_color))
+        .show();
+    Logger.get(activity).logImpression(DialerImpression.Type.POST_CALL_PROMPT_USER_TO_SEND_MESSAGE);
+    PreferenceManager.getDefaultSharedPreferences(activity)
+        .edit()
+        .remove(KEY_POST_CALL_CALL_DISCONNECT_TIME)
+        .apply();
+  }
+
+  private static void promptUserToViewSentMessage(Activity activity, View rootView) {
+    LogUtil.i(
+        "PostCall.promptUserToViewSentMessage",
+        "returned from sending a post call message, message sent.");
+    String message = activity.getString(R.string.post_call_message_sent);
+    String addMessage = activity.getString(R.string.view);
+    OnClickListener onClickListener =
+        v -> {
+          Logger.get(activity)
+              .logImpression(
+                  DialerImpression.Type.POST_CALL_PROMPT_USER_TO_VIEW_SENT_MESSAGE_CLICKED);
+          Intent intent = IntentUtil.getSendSmsIntent(getPhoneNumber(activity));
+          DialerUtils.startActivityWithErrorToast(activity, intent);
+        };
+
+    Snackbar.make(rootView, message, Snackbar.LENGTH_INDEFINITE)
+        .setAction(addMessage, onClickListener)
+        .setActionTextColor(
+            activity.getResources().getColor(R.color.dialer_snackbar_action_text_color))
+        .addCallback(
+            new BaseCallback<Snackbar>() {
+              @Override
+              public void onDismissed(Snackbar snackbar, int i) {
+                super.onDismissed(snackbar, i);
+                clear(snackbar.getContext());
+              }
+            })
+        .show();
+    Logger.get(activity)
+        .logImpression(DialerImpression.Type.POST_CALL_PROMPT_USER_TO_VIEW_SENT_MESSAGE);
+    PreferenceManager.getDefaultSharedPreferences(activity)
+        .edit()
+        .remove(KEY_POST_CALL_MESSAGE_SENT)
+        .apply();
+  }
+
+  public static void onCallDisconnected(Context context, String number, long callConnectedMillis) {
+    PreferenceManager.getDefaultSharedPreferences(context)
+        .edit()
+        .putLong(KEY_POST_CALL_CALL_CONNECT_TIME, callConnectedMillis)
+        .putLong(KEY_POST_CALL_CALL_DISCONNECT_TIME, System.currentTimeMillis())
+        .putString(KEY_POST_CALL_CALL_NUMBER, number)
+        .apply();
+  }
+
+  public static void onMessageSent(Context context, String number) {
+    PreferenceManager.getDefaultSharedPreferences(context)
+        .edit()
+        .putString(KEY_POST_CALL_CALL_NUMBER, number)
+        .putBoolean(KEY_POST_CALL_MESSAGE_SENT, true)
+        .apply();
+  }
+
+  private static void clear(Context context) {
+    PreferenceManager.getDefaultSharedPreferences(context)
+        .edit()
+        .remove(KEY_POST_CALL_CALL_DISCONNECT_TIME)
+        .remove(KEY_POST_CALL_CALL_NUMBER)
+        .remove(KEY_POST_CALL_MESSAGE_SENT)
+        .remove(KEY_POST_CALL_CALL_CONNECT_TIME)
+        .apply();
+  }
+
+  private static boolean shouldPromptUserToSendMessage(Context context) {
+    SharedPreferences manager = PreferenceManager.getDefaultSharedPreferences(context);
+    long disconnectTimeMillis = manager.getLong(KEY_POST_CALL_CALL_DISCONNECT_TIME, -1);
+    long connectTimeMillis = manager.getLong(KEY_POST_CALL_CALL_CONNECT_TIME, -1);
+
+    long timeSinceDisconnect = System.currentTimeMillis() - disconnectTimeMillis;
+    long callDurationMillis = disconnectTimeMillis - connectTimeMillis;
+
+    ConfigProvider binding = ConfigProviderBindings.get(context);
+    return disconnectTimeMillis != -1
+        && connectTimeMillis != -1
+        && binding.getLong("postcall_last_call_threshold", 30_000) > timeSinceDisconnect
+        && binding.getLong("postcall_call_duration_threshold", 60_000) > callDurationMillis;
+  }
+
+  private static boolean shouldPromptUserToViewSentMessage(Context context) {
+    return PreferenceManager.getDefaultSharedPreferences(context)
+        .getBoolean(KEY_POST_CALL_MESSAGE_SENT, false);
+  }
+
+  private static String getPhoneNumber(Context context) {
+    return PreferenceManager.getDefaultSharedPreferences(context)
+        .getString(KEY_POST_CALL_CALL_NUMBER, null);
+  }
+
+  private static boolean isEnabled(Context context) {
+    @BuildType.Type int type = BuildType.get();
+    switch (type) {
+      case BuildType.BUGFOOD:
+      case BuildType.DOGFOOD:
+      case BuildType.FISHFOOD:
+      case BuildType.TEST:
+        return ConfigProviderBindings.get(context).getBoolean("enable_post_call", true);
+      case BuildType.RELEASE:
+        return ConfigProviderBindings.get(context).getBoolean("enable_post_call_prod", true);
+      default:
+        Assert.fail();
+        return false;
+    }
+  }
+}
diff --git a/java/com/android/dialer/postcall/PostCallActivity.java b/java/com/android/dialer/postcall/PostCallActivity.java
new file mode 100644
index 0000000..8da03dc
--- /dev/null
+++ b/java/com/android/dialer/postcall/PostCallActivity.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.postcall;
+
+import android.Manifest.permission;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.telephony.SmsManager;
+import android.widget.Toolbar;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.util.PermissionsUtil;
+import com.android.dialer.widget.MessageFragment;
+
+/** Activity used to send post call messages after a phone call. */
+public class PostCallActivity extends AppCompatActivity implements MessageFragment.Listener {
+
+  public static final String KEY_PHONE_NUMBER = "phone_number";
+  public static final String KEY_MESSAGE = "message";
+  private static final int REQUEST_CODE_SEND_SMS = 1;
+
+  private boolean useRcs;
+
+  public static Intent newIntent(@NonNull Context context, @NonNull String number) {
+    Intent intent = new Intent(Assert.isNotNull(context), PostCallActivity.class);
+    intent.putExtra(KEY_PHONE_NUMBER, Assert.isNotNull(number));
+    return intent;
+  }
+
+  @Override
+  protected void onCreate(@Nullable Bundle bundle) {
+    super.onCreate(bundle);
+    setContentView(R.layout.post_call_activity);
+
+    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+    toolbar.setTitle(getString(R.string.post_call_message));
+    toolbar.setNavigationOnClickListener(v -> finish());
+
+    useRcs = canUseRcs(getIntent().getStringExtra(KEY_PHONE_NUMBER));
+    LogUtil.i("PostCallActivity.onCreate", "useRcs: %b", useRcs);
+
+    int postCallCharLimit =
+        useRcs
+            ? getResources().getInteger(R.integer.post_call_char_limit)
+            : MessageFragment.NO_CHAR_LIMIT;
+    String[] messages =
+        new String[] {
+          getString(R.string.post_call_message_1),
+          getString(R.string.post_call_message_2),
+          getString(R.string.post_call_message_3)
+        };
+    MessageFragment fragment =
+        MessageFragment.builder()
+            .setCharLimit(postCallCharLimit)
+            .showSendIcon()
+            .setMessages(messages)
+            .build();
+    getSupportFragmentManager()
+        .beginTransaction()
+        .replace(R.id.message_container, fragment)
+        .commit();
+  }
+
+  private boolean canUseRcs(@NonNull String number) {
+    EnrichedCallCapabilities capabilities =
+        getEnrichedCallManager().getCapabilities(Assert.isNotNull(number));
+    LogUtil.i(
+        "PostCallActivity.canUseRcs",
+        "number: %s, capabilities: %s",
+        LogUtil.sanitizePhoneNumber(number),
+        capabilities);
+    return capabilities != null && capabilities.supportsPostCall();
+  }
+
+  @Override
+  public void onMessageFragmentSendMessage(@NonNull String message) {
+    String number = Assert.isNotNull(getIntent().getStringExtra(KEY_PHONE_NUMBER));
+    getIntent().putExtra(KEY_MESSAGE, message);
+
+    if (useRcs) {
+      LogUtil.i("PostCallActivity.onMessageFragmentSendMessage", "sending post call Rcs.");
+      getEnrichedCallManager().sendPostCallNote(number, message);
+      PostCall.onMessageSent(this, number);
+      finish();
+    } else if (PermissionsUtil.hasPermission(this, permission.SEND_SMS)) {
+      LogUtil.i("PostCallActivity.sendMessage", "Sending post call SMS.");
+      SmsManager smsManager = SmsManager.getDefault();
+      smsManager.sendMultipartTextMessage(
+          number, null, smsManager.divideMessage(message), null, null);
+      PostCall.onMessageSent(this, number);
+      finish();
+    } else if (PermissionsUtil.isFirstRequest(this, permission.SEND_SMS)
+        || shouldShowRequestPermissionRationale(permission.SEND_SMS)) {
+      LogUtil.i("PostCallActivity.sendMessage", "Requesting SMS_SEND permission.");
+      requestPermissions(new String[] {permission.SEND_SMS}, REQUEST_CODE_SEND_SMS);
+    } else {
+      LogUtil.i(
+          "PostCallActivity.sendMessage", "Permission permanently denied, sending to settings.");
+      Intent intent = new Intent(Intent.ACTION_VIEW);
+      intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+      intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+      intent.setData(Uri.parse("package:" + this.getPackageName()));
+      startActivity(intent);
+    }
+  }
+
+  @Override
+  public void onMessageFragmentAfterTextChange(String message) {}
+
+  @Override
+  public void onRequestPermissionsResult(
+      int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+    if (permissions.length > 0 && permissions[0].equals(permission.SEND_SMS)) {
+      PermissionsUtil.permissionRequested(this, permissions[0]);
+    }
+    if (requestCode == REQUEST_CODE_SEND_SMS
+        && grantResults.length > 0
+        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+      onMessageFragmentSendMessage(getIntent().getStringExtra(KEY_MESSAGE));
+    }
+  }
+
+  @NonNull
+  private EnrichedCallManager getEnrichedCallManager() {
+    return EnrichedCallComponent.get(this).getEnrichedCallManager();
+  }
+}
diff --git a/java/com/android/dialer/postcall/res/layout/post_call_activity.xml b/java/com/android/dialer/postcall/res/layout/post_call_activity.xml
new file mode 100644
index 0000000..6ea8126
--- /dev/null
+++ b/java/com/android/dialer/postcall/res/layout/post_call_activity.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:background="@color/background_dialer_white"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent">
+
+  <FrameLayout
+    android:id="@+id/message_container"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_alignParentBottom="true"
+    android:background="@color/background_dialer_white"/>
+
+  <Toolbar
+    android:id="@+id/toolbar"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?attr/actionBarSize"
+    android:titleTextAppearance="@style/toolbar_title_text"
+    android:subtitleTextAppearance="@style/toolbar_subtitle_text"
+    android:navigationIcon="@drawable/quantum_ic_close_white_24"
+    android:background="@color/dialer_theme_color"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/postcall/res/values/strings.xml b/java/com/android/dialer/postcall/res/values/strings.xml
new file mode 100644
index 0000000..d5e085a
--- /dev/null
+++ b/java/com/android/dialer/postcall/res/values/strings.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<resources>
+  <!-- Shown as a message that notifies asks the user if they want to send a post call message -->
+  <string name="post_call_message">Say why you called</string>
+  <!-- Premade message to be sent as a text/RCS message -->
+  <string name="post_call_message_1">This is urgent. Call me back.</string>
+  <!-- Premade message to be sent as a text/RCS message -->
+  <string name="post_call_message_2">Call me back when you have some time.</string>
+  <!-- Premade message to be sent as a text/RCS message -->
+  <string name="post_call_message_3">Not urgent, we can chat later.</string>
+  <!-- Asks the user if they want to send a post call message -->
+  <string name="post_call_add_message">Add message</string>
+  <!-- Shown to let the user know that their message was sent. -->
+  <string name="post_call_message_sent">Message sent</string>
+  <string name="view">View</string>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/postcall/res/values/values.xml b/java/com/android/dialer/postcall/res/values/values.xml
new file mode 100644
index 0000000..64fe9f6
--- /dev/null
+++ b/java/com/android/dialer/postcall/res/values/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<resources>
+  <integer name="post_call_char_limit">60</integer>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/proguard/proguard.flags b/java/com/android/dialer/proguard/proguard.flags
new file mode 100644
index 0000000..0f684a0
--- /dev/null
+++ b/java/com/android/dialer/proguard/proguard.flags
@@ -0,0 +1,6 @@
+# Keep the annotation, classes, methods, and fields marked as UsedByReflection
+-keep class com.android.dialer.proguard.UsedByReflection
+-keep @com.android.dialer.proguard.UsedByReflection class *
+-keepclassmembers class * {
+    @com.android.dialer.proguard.UsedByReflection *;
+}
diff --git a/java/com/android/dialer/proguard/proguard_base.flags b/java/com/android/dialer/proguard/proguard_base.flags
new file mode 100644
index 0000000..7b5794e
--- /dev/null
+++ b/java/com/android/dialer/proguard/proguard_base.flags
@@ -0,0 +1,74 @@
+# Copied from http://google3/java/com/google/android/apps/common/proguard/base.flags
+
+# This file is intended to contain proguard options that *nobody* would ever
+# not want, in *any* configuration - they ensure basic correctness, and have
+# no downsides. You probably do not want to make changes to this file.
+
+# The presence of both of these attributes causes dalvik and other jvms to print
+# stack traces on uncaught exceptions, which is necessary to get useful crash
+# reports.
+-keepattributes SourceFile,LineNumberTable
+
+# Preverification was introduced in Java 6 to enable faster classloading, but
+# dex doesn't use the java .class format, so it has no benefit and can cause
+# problems.
+-dontpreverify
+
+# Skipping analysis of some classes may make proguard strip something that's
+# needed.
+-dontskipnonpubliclibraryclasses
+
+# Case-insensitive filesystems can't handle when a.class and A.class exist in
+# the same directory.
+-dontusemixedcaseclassnames
+
+# This prevents the names of native methods from being obfuscated and prevents
+# UnsatisfiedLinkErrors.
+-keepclasseswithmembernames class * {
+    native <methods>;
+}
+
+# hackbod discourages the use of enums on android, but if you use them, they
+# should work. Allow instantiation via reflection by keeping the values method.
+-keepclassmembers enum * {
+    public static **[] values();
+}
+
+# Parcel reflectively accesses this field.
+-keepclassmembers class * implements android.os.Parcelable {
+  public static *** CREATOR;
+}
+
+# These methods are needed to ensure that serialization behaves as expected when
+# classes are obfuscated, shrunk, and/or optimized.
+-keepclassmembers class * implements java.io.Serializable {
+    static final long serialVersionUID;
+    private static final java.io.ObjectStreamField[] serialPersistentFields;
+    private void writeObject(java.io.ObjectOutputStream);
+    private void readObject(java.io.ObjectInputStream);
+    java.lang.Object writeReplace();
+    java.lang.Object readResolve();
+}
+
+# Don't warn about Guava. Any Guava-using app will fail the proguard stage without this dontwarn,
+# and since Guava is so widely used, we include it here in the base.
+-dontwarn com.google.common.**
+
+# Don't warn about Error Prone annotations (e.g. @CompileTimeConstant)
+-dontwarn com.google.errorprone.annotations.**
+
+# Based on http://ag/718466: android.app.Notification.setLatestEventInfo() was
+# removed in MNC, but is still referenced (safely) by the NotificationCompat
+# code.
+-dontwarn android.app.Notification
+
+# Silence notes about dynamically referenced classes from AOSP support
+# libraries.
+-dontnote android.graphics.Insets
+
+# AOSP support library:  ICU references to gender and plurals messages.
+-dontnote libcore.icu.ICU
+-keep class libcore.icu.ICU { *** get(...);}
+
+# AOSP support library:  Handle classes that use reflection.
+-dontnote android.support.v4.app.NotificationCompatJellybean
diff --git a/java/com/android/dialer/proguard/proguard_release.flags b/java/com/android/dialer/proguard/proguard_release.flags
new file mode 100644
index 0000000..1c845cf
--- /dev/null
+++ b/java/com/android/dialer/proguard/proguard_release.flags
@@ -0,0 +1,24 @@
+# Copied from http://google3/java/com/google/android/apps/common/proguard/release.flags
+
+# Used for building release binaries. Obfuscates, optimizes, and shrinks.
+
+# By default, proguard leaves all classes in their original package, which
+# needlessly repeats com.google.android.apps.etc.
+-repackageclasses ""
+
+# Allows proguard to make private and protected methods and fields public as
+# part of optimization. This lets proguard inline trivial getter/setter methods.
+-allowaccessmodification
+
+# The source file attribute must be present in order to print stack traces, but
+# we rename it in order to avoid leaking the pre-obfuscation class name.
+-renamesourcefileattribute PG
+
+# This allows proguard to strip isLoggable() blocks containing only debug log
+# code from release builds.
+-assumenosideeffects class android.util.Log {
+  static *** i(...);
+  static *** d(...);
+  static *** v(...);
+  static *** isLoggable(...);
+}
diff --git a/java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java b/java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java
deleted file mode 100644
index ef995c8..0000000
--- a/java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.dialer.shortcuts;
-
-import android.support.annotation.NonNull;
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_DialerShortcut extends DialerShortcut {
-
-  private final long contactId;
-  private final String lookupKey;
-  private final String displayName;
-  private final int rank;
-
-  private AutoValue_DialerShortcut(
-      long contactId,
-      String lookupKey,
-      String displayName,
-      int rank) {
-    this.contactId = contactId;
-    this.lookupKey = lookupKey;
-    this.displayName = displayName;
-    this.rank = rank;
-  }
-
-  @Override
-  long getContactId() {
-    return contactId;
-  }
-
-  @NonNull
-  @Override
-  String getLookupKey() {
-    return lookupKey;
-  }
-
-  @NonNull
-  @Override
-  String getDisplayName() {
-    return displayName;
-  }
-
-  @Override
-  int getRank() {
-    return rank;
-  }
-
-  @Override
-  public String toString() {
-    return "DialerShortcut{"
-        + "contactId=" + contactId + ", "
-        + "lookupKey=" + lookupKey + ", "
-        + "displayName=" + displayName + ", "
-        + "rank=" + rank
-        + "}";
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o == this) {
-      return true;
-    }
-    if (o instanceof DialerShortcut) {
-      DialerShortcut that = (DialerShortcut) o;
-      return (this.contactId == that.getContactId())
-           && (this.lookupKey.equals(that.getLookupKey()))
-           && (this.displayName.equals(that.getDisplayName()))
-           && (this.rank == that.getRank());
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    int h = 1;
-    h *= 1000003;
-    h ^= (this.contactId >>> 32) ^ this.contactId;
-    h *= 1000003;
-    h ^= this.lookupKey.hashCode();
-    h *= 1000003;
-    h ^= this.displayName.hashCode();
-    h *= 1000003;
-    h ^= this.rank;
-    return h;
-  }
-
-  static final class Builder extends DialerShortcut.Builder {
-    private Long contactId;
-    private String lookupKey;
-    private String displayName;
-    private Integer rank;
-    Builder() {
-    }
-    private Builder(DialerShortcut source) {
-      this.contactId = source.getContactId();
-      this.lookupKey = source.getLookupKey();
-      this.displayName = source.getDisplayName();
-      this.rank = source.getRank();
-    }
-    @Override
-    DialerShortcut.Builder setContactId(long contactId) {
-      this.contactId = contactId;
-      return this;
-    }
-    @Override
-    DialerShortcut.Builder setLookupKey(String lookupKey) {
-      this.lookupKey = lookupKey;
-      return this;
-    }
-    @Override
-    DialerShortcut.Builder setDisplayName(String displayName) {
-      this.displayName = displayName;
-      return this;
-    }
-    @Override
-    DialerShortcut.Builder setRank(int rank) {
-      this.rank = rank;
-      return this;
-    }
-    @Override
-    DialerShortcut build() {
-      String missing = "";
-      if (this.contactId == null) {
-        missing += " contactId";
-      }
-      if (this.lookupKey == null) {
-        missing += " lookupKey";
-      }
-      if (this.displayName == null) {
-        missing += " displayName";
-      }
-      if (this.rank == null) {
-        missing += " rank";
-      }
-      if (!missing.isEmpty()) {
-        throw new IllegalStateException("Missing required properties:" + missing);
-      }
-      return new AutoValue_DialerShortcut(
-          this.contactId,
-          this.lookupKey,
-          this.displayName,
-          this.rank);
-    }
-  }
-
-}
\ No newline at end of file
diff --git a/java/com/android/dialer/shortcuts/CallContactActivity.java b/java/com/android/dialer/shortcuts/CallContactActivity.java
index 1e9a01b..40bf97b 100644
--- a/java/com/android/dialer/shortcuts/CallContactActivity.java
+++ b/java/com/android/dialer/shortcuts/CallContactActivity.java
@@ -56,11 +56,20 @@
     }
   }
 
+  /**
+   * Attempt to make a call, finishing the activity if the required permissions are already granted.
+   * If the required permissions are not already granted, the activity is not finished so that the
+   * user can choose to grant or deny them.
+   */
   private void makeCall() {
     CallSpecificAppData callSpecificAppData = new CallSpecificAppData();
     callSpecificAppData.callInitiationType = CallInitiationType.Type.LAUNCHER_SHORTCUT;
-    PhoneNumberInteraction.startInteractionForPhoneCall(
-        this, contactUri, false /* isVideoCall */, callSpecificAppData);
+    boolean interactionStarted =
+        PhoneNumberInteraction.startInteractionForPhoneCall(
+            this, contactUri, false /* isVideoCall */, callSpecificAppData);
+    if (interactionStarted) {
+      finish();
+    }
   }
 
   @Override
@@ -115,6 +124,7 @@
       int requestCode, String[] permissions, int[] grantResults) {
     switch (requestCode) {
       case PhoneNumberInteraction.REQUEST_READ_CONTACTS:
+      case PhoneNumberInteraction.REQUEST_CALL_PHONE:
         {
           // If request is cancelled, the result arrays are empty.
           if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
@@ -122,8 +132,8 @@
           } else {
             Toast.makeText(this, R.string.dialer_shortcut_no_permissions, Toast.LENGTH_SHORT)
                 .show();
+            finish();
           }
-          finish();
           break;
         }
       default:
diff --git a/java/com/android/dialer/shortcuts/DialerShortcut.java b/java/com/android/dialer/shortcuts/DialerShortcut.java
index f2fb330..a8d4204 100644
--- a/java/com/android/dialer/shortcuts/DialerShortcut.java
+++ b/java/com/android/dialer/shortcuts/DialerShortcut.java
@@ -22,7 +22,7 @@
 import android.os.Build.VERSION_CODES;
 import android.provider.ContactsContract.Contacts;
 import android.support.annotation.NonNull;
-
+import com.google.auto.value.AutoValue;
 
 /**
  * Convenience data structure.
@@ -31,7 +31,7 @@
  * convenience methods for doing things like constructing labels.
  */
 @TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1
-
+@AutoValue
 abstract class DialerShortcut {
 
   /** Marker value indicates that shortcut has no setRank. Used by pinned shortcuts. */
@@ -160,7 +160,7 @@
     return new AutoValue_DialerShortcut.Builder().setRank(NO_RANK);
   }
 
-
+  @AutoValue.Builder
   abstract static class Builder {
 
     /**
diff --git a/java/com/android/dialer/shortcuts/res/values/strings.xml b/java/com/android/dialer/shortcuts/res/values/strings.xml
index 1e2c87f..5f14a81 100644
--- a/java/com/android/dialer/shortcuts/res/values/strings.xml
+++ b/java/com/android/dialer/shortcuts/res/values/strings.xml
@@ -30,8 +30,8 @@
        be found or doesn't have any phone numbers. [CHAR LIMIT=70] -->
   <string name="dialer_shortcut_contact_not_found_or_has_no_number">Contact no longer available.</string>
 
-  <!-- Error message to display when a tapping a shortcut fails because contact permissions are
-       missing. [CHAR LIMIT=70] -->
-  <string name="dialer_shortcut_no_permissions">Cannot call without contact permissions.</string>
+  <!-- Error message to display when a tapping a shortcut fails because permissions are missing.
+       [CHAR LIMIT=70] -->
+  <string name="dialer_shortcut_no_permissions">Cannot call without permissions.</string>
 
 </resources>
diff --git a/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml b/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml
index 5e8f58d..49149e3 100644
--- a/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml
+++ b/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml
@@ -24,8 +24,6 @@
 
     <intent
       android:action="android.intent.action.INSERT"
-      android:data="content://com.android.contacts/contacts"
-      android:targetPackage="com.google.android.contacts"
-      android:targetClass="com.android.contacts.activities.CompactContactEditorActivity"/>
+      android:data="content://com.android.contacts/contacts"/>
   </shortcut>
 </shortcuts>
diff --git a/java/com/android/dialer/simulator/SimulatorComponent.java b/java/com/android/dialer/simulator/SimulatorComponent.java
new file mode 100644
index 0000000..a16592e
--- /dev/null
+++ b/java/com/android/dialer/simulator/SimulatorComponent.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.simulator;
+
+import android.content.Context;
+import dagger.Subcomponent;
+import com.android.dialer.simulator.impl.SimulatorImpl;
+
+/** Subcomponent that can be used to access the simulator implementation. */
+public class SimulatorComponent {
+  private static SimulatorComponent instance;
+  private Simulator simulator;
+
+  public Simulator getSimulator() {
+    if (simulator == null) {
+        simulator = new SimulatorImpl();
+    }
+    return simulator;
+  }
+
+  public static SimulatorComponent get(Context context) {
+    if (instance == null) {
+        instance = new SimulatorComponent();
+    }
+    return instance;
+  }
+
+  /** Used to refer to the root application component. */
+  public interface HasComponent {
+    SimulatorComponent simulatorComponent();
+  }
+}
diff --git a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java
deleted file mode 100644
index 5918198..0000000
--- a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.simulator.impl;
-
-import android.support.annotation.NonNull;
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_SimulatorCallLog_CallEntry extends SimulatorCallLog.CallEntry {
-
-  private final String number;
-  private final int type;
-  private final int presentation;
-  private final long timeMillis;
-
-  private AutoValue_SimulatorCallLog_CallEntry(
-      String number,
-      int type,
-      int presentation,
-      long timeMillis) {
-    this.number = number;
-    this.type = type;
-    this.presentation = presentation;
-    this.timeMillis = timeMillis;
-  }
-
-  @NonNull
-  @Override
-  String getNumber() {
-    return number;
-  }
-
-  @Override
-  int getType() {
-    return type;
-  }
-
-  @Override
-  int getPresentation() {
-    return presentation;
-  }
-
-  @Override
-  long getTimeMillis() {
-    return timeMillis;
-  }
-
-  @Override
-  public String toString() {
-    return "CallEntry{"
-        + "number=" + number + ", "
-        + "type=" + type + ", "
-        + "presentation=" + presentation + ", "
-        + "timeMillis=" + timeMillis
-        + "}";
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o == this) {
-      return true;
-    }
-    if (o instanceof SimulatorCallLog.CallEntry) {
-      SimulatorCallLog.CallEntry that = (SimulatorCallLog.CallEntry) o;
-      return (this.number.equals(that.getNumber()))
-           && (this.type == that.getType())
-           && (this.presentation == that.getPresentation())
-           && (this.timeMillis == that.getTimeMillis());
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    int h = 1;
-    h *= 1000003;
-    h ^= this.number.hashCode();
-    h *= 1000003;
-    h ^= this.type;
-    h *= 1000003;
-    h ^= this.presentation;
-    h *= 1000003;
-    h ^= (this.timeMillis >>> 32) ^ this.timeMillis;
-    return h;
-  }
-
-  static final class Builder extends SimulatorCallLog.CallEntry.Builder {
-    private String number;
-    private Integer type;
-    private Integer presentation;
-    private Long timeMillis;
-    Builder() {
-    }
-    private Builder(SimulatorCallLog.CallEntry source) {
-      this.number = source.getNumber();
-      this.type = source.getType();
-      this.presentation = source.getPresentation();
-      this.timeMillis = source.getTimeMillis();
-    }
-    @Override
-    SimulatorCallLog.CallEntry.Builder setNumber(String number) {
-      this.number = number;
-      return this;
-    }
-    @Override
-    SimulatorCallLog.CallEntry.Builder setType(int type) {
-      this.type = type;
-      return this;
-    }
-    @Override
-    SimulatorCallLog.CallEntry.Builder setPresentation(int presentation) {
-      this.presentation = presentation;
-      return this;
-    }
-    @Override
-    SimulatorCallLog.CallEntry.Builder setTimeMillis(long timeMillis) {
-      this.timeMillis = timeMillis;
-      return this;
-    }
-    @Override
-    SimulatorCallLog.CallEntry build() {
-      String missing = "";
-      if (this.number == null) {
-        missing += " number";
-      }
-      if (this.type == null) {
-        missing += " type";
-      }
-      if (this.presentation == null) {
-        missing += " presentation";
-      }
-      if (this.timeMillis == null) {
-        missing += " timeMillis";
-      }
-      if (!missing.isEmpty()) {
-        throw new IllegalStateException("Missing required properties:" + missing);
-      }
-      return new AutoValue_SimulatorCallLog_CallEntry(
-          this.number,
-          this.type,
-          this.presentation,
-          this.timeMillis);
-    }
-  }
-
-}
diff --git a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java
deleted file mode 100644
index 00295f3..0000000
--- a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.simulator.impl;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import java.io.ByteArrayOutputStream;
-import java.util.List;
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_SimulatorContacts_Contact extends SimulatorContacts.Contact {
-
-  private final String accountType;
-  private final String accountName;
-  private final String name;
-  private final boolean isStarred;
-  private final ByteArrayOutputStream photoStream;
-  private final List<SimulatorContacts.PhoneNumber> phoneNumbers;
-  private final List<SimulatorContacts.Email> emails;
-
-  private AutoValue_SimulatorContacts_Contact(
-      String accountType,
-      String accountName,
-      @Nullable String name,
-      boolean isStarred,
-      @Nullable ByteArrayOutputStream photoStream,
-      List<SimulatorContacts.PhoneNumber> phoneNumbers,
-      List<SimulatorContacts.Email> emails) {
-    this.accountType = accountType;
-    this.accountName = accountName;
-    this.name = name;
-    this.isStarred = isStarred;
-    this.photoStream = photoStream;
-    this.phoneNumbers = phoneNumbers;
-    this.emails = emails;
-  }
-
-  @NonNull
-  @Override
-  String getAccountType() {
-    return accountType;
-  }
-
-  @NonNull
-  @Override
-  String getAccountName() {
-    return accountName;
-  }
-
-  @Nullable
-  @Override
-  String getName() {
-    return name;
-  }
-
-  @Override
-  boolean getIsStarred() {
-    return isStarred;
-  }
-
-  @Nullable
-  @Override
-  ByteArrayOutputStream getPhotoStream() {
-    return photoStream;
-  }
-
-  @NonNull
-  @Override
-  List<SimulatorContacts.PhoneNumber> getPhoneNumbers() {
-    return phoneNumbers;
-  }
-
-  @NonNull
-  @Override
-  List<SimulatorContacts.Email> getEmails() {
-    return emails;
-  }
-
-  @Override
-  public String toString() {
-    return "Contact{"
-        + "accountType=" + accountType + ", "
-        + "accountName=" + accountName + ", "
-        + "name=" + name + ", "
-        + "isStarred=" + isStarred + ", "
-        + "photoStream=" + photoStream + ", "
-        + "phoneNumbers=" + phoneNumbers + ", "
-        + "emails=" + emails
-        + "}";
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o == this) {
-      return true;
-    }
-    if (o instanceof SimulatorContacts.Contact) {
-      SimulatorContacts.Contact that = (SimulatorContacts.Contact) o;
-      return (this.accountType.equals(that.getAccountType()))
-           && (this.accountName.equals(that.getAccountName()))
-           && ((this.name == null) ? (that.getName() == null) : this.name.equals(that.getName()))
-           && (this.isStarred == that.getIsStarred())
-           && ((this.photoStream == null) ? (that.getPhotoStream() == null) : this.photoStream.equals(that.getPhotoStream()))
-           && (this.phoneNumbers.equals(that.getPhoneNumbers()))
-           && (this.emails.equals(that.getEmails()));
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    int h = 1;
-    h *= 1000003;
-    h ^= this.accountType.hashCode();
-    h *= 1000003;
-    h ^= this.accountName.hashCode();
-    h *= 1000003;
-    h ^= (name == null) ? 0 : this.name.hashCode();
-    h *= 1000003;
-    h ^= this.isStarred ? 1231 : 1237;
-    h *= 1000003;
-    h ^= (photoStream == null) ? 0 : this.photoStream.hashCode();
-    h *= 1000003;
-    h ^= this.phoneNumbers.hashCode();
-    h *= 1000003;
-    h ^= this.emails.hashCode();
-    return h;
-  }
-
-  static final class Builder extends SimulatorContacts.Contact.Builder {
-    private String accountType;
-    private String accountName;
-    private String name;
-    private Boolean isStarred;
-    private ByteArrayOutputStream photoStream;
-    private List<SimulatorContacts.PhoneNumber> phoneNumbers;
-    private List<SimulatorContacts.Email> emails;
-    Builder() {
-    }
-    private Builder(SimulatorContacts.Contact source) {
-      this.accountType = source.getAccountType();
-      this.accountName = source.getAccountName();
-      this.name = source.getName();
-      this.isStarred = source.getIsStarred();
-      this.photoStream = source.getPhotoStream();
-      this.phoneNumbers = source.getPhoneNumbers();
-      this.emails = source.getEmails();
-    }
-    @Override
-    SimulatorContacts.Contact.Builder setAccountType(String accountType) {
-      this.accountType = accountType;
-      return this;
-    }
-    @Override
-    SimulatorContacts.Contact.Builder setAccountName(String accountName) {
-      this.accountName = accountName;
-      return this;
-    }
-    @Override
-    SimulatorContacts.Contact.Builder setName(@Nullable String name) {
-      this.name = name;
-      return this;
-    }
-    @Override
-    SimulatorContacts.Contact.Builder setIsStarred(boolean isStarred) {
-      this.isStarred = isStarred;
-      return this;
-    }
-    @Override
-    SimulatorContacts.Contact.Builder setPhotoStream(@Nullable ByteArrayOutputStream photoStream) {
-      this.photoStream = photoStream;
-      return this;
-    }
-    @Override
-    SimulatorContacts.Contact.Builder setPhoneNumbers(List<SimulatorContacts.PhoneNumber> phoneNumbers) {
-      this.phoneNumbers = phoneNumbers;
-      return this;
-    }
-    @Override
-    SimulatorContacts.Contact.Builder setEmails(List<SimulatorContacts.Email> emails) {
-      this.emails = emails;
-      return this;
-    }
-    @Override
-    SimulatorContacts.Contact build() {
-      String missing = "";
-      if (this.accountType == null) {
-        missing += " accountType";
-      }
-      if (this.accountName == null) {
-        missing += " accountName";
-      }
-      if (this.isStarred == null) {
-        missing += " isStarred";
-      }
-      if (this.phoneNumbers == null) {
-        missing += " phoneNumbers";
-      }
-      if (this.emails == null) {
-        missing += " emails";
-      }
-      if (!missing.isEmpty()) {
-        throw new IllegalStateException("Missing required properties:" + missing);
-      }
-      return new AutoValue_SimulatorContacts_Contact(
-          this.accountType,
-          this.accountName,
-          this.name,
-          this.isStarred,
-          this.photoStream,
-          this.phoneNumbers,
-          this.emails);
-    }
-  }
-
-}
diff --git a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java
deleted file mode 100644
index 5893480..0000000
--- a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.dialer.simulator.impl;
-
-import android.support.annotation.NonNull;
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_SimulatorVoicemail_Voicemail extends SimulatorVoicemail.Voicemail {
-
-  private final String phoneNumber;
-  private final String transcription;
-  private final long durationSeconds;
-  private final long timeMillis;
-  private final boolean isRead;
-
-  private AutoValue_SimulatorVoicemail_Voicemail(
-      String phoneNumber,
-      String transcription,
-      long durationSeconds,
-      long timeMillis,
-      boolean isRead) {
-    this.phoneNumber = phoneNumber;
-    this.transcription = transcription;
-    this.durationSeconds = durationSeconds;
-    this.timeMillis = timeMillis;
-    this.isRead = isRead;
-  }
-
-  @NonNull
-  @Override
-  String getPhoneNumber() {
-    return phoneNumber;
-  }
-
-  @NonNull
-  @Override
-  String getTranscription() {
-    return transcription;
-  }
-
-  @Override
-  long getDurationSeconds() {
-    return durationSeconds;
-  }
-
-  @Override
-  long getTimeMillis() {
-    return timeMillis;
-  }
-
-  @Override
-  boolean getIsRead() {
-    return isRead;
-  }
-
-  @Override
-  public String toString() {
-    return "Voicemail{"
-        + "phoneNumber=" + phoneNumber + ", "
-        + "transcription=" + transcription + ", "
-        + "durationSeconds=" + durationSeconds + ", "
-        + "timeMillis=" + timeMillis + ", "
-        + "isRead=" + isRead
-        + "}";
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o == this) {
-      return true;
-    }
-    if (o instanceof SimulatorVoicemail.Voicemail) {
-      SimulatorVoicemail.Voicemail that = (SimulatorVoicemail.Voicemail) o;
-      return (this.phoneNumber.equals(that.getPhoneNumber()))
-           && (this.transcription.equals(that.getTranscription()))
-           && (this.durationSeconds == that.getDurationSeconds())
-           && (this.timeMillis == that.getTimeMillis())
-           && (this.isRead == that.getIsRead());
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    int h = 1;
-    h *= 1000003;
-    h ^= this.phoneNumber.hashCode();
-    h *= 1000003;
-    h ^= this.transcription.hashCode();
-    h *= 1000003;
-    h ^= (this.durationSeconds >>> 32) ^ this.durationSeconds;
-    h *= 1000003;
-    h ^= (this.timeMillis >>> 32) ^ this.timeMillis;
-    h *= 1000003;
-    h ^= this.isRead ? 1231 : 1237;
-    return h;
-  }
-
-  static final class Builder extends SimulatorVoicemail.Voicemail.Builder {
-    private String phoneNumber;
-    private String transcription;
-    private Long durationSeconds;
-    private Long timeMillis;
-    private Boolean isRead;
-    Builder() {
-    }
-    private Builder(SimulatorVoicemail.Voicemail source) {
-      this.phoneNumber = source.getPhoneNumber();
-      this.transcription = source.getTranscription();
-      this.durationSeconds = source.getDurationSeconds();
-      this.timeMillis = source.getTimeMillis();
-      this.isRead = source.getIsRead();
-    }
-    @Override
-    SimulatorVoicemail.Voicemail.Builder setPhoneNumber(String phoneNumber) {
-      this.phoneNumber = phoneNumber;
-      return this;
-    }
-    @Override
-    SimulatorVoicemail.Voicemail.Builder setTranscription(String transcription) {
-      this.transcription = transcription;
-      return this;
-    }
-    @Override
-    SimulatorVoicemail.Voicemail.Builder setDurationSeconds(long durationSeconds) {
-      this.durationSeconds = durationSeconds;
-      return this;
-    }
-    @Override
-    SimulatorVoicemail.Voicemail.Builder setTimeMillis(long timeMillis) {
-      this.timeMillis = timeMillis;
-      return this;
-    }
-    @Override
-    SimulatorVoicemail.Voicemail.Builder setIsRead(boolean isRead) {
-      this.isRead = isRead;
-      return this;
-    }
-    @Override
-    SimulatorVoicemail.Voicemail build() {
-      String missing = "";
-      if (this.phoneNumber == null) {
-        missing += " phoneNumber";
-      }
-      if (this.transcription == null) {
-        missing += " transcription";
-      }
-      if (this.durationSeconds == null) {
-        missing += " durationSeconds";
-      }
-      if (this.timeMillis == null) {
-        missing += " timeMillis";
-      }
-      if (this.isRead == null) {
-        missing += " isRead";
-      }
-      if (!missing.isEmpty()) {
-        throw new IllegalStateException("Missing required properties:" + missing);
-      }
-      return new AutoValue_SimulatorVoicemail_Voicemail(
-          this.phoneNumber,
-          this.transcription,
-          this.durationSeconds,
-          this.timeMillis,
-          this.isRead);
-    }
-  }
-
-}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorCallLog.java b/java/com/android/dialer/simulator/impl/SimulatorCallLog.java
index 9ace047..f127d56 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorCallLog.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorCallLog.java
@@ -26,7 +26,7 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.WorkerThread;
 import com.android.dialer.common.Assert;
-
+import com.google.auto.value.AutoValue;
 import java.util.ArrayList;
 import java.util.concurrent.TimeUnit;
 
@@ -96,7 +96,7 @@
     }
   }
 
-
+  @AutoValue
   abstract static class CallEntry {
     @NonNull
     abstract String getNumber();
@@ -121,7 +121,7 @@
       return values;
     }
 
-
+    @AutoValue.Builder
     abstract static class Builder {
       abstract Builder setNumber(@NonNull String number);
 
diff --git a/java/com/android/dialer/simulator/impl/SimulatorContacts.java b/java/com/android/dialer/simulator/impl/SimulatorContacts.java
index 8931509..c5e25b3 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorContacts.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorContacts.java
@@ -31,7 +31,7 @@
 import android.support.annotation.WorkerThread;
 import android.text.TextUtils;
 import com.android.dialer.common.Assert;
-
+import com.google.auto.value.AutoValue;
 import java.io.ByteArrayOutputStream;
 import java.util.ArrayList;
 import java.util.List;
@@ -190,7 +190,7 @@
     }
   }
 
-
+  @AutoValue
   abstract static class Contact {
     @NonNull
     abstract String getAccountType();
@@ -221,7 +221,7 @@
           .setEmails(new ArrayList<>());
     }
 
-
+    @AutoValue.Builder
     abstract static class Builder {
       @NonNull private final List<PhoneNumber> phoneNumbers = new ArrayList<>();
       @NonNull private final List<Email> emails = new ArrayList<>();
diff --git a/java/com/android/dialer/simulator/impl/SimulatorImpl.java b/java/com/android/dialer/simulator/impl/SimulatorImpl.java
new file mode 100644
index 0000000..9c68269
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorImpl.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.simulator.impl;
+
+import android.content.Context;
+import android.view.ActionProvider;
+import com.android.dialer.buildtype.BuildType;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.simulator.Simulator;
+import javax.inject.Inject;
+
+/** The entry point for the simulator feature. */
+final public class SimulatorImpl implements Simulator {
+  @Inject
+  public SimulatorImpl() {}
+
+  @Override
+  public boolean shouldShow() {
+    return BuildType.get() == BuildType.BUGFOOD || LogUtil.isDebugEnabled();
+  }
+
+  @Override
+  public ActionProvider getActionProvider(Context context) {
+    return new SimulatorActionProvider(context);
+  }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorModule.java b/java/com/android/dialer/simulator/impl/SimulatorModule.java
index 0f8ad39..c0cca27 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorModule.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorModule.java
@@ -16,19 +16,15 @@
 
 package com.android.dialer.simulator.impl;
 
-import android.content.Context;
-import android.view.ActionProvider;
 import com.android.dialer.simulator.Simulator;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Singleton;
 
-/** The entry point for the simulator module. */
-public final class SimulatorModule implements Simulator {
-  @Override
-  public boolean shouldShow() {
-    return true;
-  }
-
-  @Override
-  public ActionProvider getActionProvider(Context context) {
-    return new SimulatorActionProvider(context);
-  }
+/** This module provides an instance of the simulator. */
+@Module
+public abstract class SimulatorModule {
+  @Binds
+  @Singleton
+  public abstract Simulator bindsSimulator(SimulatorImpl simulator);
 }
diff --git a/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java b/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java
index ffb9191..04de201 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java
@@ -26,7 +26,7 @@
 import android.telecom.PhoneAccountHandle;
 import android.telephony.TelephonyManager;
 import com.android.dialer.common.Assert;
-
+import com.google.auto.value.AutoValue;
 import java.util.concurrent.TimeUnit;
 
 /** Populates the device database with voicemail entries. */
@@ -105,7 +105,7 @@
     context.getContentResolver().insert(Status.buildSourceUri(context.getPackageName()), values);
   }
 
-
+  @AutoValue
   abstract static class Voicemail {
     @NonNull
     abstract String getPhoneNumber();
@@ -134,7 +134,7 @@
       return values;
     }
 
-
+    @AutoValue.Builder
     abstract static class Builder {
       abstract Builder setPhoneNumber(@NonNull String phoneNumber);
 
diff --git a/java/com/android/dialer/telecom/TelecomUtil.java b/java/com/android/dialer/telecom/TelecomUtil.java
index a11e7f7..87ddda5 100644
--- a/java/com/android/dialer/telecom/TelecomUtil.java
+++ b/java/com/android/dialer/telecom/TelecomUtil.java
@@ -23,12 +23,13 @@
 import android.net.Uri;
 import android.provider.CallLog.Calls;
 import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
 import android.support.v4.content.ContextCompat;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.text.TextUtils;
-import android.util.Log;
+import com.android.dialer.common.LogUtil;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -41,6 +42,8 @@
 
   private static final String TAG = "TelecomUtil";
   private static boolean sWarningLogged = false;
+  private static Boolean isDefaultDialerForTesting;
+  private static Boolean hasPermissionForTesting;
 
   public static void showInCallScreen(Context context, boolean showDialpad) {
     if (hasReadPhoneStatePermission(context)) {
@@ -48,7 +51,7 @@
         getTelecomManager(context).showInCallScreen(showDialpad);
       } catch (SecurityException e) {
         // Just in case
-        Log.w(TAG, "TelecomManager.showInCallScreen called without permission.");
+        LogUtil.w(TAG, "TelecomManager.showInCallScreen called without permission.");
       }
     }
   }
@@ -59,7 +62,7 @@
         getTelecomManager(context).silenceRinger();
       } catch (SecurityException e) {
         // Just in case
-        Log.w(TAG, "TelecomManager.silenceRinger called without permission.");
+        LogUtil.w(TAG, "TelecomManager.silenceRinger called without permission.");
       }
     }
   }
@@ -69,7 +72,7 @@
       try {
         getTelecomManager(context).cancelMissedCallsNotification();
       } catch (SecurityException e) {
-        Log.w(TAG, "TelecomManager.cancelMissedCalls called without permission.");
+        LogUtil.w(TAG, "TelecomManager.cancelMissedCalls called without permission.");
       }
     }
   }
@@ -79,7 +82,7 @@
       try {
         return getTelecomManager(context).getAdnUriForPhoneAccount(handle);
       } catch (SecurityException e) {
-        Log.w(TAG, "TelecomManager.getAdnUriForPhoneAccount called without permission.");
+        LogUtil.w(TAG, "TelecomManager.getAdnUriForPhoneAccount called without permission.");
       }
     }
     return null;
@@ -95,7 +98,7 @@
           return getTelecomManager(context).handleMmi(dialString, handle);
         }
       } catch (SecurityException e) {
-        Log.w(TAG, "TelecomManager.handleMmi called without permission.");
+        LogUtil.w(TAG, "TelecomManager.handleMmi called without permission.");
       }
     }
     return false;
@@ -186,11 +189,17 @@
   }
 
   private static boolean hasPermission(Context context, String permission) {
+    if (hasPermissionForTesting != null) {
+      return hasPermissionForTesting;
+    }
     return ContextCompat.checkSelfPermission(context, permission)
         == PackageManager.PERMISSION_GRANTED;
   }
 
   public static boolean isDefaultDialer(Context context) {
+    if (isDefaultDialerForTesting != null) {
+      return isDefaultDialerForTesting;
+    }
     final boolean result =
         TextUtils.equals(
             context.getPackageName(), getTelecomManager(context).getDefaultDialerPackage());
@@ -199,7 +208,7 @@
     } else {
       if (!sWarningLogged) {
         // Log only once to prevent spam.
-        Log.w(TAG, "Dialer is not currently set to be default dialer");
+        LogUtil.w(TAG, "Dialer is not currently set to be default dialer");
         sWarningLogged = true;
       }
     }
@@ -209,4 +218,14 @@
   private static TelecomManager getTelecomManager(Context context) {
     return (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
   }
+
+  @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+  public static void setIsDefaultDialerForTesting(Boolean defaultDialer) {
+    isDefaultDialerForTesting = defaultDialer;
+  }
+
+  @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+  public static void setHasPermissionForTesting(Boolean hasPermission) {
+    hasPermissionForTesting = hasPermission;
+  }
 }
diff --git a/java/com/android/dialer/theme/res/drawable-hdpi/ic_block_24dp.png b/java/com/android/dialer/theme/res/drawable-hdpi/ic_block_24dp.png
new file mode 100644
index 0000000..2ccc89d
--- /dev/null
+++ b/java/com/android/dialer/theme/res/drawable-hdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png b/java/com/android/dialer/theme/res/drawable-hdpi/ic_call_arrow.png
similarity index 100%
rename from java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png
rename to java/com/android/dialer/theme/res/drawable-hdpi/ic_call_arrow.png
Binary files differ
diff --git a/java/com/android/dialer/theme/res/drawable-mdpi/ic_block_24dp.png b/java/com/android/dialer/theme/res/drawable-mdpi/ic_block_24dp.png
new file mode 100644
index 0000000..ec1b33f
--- /dev/null
+++ b/java/com/android/dialer/theme/res/drawable-mdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png b/java/com/android/dialer/theme/res/drawable-mdpi/ic_call_arrow.png
similarity index 100%
rename from java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png
rename to java/com/android/dialer/theme/res/drawable-mdpi/ic_call_arrow.png
Binary files differ
diff --git a/java/com/android/dialer/theme/res/drawable-xhdpi/ic_block_24dp.png b/java/com/android/dialer/theme/res/drawable-xhdpi/ic_block_24dp.png
new file mode 100644
index 0000000..7aba97b
--- /dev/null
+++ b/java/com/android/dialer/theme/res/drawable-xhdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png b/java/com/android/dialer/theme/res/drawable-xhdpi/ic_call_arrow.png
similarity index 100%
rename from java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png
rename to java/com/android/dialer/theme/res/drawable-xhdpi/ic_call_arrow.png
Binary files differ
diff --git a/java/com/android/dialer/theme/res/drawable-xxhdpi/ic_block_24dp.png b/java/com/android/dialer/theme/res/drawable-xxhdpi/ic_block_24dp.png
new file mode 100644
index 0000000..fddfa54
--- /dev/null
+++ b/java/com/android/dialer/theme/res/drawable-xxhdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png b/java/com/android/dialer/theme/res/drawable-xxhdpi/ic_call_arrow.png
similarity index 100%
rename from java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png
rename to java/com/android/dialer/theme/res/drawable-xxhdpi/ic_call_arrow.png
Binary files differ
diff --git a/java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_block_24dp.png b/java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_block_24dp.png
new file mode 100644
index 0000000..0378d1b
--- /dev/null
+++ b/java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png b/java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_call_arrow.png
similarity index 100%
rename from java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png
rename to java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_call_arrow.png
Binary files differ
diff --git a/java/com/android/dialer/theme/res/values/dimens.xml b/java/com/android/dialer/theme/res/values/dimens.xml
index 2d11ecc..fa750c6 100644
--- a/java/com/android/dialer/theme/res/values/dimens.xml
+++ b/java/com/android/dialer/theme/res/values/dimens.xml
@@ -25,4 +25,9 @@
   <!-- actionbar height + tab height -->
   <dimen name="actionbar_and_tab_height">107dp</dimen>
   <dimen name="actionbar_contentInsetStart">72dp</dimen>
+
+  <dimen name="toolbar_title_text_size">20sp</dimen>
+  <dimen name="toolbar_subtitle_text_size">14sp</dimen>
+
+  <dimen name="call_log_icon_margin">4dp</dimen>
 </resources>
diff --git a/java/com/android/dialer/theme/res/values/styles.xml b/java/com/android/dialer/theme/res/values/styles.xml
index ac94d06..b5e89ff 100644
--- a/java/com/android/dialer/theme/res/values/styles.xml
+++ b/java/com/android/dialer/theme/res/values/styles.xml
@@ -53,4 +53,15 @@
     <item name="android:background">@color/actionbar_background_color</item>
     <item name="background">@color/actionbar_background_color</item>
   </style>
+
+  <style name="toolbar_title_text">
+    <item name="android:textSize">@dimen/toolbar_title_text_size</item>
+    <item name="android:textColor">@color/background_dialer_white</item>
+    <item name="android:fontFamily">sans-serif-medium</item>
+  </style>
+
+  <style name="toolbar_subtitle_text">
+    <item name="android:textSize">@dimen/toolbar_subtitle_text_size</item>
+    <item name="android:textColor">@color/background_dialer_white</item>
+  </style>
 </resources>
diff --git a/java/com/android/dialer/util/AndroidManifest.xml b/java/com/android/dialer/util/AndroidManifest.xml
index 499df9b..ba22c17 100644
--- a/java/com/android/dialer/util/AndroidManifest.xml
+++ b/java/com/android/dialer/util/AndroidManifest.xml
@@ -1,3 +1,19 @@
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
 <manifest
   package="com.android.dialer.util">
 </manifest>
diff --git a/java/com/android/dialer/util/PermissionsUtil.java b/java/com/android/dialer/util/PermissionsUtil.java
index 70b96df..5741e73 100644
--- a/java/com/android/dialer/util/PermissionsUtil.java
+++ b/java/com/android/dialer/util/PermissionsUtil.java
@@ -47,6 +47,10 @@
     return hasPermission(context, permission.CAMERA);
   }
 
+  public static boolean hasMicrophonePermissions(Context context) {
+    return hasPermission(context, permission.RECORD_AUDIO);
+  }
+
   public static boolean hasPermission(Context context, String permission) {
     return ContextCompat.checkSelfPermission(context, permission)
         == PackageManager.PERMISSION_GRANTED;
diff --git a/java/com/android/dialer/util/SettingsUtil.java b/java/com/android/dialer/util/SettingsUtil.java
index c61c09b..5043c3d 100644
--- a/java/com/android/dialer/util/SettingsUtil.java
+++ b/java/com/android/dialer/util/SettingsUtil.java
@@ -69,6 +69,15 @@
         }
       }
     }
+    getRingtoneName(context, handler, ringtoneUri, msg, defaultRingtone);
+  }
+
+  public static void getRingtoneName(Context context, Handler handler, Uri ringtoneUri, int msg) {
+    getRingtoneName(context, handler, ringtoneUri, msg, false);
+  }
+
+  public static void getRingtoneName(
+      Context context, Handler handler, Uri ringtoneUri, int msg, boolean defaultRingtone) {
     CharSequence summary = context.getString(R.string.ringtone_unknown);
     // Is it a silent ringtone?
     if (ringtoneUri == null) {
diff --git a/java/com/android/dialer/util/ViewUtil.java b/java/com/android/dialer/util/ViewUtil.java
index de08e41..81a32f9 100644
--- a/java/com/android/dialer/util/ViewUtil.java
+++ b/java/com/android/dialer/util/ViewUtil.java
@@ -27,6 +27,7 @@
 import android.util.TypedValue;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 import android.view.ViewTreeObserver.OnPreDrawListener;
 import android.widget.TextView;
 import java.util.Locale;
@@ -113,6 +114,18 @@
             });
   }
 
+  public static void doOnGlobalLayout(@NonNull final View view, final ViewRunnable runnable) {
+    view.getViewTreeObserver()
+        .addOnGlobalLayoutListener(
+            new OnGlobalLayoutListener() {
+              @Override
+              public void onGlobalLayout() {
+                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                runnable.run(view);
+              }
+            });
+  }
+
   /**
    * Returns {@code true} if animations should be disabled.
    *
diff --git a/java/com/android/dialer/widget/MessageFragment.java b/java/com/android/dialer/widget/MessageFragment.java
new file mode 100644
index 0000000..ab47f24
--- /dev/null
+++ b/java/com/android/dialer/widget/MessageFragment.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.widget;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FragmentUtils;
+
+/** Fragment used to compose call with message fragment. */
+public class MessageFragment extends Fragment implements OnClickListener, TextWatcher {
+  private static final String CHAR_LIMIT_KEY = "char_limit";
+  private static final String SHOW_SEND_ICON_KEY = "show_send_icon";
+  private static final String MESSAGE_LIST_KEY = "message_list";
+
+  public static final int NO_CHAR_LIMIT = -1;
+
+  private EditText customMessage;
+  private ImageView sendMessage;
+  private TextView remainingChar;
+  private int charLimit;
+
+  private static MessageFragment newInstance(Builder builder) {
+    MessageFragment fragment = new MessageFragment();
+    Bundle args = new Bundle();
+    args.putInt(CHAR_LIMIT_KEY, builder.charLimit);
+    args.putBoolean(SHOW_SEND_ICON_KEY, builder.showSendIcon);
+    args.putStringArray(MESSAGE_LIST_KEY, builder.messages);
+    fragment.setArguments(args);
+    return fragment;
+  }
+
+  @Nullable
+  public String getMessage() {
+    return customMessage == null ? null : customMessage.getText().toString();
+  }
+
+  @Nullable
+  @Override
+  public View onCreateView(
+      LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+    View view = inflater.inflate(R.layout.fragment_message, container, false);
+
+    sendMessage = (ImageView) view.findViewById(R.id.send_message);
+    if (getArguments().getBoolean(SHOW_SEND_ICON_KEY, false)) {
+      sendMessage.setVisibility(View.VISIBLE);
+      sendMessage.setEnabled(false);
+      sendMessage.setOnClickListener(this);
+    }
+
+    customMessage = (EditText) view.findViewById(R.id.custom_message);
+    customMessage.addTextChangedListener(this);
+    charLimit = getArguments().getInt(CHAR_LIMIT_KEY, NO_CHAR_LIMIT);
+    if (charLimit != NO_CHAR_LIMIT) {
+      remainingChar = (TextView) view.findViewById(R.id.remaining_characters);
+      remainingChar.setVisibility(View.VISIBLE);
+      remainingChar = (TextView) view.findViewById(R.id.remaining_characters);
+      remainingChar.setText("" + charLimit);
+      customMessage.setFilters(new InputFilter[] {new InputFilter.LengthFilter(charLimit)});
+    }
+
+    LinearLayout messageContainer = (LinearLayout) view.findViewById(R.id.message_container);
+    for (String message : getArguments().getStringArray(MESSAGE_LIST_KEY)) {
+      TextView textView = (TextView) inflater.inflate(R.layout.selectable_text_view, null);
+      textView.setOnClickListener(this);
+      textView.setText(message);
+      messageContainer.addView(textView);
+    }
+    return view;
+  }
+
+  @Override
+  public void onClick(View view) {
+    if (view == sendMessage) {
+      getListener().onMessageFragmentSendMessage(customMessage.getText().toString());
+    } else if (view.getId() == R.id.selectable_text_view) {
+      customMessage.setText(((TextView) view).getText());
+      customMessage.setSelection(customMessage.getText().length());
+    } else {
+      Assert.fail("Unknown view clicked");
+    }
+  }
+
+  @Override
+  public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+  @Override
+  public void onTextChanged(CharSequence s, int start, int before, int count) {
+    sendMessage.setEnabled(s.length() > 0);
+  }
+
+  @Override
+  public void afterTextChanged(Editable s) {
+    if (charLimit != NO_CHAR_LIMIT) {
+      remainingChar.setText("" + (charLimit - s.length()));
+    }
+    getListener().onMessageFragmentAfterTextChange(s.toString());
+  }
+
+  private Listener getListener() {
+    return FragmentUtils.getParentUnsafe(this, Listener.class);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Builder for {@link MessageFragment}. */
+  public static class Builder {
+    private String[] messages;
+    private boolean showSendIcon;
+    private int charLimit = NO_CHAR_LIMIT;
+
+    /**
+     * @throws NullPointerException if message is null
+     * @throws IllegalArgumentException if messages.length is outside the range [1,3].
+     */
+    public Builder setMessages(String... messages) {
+      // Since we only allow up to 3 messages, crash if more are set.
+      Assert.checkArgument(messages.length > 0 && messages.length <= 3);
+      this.messages = messages;
+      return this;
+    }
+
+    public Builder showSendIcon() {
+      showSendIcon = true;
+      return this;
+    }
+
+    public Builder setCharLimit(int charLimit) {
+      this.charLimit = charLimit;
+      return this;
+    }
+
+    public MessageFragment build() {
+      return MessageFragment.newInstance(this);
+    }
+  }
+
+  /** Interface for parent activity to implement to listen for important events. */
+  public interface Listener {
+    void onMessageFragmentSendMessage(String message);
+
+    void onMessageFragmentAfterTextChange(String message);
+  }
+}
diff --git a/java/com/android/voicemailomtp/mail/BodyPart.java b/java/com/android/dialer/widget/res/color/dialer_tint_state.xml
similarity index 68%
copy from java/com/android/voicemailomtp/mail/BodyPart.java
copy to java/com/android/dialer/widget/res/color/dialer_tint_state.xml
index 62390a4..c29f334 100644
--- a/java/com/android/voicemailomtp/mail/BodyPart.java
+++ b/java/com/android/dialer/widget/res/color/dialer_tint_state.xml
@@ -1,3 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
 /*
  * Copyright (C) 2015 The Android Open Source Project
  *
@@ -13,12 +15,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.voicemailomtp.mail;
+-->
 
-public abstract class BodyPart implements Part {
-    protected Multipart mParent;
-
-    public Multipart getParent() {
-        return mParent;
-    }
-}
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+  <item android:color="@color/dialer_edit_text_hint_color" android:state_enabled="false"/>
+  <item android:color="@color/dialer_theme_color"/>
+</selector>
\ No newline at end of file
diff --git a/java/com/android/dialer/widget/res/layout/fragment_message.xml b/java/com/android/dialer/widget/res/layout/fragment_message.xml
new file mode 100644
index 0000000..f09c54f
--- /dev/null
+++ b/java/com/android/dialer/widget/res/layout/fragment_message.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2016 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<LinearLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent"
+  android:orientation="vertical"
+  android:gravity="bottom"
+  android:background="@color/background_dialer_white">
+
+  <LinearLayout
+    android:id="@+id/message_container"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"/>
+
+  <View
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/message_divider_height"
+    android:background="#12000000"/>
+
+  <RelativeLayout
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+
+    <EditText
+      android:id="@+id/custom_message"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:padding="@dimen/textview_item_padding"
+      android:textSize="@dimen/message_item_text_size"
+      android:hint="@string/custom_message_hint"
+      android:textColor="@color/dialer_primary_text_color"
+      android:textColorHint="@color/dialer_edit_text_hint_color"
+      android:background="@color/background_dialer_white"
+      android:textCursorDrawable="@drawable/searchedittext_custom_cursor"
+      android:layout_toStartOf="@+id/count_and_send_container"/>
+
+    <LinearLayout
+      android:id="@+id/count_and_send_container"
+      android:orientation="vertical"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_alignParentEnd="true"
+      android:layout_centerVertical="true"
+      android:layout_marginEnd="@dimen/textview_item_padding"
+      android:gravity="center">
+
+      <ImageView
+        android:id="@+id/send_message"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        android:src="@drawable/quantum_ic_send_white_24"
+        android:background="?android:attr/selectableItemBackgroundBorderless"
+        android:tint="@color/dialer_tint_state"/>
+
+      <TextView
+        android:id="@+id/remaining_characters"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        android:textSize="@dimen/message_remaining_char_text_size"
+        android:textColor="@color/dialer_edit_text_hint_color"/>
+    </LinearLayout>
+  </RelativeLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/widget/res/layout/selectable_text_view.xml b/java/com/android/dialer/widget/res/layout/selectable_text_view.xml
new file mode 100644
index 0000000..3d120d1
--- /dev/null
+++ b/java/com/android/dialer/widget/res/layout/selectable_text_view.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2016 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<TextView
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:id="@+id/selectable_text_view"
+  android:layout_width="match_parent"
+  android:layout_height="wrap_content"
+  android:textSize="16sp"
+  android:textColor="@color/dialer_primary_text_color"
+  android:padding="16dp"
+  android:background="@drawable/item_background_material_light"/>
\ No newline at end of file
diff --git a/java/com/android/dialer/widget/res/values/dimens.xml b/java/com/android/dialer/widget/res/values/dimens.xml
new file mode 100644
index 0000000..6c4ea60
--- /dev/null
+++ b/java/com/android/dialer/widget/res/values/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2016 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<resources>
+  <!-- Message Fragment -->
+  <dimen name="message_item_text_size">16sp</dimen>
+  <dimen name="textview_item_padding">16dp</dimen>
+  <dimen name="message_remaining_char_text_size">12sp</dimen>
+  <dimen name="message_divider_height">1dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/widget/res/values/strings.xml b/java/com/android/dialer/widget/res/values/strings.xml
new file mode 100644
index 0000000..6904c2d
--- /dev/null
+++ b/java/com/android/dialer/widget/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <!-- Hint in a text field to compose a custom message to send with a phone call [CHAR LIMIT=27] -->
+  <string name="custom_message_hint">Write a custom message</string>
+</resources>
\ No newline at end of file
diff --git a/java/com/android/incallui/AnswerScreenPresenter.java b/java/com/android/incallui/AnswerScreenPresenter.java
index a21876b..442ad26 100644
--- a/java/com/android/incallui/AnswerScreenPresenter.java
+++ b/java/com/android/incallui/AnswerScreenPresenter.java
@@ -20,6 +20,7 @@
 import android.support.annotation.FloatRange;
 import android.support.annotation.NonNull;
 import android.support.v4.os.UserManagerCompat;
+import android.telecom.VideoProfile;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.incallui.answer.protocol.AnswerScreen;
@@ -71,18 +72,26 @@
   }
 
   @Override
-  public void onAnswer(int videoState) {
+  public void onAnswer(boolean answerVideoAsAudio) {
     if (answerScreen.isVideoUpgradeRequest()) {
-      call.acceptUpgradeRequest(videoState);
+      if (answerVideoAsAudio) {
+        call.getVideoTech().acceptVideoRequestAsAudio();
+      } else {
+        call.getVideoTech().acceptVideoRequest();
+      }
     } else {
-      call.answer(videoState);
+      if (answerVideoAsAudio) {
+        call.answer(VideoProfile.STATE_AUDIO_ONLY);
+      } else {
+        call.answer();
+      }
     }
   }
 
   @Override
   public void onReject() {
     if (answerScreen.isVideoUpgradeRequest()) {
-      call.declineUpgradeRequest();
+      call.getVideoTech().declineVideoRequest();
     } else {
       call.reject(false /* rejectWithMessage */, null);
     }
diff --git a/java/com/android/incallui/AnswerScreenPresenterStub.java b/java/com/android/incallui/AnswerScreenPresenterStub.java
index fc47bf5..fc4e7df 100644
--- a/java/com/android/incallui/AnswerScreenPresenterStub.java
+++ b/java/com/android/incallui/AnswerScreenPresenterStub.java
@@ -34,7 +34,7 @@
   public void onRejectCallWithMessage(String message) {}
 
   @Override
-  public void onAnswer(int videoState) {}
+  public void onAnswer(boolean answerVideoAsAudio) {}
 
   @Override
   public void onReject() {}
diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java
index d6f4cdd..c5c43f7 100644
--- a/java/com/android/incallui/CallButtonPresenter.java
+++ b/java/com/android/incallui/CallButtonPresenter.java
@@ -17,17 +17,13 @@
 package com.android.incallui;
 
 import android.content.Context;
-import android.os.Build;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
 import android.support.v4.os.UserManagerCompat;
 import android.telecom.CallAudioState;
-import android.telecom.InCallService.VideoCall;
-import android.telecom.VideoProfile;
 import com.android.contacts.common.compat.CallCompat;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
-import com.android.dialer.compat.SdkVersionOverride;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.logging.nano.DialerImpression;
 import com.android.incallui.AudioModeProvider.AudioModeListener;
@@ -39,6 +35,7 @@
 import com.android.incallui.InCallPresenter.IncomingCallListener;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.CameraDirection;
 import com.android.incallui.call.TelecomAdapter;
 import com.android.incallui.call.VideoUtils;
 import com.android.incallui.incall.protocol.InCallButtonIds;
@@ -212,6 +209,13 @@
   @Override
   public void muteClicked(boolean checked) {
     LogUtil.v("CallButtonPresenter", "turning on mute: " + checked);
+    Logger.get(mContext)
+        .logCallImpression(
+            checked
+                ? DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_MUTE
+                : DialerImpression.Type.IN_CALL_SCREEN_TURN_OFF_MUTE,
+            mCall.getUniqueCallId(),
+            mCall.getTimeAddedMs());
     TelecomAdapter.getInstance().mute(checked);
   }
 
@@ -262,18 +266,8 @@
 
   @Override
   public void changeToVideoClicked() {
-    VideoCall videoCall = mCall.getVideoCall();
-    if (videoCall == null) {
-      return;
-    }
-    int currVideoState = mCall.getVideoState();
-    int currUnpausedVideoState = VideoUtils.getUnPausedVideoState(currVideoState);
-    currUnpausedVideoState |= VideoProfile.STATE_BIDIRECTIONAL;
-
-    VideoProfile videoProfile = new VideoProfile(currUnpausedVideoState);
-    videoCall.sendSessionModifyRequest(videoProfile);
-    mCall.setSessionModificationState(
-        DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE);
+    LogUtil.enterBlock("CallButtonPresenter.changeToVideoClicked");
+    mCall.getVideoTech().upgradeToVideo();
   }
 
   @Override
@@ -300,26 +294,25 @@
     InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
     cameraManager.setUseFrontFacingCamera(useFrontFacingCamera);
 
-    VideoCall videoCall = mCall.getVideoCall();
-    if (videoCall == null) {
-      return;
-    }
-
     String cameraId = cameraManager.getActiveCameraId();
     if (cameraId != null) {
       final int cameraDir =
           cameraManager.isUsingFrontFacingCamera()
-              ? DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING
-              : DialerCall.VideoSettings.CAMERA_DIRECTION_BACK_FACING;
-      mCall.getVideoSettings().setCameraDir(cameraDir);
-      videoCall.setCamera(cameraId);
-      videoCall.requestCameraCapabilities();
+              ? CameraDirection.CAMERA_DIRECTION_FRONT_FACING
+              : CameraDirection.CAMERA_DIRECTION_BACK_FACING;
+      mCall.setCameraDir(cameraDir);
+      mCall.getVideoTech().setCamera(cameraId);
     }
   }
 
   @Override
   public void toggleCameraClicked() {
     LogUtil.i("CallButtonPresenter.toggleCameraClicked", "");
+    Logger.get(mContext)
+        .logCallImpression(
+            DialerImpression.Type.IN_CALL_SCREEN_SWAP_CAMERA,
+            mCall.getUniqueCallId(),
+            mCall.getTimeAddedMs());
     switchCameraClicked(
         !InCallPresenter.getInstance().getInCallCameraManager().isUsingFrontFacingCamera());
   }
@@ -333,24 +326,19 @@
   @Override
   public void pauseVideoClicked(boolean pause) {
     LogUtil.i("CallButtonPresenter.pauseVideoClicked", "%s", pause ? "pause" : "unpause");
-    VideoCall videoCall = mCall.getVideoCall();
-    if (videoCall == null) {
-      return;
-    }
 
-    int currUnpausedVideoState = VideoUtils.getUnPausedVideoState(mCall.getVideoState());
+    Logger.get(mContext)
+        .logCallImpression(
+            pause
+                ? DialerImpression.Type.IN_CALL_SCREEN_TURN_OFF_VIDEO
+                : DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_VIDEO,
+            mCall.getUniqueCallId(),
+            mCall.getTimeAddedMs());
+
     if (pause) {
-      videoCall.setCamera(null);
-      VideoProfile videoProfile =
-          new VideoProfile(currUnpausedVideoState & ~VideoProfile.STATE_TX_ENABLED);
-      videoCall.sendSessionModifyRequest(videoProfile);
+      mCall.getVideoTech().stopTransmission();
     } else {
-      InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
-      videoCall.setCamera(cameraManager.getActiveCameraId());
-      VideoProfile videoProfile =
-          new VideoProfile(currUnpausedVideoState | VideoProfile.STATE_TX_ENABLED);
-      videoCall.sendSessionModifyRequest(videoProfile);
-      mCall.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE);
+      mCall.getVideoTech().resumeTransmission();
     }
 
     mInCallButtonUi.setVideoPaused(pause);
@@ -386,7 +374,7 @@
    */
   private void updateButtonsState(DialerCall call) {
     LogUtil.v("CallButtonPresenter.updateButtonsState", "");
-    final boolean isVideo = VideoUtils.isVideoCall(call);
+    final boolean isVideo = call.isVideoCall();
 
     // Common functionality (audio, hold, etc).
     // Show either HOLD or SWAP, but not both. If neither HOLD or SWAP is available:
@@ -402,7 +390,7 @@
     final boolean showAddCall =
         TelecomAdapter.getInstance().canAddCall() && UserManagerCompat.isUserUnlocked(mContext);
     final boolean showMerge = call.can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE);
-    final boolean showUpgradeToVideo = !isVideo && hasVideoCallCapabilities(call);
+    final boolean showUpgradeToVideo = !isVideo && (hasVideoCallCapabilities(call));
     final boolean showDowngradeToAudio = isVideo && isDowngradeToAudioSupported(call);
     final boolean showMute = call.can(android.telecom.Call.Details.CAPABILITY_MUTE);
 
@@ -427,8 +415,7 @@
         InCallButtonIds.BUTTON_SWITCH_CAMERA, isVideo && hasCameraPermission);
     mInCallButtonUi.showButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, showPauseVideo);
     if (isVideo) {
-      mInCallButtonUi.setVideoPaused(
-          !VideoUtils.isTransmissionEnabled(call) || !hasCameraPermission);
+      mInCallButtonUi.setVideoPaused(!call.getVideoTech().isTransmitting() || !hasCameraPermission);
     }
     mInCallButtonUi.showButton(InCallButtonIds.BUTTON_DIALPAD, true);
     mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MERGE, showMerge);
@@ -437,12 +424,7 @@
   }
 
   private boolean hasVideoCallCapabilities(DialerCall call) {
-    if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) {
-      return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX)
-          && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX);
-    }
-    // In L, this single flag represents both video transmitting and receiving capabilities
-    return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX);
+    return call.getVideoTech().isAvailable();
   }
 
   /**
@@ -454,6 +436,7 @@
    * @return {@code true} if downgrading to an audio-only call from a video call is supported.
    */
   private boolean isDowngradeToAudioSupported(DialerCall call) {
+    // TODO(b/33676907): If there is an RCS video share session, return true here
     return !call.can(CallCompat.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO);
   }
 
diff --git a/java/com/android/incallui/CallCardPresenter.java b/java/com/android/incallui/CallCardPresenter.java
index 9307757..668692d 100644
--- a/java/com/android/incallui/CallCardPresenter.java
+++ b/java/com/android/incallui/CallCardPresenter.java
@@ -19,7 +19,6 @@
 import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL;
 
 import android.Manifest;
-import android.app.Application;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -29,6 +28,7 @@
 import android.hardware.display.DisplayManager;
 import android.os.BatteryManager;
 import android.os.Handler;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.v4.app.Fragment;
 import android.support.v4.content.ContextCompat;
@@ -46,9 +46,12 @@
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.ConfigProviderBindings;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.ActivityCompat;
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
 import com.android.dialer.enrichedcall.EnrichedCallManager;
 import com.android.dialer.enrichedcall.Session;
 import com.android.dialer.multimedia.MultimediaData;
+import com.android.dialer.oem.MotorolaUtils;
 import com.android.incallui.ContactInfoCache.ContactCacheEntry;
 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
 import com.android.incallui.InCallPresenter.InCallDetailsListener;
@@ -58,14 +61,16 @@
 import com.android.incallui.InCallPresenter.IncomingCallListener;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
 import com.android.incallui.call.DialerCallListener;
+import com.android.incallui.calllocation.CallLocation;
+import com.android.incallui.calllocation.CallLocationComponent;
 import com.android.incallui.incall.protocol.ContactPhotoType;
 import com.android.incallui.incall.protocol.InCallScreen;
 import com.android.incallui.incall.protocol.InCallScreenDelegate;
 import com.android.incallui.incall.protocol.PrimaryCallState;
 import com.android.incallui.incall.protocol.PrimaryInfo;
 import com.android.incallui.incall.protocol.SecondaryInfo;
+import com.android.incallui.videotech.VideoTech;
 import java.lang.ref.WeakReference;
 
 /**
@@ -116,7 +121,8 @@
   private InCallScreen mInCallScreen;
   private boolean isInCallScreenReady;
   private boolean shouldSendAccessibilityEvent;
-  private final String locationModule = null;
+
+  @NonNull private final CallLocation callLocation;
   private final Runnable sendAccessibilityEventRunnable =
       new Runnable() {
         @Override
@@ -135,6 +141,7 @@
   public CallCardPresenter(Context context) {
     LogUtil.i("CallCardController.constructor", null);
     mContext = Assert.isNotNull(context).getApplicationContext();
+    callLocation = CallLocationComponent.get(mContext).getCallLocation();
   }
 
   private static boolean hasCallSubject(DialerCall call) {
@@ -175,8 +182,7 @@
       mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
     }
 
-    EnrichedCallManager.Accessor.getInstance(((Application) mContext))
-        .registerStateChangedListener(this);
+    EnrichedCallComponent.get(mContext).getEnrichedCallManager().registerStateChangedListener(this);
 
     // Contact search may have completed before ui is ready.
     if (mPrimaryContactInfo != null) {
@@ -189,6 +195,11 @@
     InCallPresenter.getInstance().addDetailsListener(this);
     InCallPresenter.getInstance().addInCallEventListener(this);
     isInCallScreenReady = true;
+
+    // Showing the location may have been skipped if the UI wasn't ready during previous layout.
+    if (shouldShowLocation()) {
+      updatePrimaryDisplayInfo();
+    }
   }
 
   @Override
@@ -196,7 +207,8 @@
     LogUtil.i("CallCardController.onInCallScreenUnready", null);
     Assert.checkState(isInCallScreenReady);
 
-    EnrichedCallManager.Accessor.getInstance(((Application) mContext))
+    EnrichedCallComponent.get(mContext)
+        .getEnrichedCallManager()
         .unregisterStateChangedListener(this);
     // stop getting call state changes
     InCallPresenter.getInstance().removeListener(this);
@@ -207,6 +219,8 @@
       mPrimary.removeListener(this);
     }
 
+    callLocation.close();
+
     mPrimary = null;
     mPrimaryContactInfo = null;
     mSecondaryContactInfo = null;
@@ -282,7 +296,6 @@
               mContext, mPrimary, mPrimary.getState() == DialerCall.State.INCOMING);
       updatePrimaryDisplayInfo();
       maybeStartSearch(mPrimary, true);
-      maybeClearSessionModificationState(mPrimary);
     }
 
     if (previousPrimary != null && mPrimary == null) {
@@ -300,7 +313,6 @@
               mContext, mSecondary, mSecondary.getState() == DialerCall.State.INCOMING);
       updateSecondaryDisplayInfo();
       maybeStartSearch(mSecondary, false);
-      maybeClearSessionModificationState(mSecondary);
     }
 
     // Set the call state
@@ -373,25 +385,18 @@
   @Override
   public void onDialerCallUpgradeToVideo() {}
 
-  /**
-   * Handles a change to the session modification state for a call.
-   *
-   * @param sessionModificationState The new session modification state.
-   */
+  /** Handles a change to the session modification state for a call. */
   @Override
-  public void onDialerCallSessionModificationStateChange(
-      @SessionModificationState int sessionModificationState) {
-    LogUtil.v(
-        "CallCardPresenter.onDialerCallSessionModificationStateChange",
-        "state: " + sessionModificationState);
+  public void onDialerCallSessionModificationStateChange() {
+    LogUtil.enterBlock("CallCardPresenter.onDialerCallSessionModificationStateChange");
 
     if (mPrimary == null) {
       return;
     }
     getUi()
         .setEndCallButtonEnabled(
-            sessionModificationState
-                != DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
+            mPrimary.getVideoTech().getSessionModificationState()
+                != VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
             true /* shouldAnimate */);
     updatePrimaryCallState();
   }
@@ -418,6 +423,13 @@
                   && mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
       boolean isHdAudioCall =
           isPrimaryCallActive() && mPrimary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO);
+      boolean isAttemptingHdAudioCall =
+          !isHdAudioCall
+              && !mPrimary.hasProperty(DialerCall.PROPERTY_CODEC_KNOWN)
+              && MotorolaUtils.shouldBlinkHdIconWhenConnectingCall(mContext);
+
+      boolean isBusiness = mPrimaryContactInfo != null && mPrimaryContactInfo.isBusiness;
+
       // Check for video state change and update the visibility of the contact photo.  The contact
       // photo is hidden when the incoming video surface is shown.
       // The contact photo visibility can also change in setPrimary().
@@ -427,8 +439,8 @@
           .setCallState(
               new PrimaryCallState(
                   mPrimary.getState(),
-                  mPrimary.getVideoState(),
-                  mPrimary.getSessionModificationState(),
+                  mPrimary.isVideoCall(),
+                  mPrimary.getVideoTech().getSessionModificationState(),
                   mPrimary.getDisconnectCause(),
                   getConnectionLabel(),
                   getCallStateIcon(),
@@ -438,12 +450,14 @@
                   mPrimary.hasProperty(Details.PROPERTY_WIFI),
                   mPrimary.isConferenceCall(),
                   isWorkCall,
+                  isAttemptingHdAudioCall,
                   isHdAudioCall,
                   !TextUtils.isEmpty(mPrimary.getLastForwardedNumber()),
                   shouldShowContactPhoto,
                   mPrimary.getConnectTimeMillis(),
                   CallerInfoUtils.isVoiceMailNumber(mContext, mPrimary),
-                  mPrimary.isRemotelyHeld()));
+                  mPrimary.isRemotelyHeld(),
+                  isBusiness));
 
       InCallActivity activity =
           (InCallActivity) (mInCallScreen.getInCallScreenFragment().getActivity());
@@ -508,15 +522,6 @@
     }
   }
 
-  private void maybeClearSessionModificationState(DialerCall call) {
-    @SessionModificationState int state = call.getSessionModificationState();
-    if (state != DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST
-        && state != DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
-      LogUtil.i("CallCardPresenter.maybeClearSessionModificationState", "clearing state");
-      call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
-    }
-  }
-
   /** Starts a query for more contact data for the save primary and secondary calls. */
   private void startContactInfoSearch(
       final DialerCall call, final boolean isPrimary, boolean isIncoming) {
@@ -642,13 +647,17 @@
     // DialerCall placed through a work phone account.
     boolean hasWorkCallProperty = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL);
 
-    Session enrichedCallSession =
-        mPrimary.getNumber() == null
-            ? null
-            : EnrichedCallManager.Accessor.getInstance(((Application) mContext))
-                .getSession(mPrimary.getNumber());
-    MultimediaData enrichedCallMultimediaData =
-        enrichedCallSession == null ? null : enrichedCallSession.getMultimediaData();
+    MultimediaData multimediaData = null;
+    if (mPrimary.getNumber() != null) {
+      Session enrichedCallSession =
+          EnrichedCallComponent.get(mContext)
+              .getEnrichedCallManager()
+              .getSession(mPrimary.getUniqueCallId(), mPrimary.getNumber());
+      if (enrichedCallSession != null) {
+        enrichedCallSession.setUniqueDialerCallId(mPrimary.getUniqueCallId());
+        multimediaData = enrichedCallSession.getMultimediaData();
+      }
+    }
 
     if (mPrimary.isConferenceCall()) {
       LogUtil.v(
@@ -671,7 +680,8 @@
               false /* answeringDisconnectsOngoingCall */,
               shouldShowLocation(),
               null /* contactInfoLookupKey */,
-              null /* enrichedCallMultimediaData */));
+              null /* enrichedCallMultimediaData */,
+              mPrimary.getNumberPresentation()));
     } else if (mPrimaryContactInfo != null) {
       LogUtil.v(
           "CallCardPresenter.updatePrimaryDisplayInfo",
@@ -696,6 +706,7 @@
       }
 
       boolean nameIsNumber = name != null && name.equals(mPrimaryContactInfo.number);
+
       // DialerCall with caller that is a work contact.
       boolean isWorkContact = (mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
       mInCallScreen.setPrimary(
@@ -714,13 +725,52 @@
               mPrimary.answeringDisconnectsForegroundVideoCall(),
               shouldShowLocation(),
               mPrimaryContactInfo.lookupKey,
-              enrichedCallMultimediaData));
+              multimediaData,
+              mPrimary.getNumberPresentation()));
     } else {
       // Clear the primary display info.
       mInCallScreen.setPrimary(PrimaryInfo.createEmptyPrimaryInfo());
     }
 
-    mInCallScreen.showLocationUi(null);
+    if (isInCallScreenReady) {
+      mInCallScreen.showLocationUi(getLocationFragment());
+    } else {
+      LogUtil.i("CallCardPresenter.updatePrimaryDisplayInfo", "UI not ready, not showing location");
+    }
+  }
+
+  private Fragment getLocationFragment() {
+    if (!ConfigProviderBindings.get(mContext)
+        .getBoolean(CONFIG_ENABLE_EMERGENCY_LOCATION, CONFIG_ENABLE_EMERGENCY_LOCATION_DEFAULT)) {
+      LogUtil.i("CallCardPresenter.getLocationFragment", "disabled by config.");
+      return null;
+    }
+    if (!shouldShowLocation()) {
+      LogUtil.i("CallCardPresenter.getLocationFragment", "shouldn't show location");
+      return null;
+    }
+    if (!hasLocationPermission()) {
+      LogUtil.i("CallCardPresenter.getLocationFragment", "no location permission.");
+      return null;
+    }
+    if (isBatteryTooLowForEmergencyLocation()) {
+      LogUtil.i("CallCardPresenter.getLocationFragment", "low battery.");
+      return null;
+    }
+    if (ActivityCompat.isInMultiWindowMode(mInCallScreen.getInCallScreenFragment().getActivity())) {
+      LogUtil.i("CallCardPresenter.getLocationFragment", "in multi-window mode");
+      return null;
+    }
+    if (mPrimary.isVideoCall()) {
+      LogUtil.i("CallCardPresenter.getLocationFragment", "emergency video calls not supported");
+      return null;
+    }
+    if (!callLocation.canGetLocation(mContext)) {
+      LogUtil.i("CallCardPresenter.getLocationFragment", "can't get current location");
+      return null;
+    }
+    LogUtil.i("CallCardPresenter.getLocationFragment", "returning location fragment");
+    return callLocation.getLocationFragment(mContext);
   }
 
   private boolean shouldShowLocation() {
@@ -972,8 +1022,8 @@
         || callState == DialerCall.State.INCOMING) {
       return false;
     }
-    if (mPrimary.getSessionModificationState()
-        == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+    if (mPrimary.getVideoTech().getSessionModificationState()
+        == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
       return false;
     }
     return true;
diff --git a/java/com/android/incallui/CallerInfoAsyncQuery.java b/java/com/android/incallui/CallerInfoAsyncQuery.java
index f8d7ac6..d620d47 100644
--- a/java/com/android/incallui/CallerInfoAsyncQuery.java
+++ b/java/com/android/incallui/CallerInfoAsyncQuery.java
@@ -55,7 +55,7 @@
 public class CallerInfoAsyncQuery {
 
   /** Interface for a CallerInfoAsyncQueryHandler result return. */
-  public interface OnQueryCompleteListener {
+  interface OnQueryCompleteListener {
 
     /** Called when the query is complete. */
     @MainThread
@@ -85,7 +85,7 @@
   private CallerInfoAsyncQuery() {}
 
   @RequiresPermission(Manifest.permission.READ_CONTACTS)
-  public static void startQuery(
+  static void startQuery(
       final int token,
       final Context context,
       final CallerInfo info,
@@ -99,7 +99,7 @@
         new OnQueryCompleteListener() {
           @Override
           public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
-            Log.d(LOG_TAG, "contactsProviderQueryCompleteListener done");
+            Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onQueryComplete");
             // If there are no other directory queries, make sure that the listener is
             // notified of this result.  see b/27621628
             if ((ci != null && ci.contactExists)
@@ -112,6 +112,7 @@
 
           @Override
           public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+            Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onDataLoaded");
             listener.onDataLoaded(token, cookie, ci);
           }
         };
@@ -270,9 +271,9 @@
   /* Directory lookup related code - END */
 
   /** Simple exception used to communicate problems with the query pool. */
-  public static class QueryPoolException extends SQLException {
+  private static class QueryPoolException extends SQLException {
 
-    public QueryPoolException(String error) {
+    QueryPoolException(String error) {
       super(error);
     }
   }
@@ -337,7 +338,7 @@
       }
     }
 
-    public OnQueryCompleteListener newListener(long directoryId) {
+    OnQueryCompleteListener newListener(long directoryId) {
       return new DirectoryQueryCompleteListener(directoryId);
     }
 
@@ -351,11 +352,13 @@
 
       @Override
       public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+        Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onDataLoaded");
         mListener.onDataLoaded(token, cookie, ci);
       }
 
       @Override
       public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
+        Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onQueryComplete");
         onDirectoryQueryComplete(token, cookie, ci, mDirectoryId);
       }
     }
@@ -446,7 +449,7 @@
       mCallerInfo = null;
     }
 
-    protected void updateData(int token, Object cookie, Cursor cursor) {
+    void updateData(int token, Object cookie, Cursor cursor) {
       try {
         Log.d(this, "##### updateData() #####  for token: " + token);
 
@@ -549,9 +552,9 @@
      * times before the query is complete. All accesses (listeners) must be queued up and informed
      * in order when the query is complete.
      */
-    protected class CallerInfoWorkerHandler extends WorkerHandler {
+    class CallerInfoWorkerHandler extends WorkerHandler {
 
-      public CallerInfoWorkerHandler(Looper looper) {
+      CallerInfoWorkerHandler(Looper looper) {
         super(looper);
       }
 
@@ -624,7 +627,7 @@
             case EVENT_ADD_LISTENER:
               updateData(msg.arg1, cw, (Cursor) args.result);
               break;
-            default:
+            default: // fall out
           }
           Message reply = args.handler.obtainMessage(msg.what);
           reply.obj = args;
diff --git a/java/com/android/incallui/CallerInfoUtils.java b/java/com/android/incallui/CallerInfoUtils.java
index 9f57fba..7c14533 100644
--- a/java/com/android/incallui/CallerInfoUtils.java
+++ b/java/com/android/incallui/CallerInfoUtils.java
@@ -22,6 +22,7 @@
 import android.content.Loader.OnLoadCompleteListener;
 import android.content.pm.PackageManager;
 import android.net.Uri;
+import android.support.annotation.NonNull;
 import android.support.v4.content.ContextCompat;
 import android.telecom.PhoneAccount;
 import android.telecom.TelecomManager;
@@ -53,7 +54,7 @@
    * OnQueryCompleteListener (which contains information about the phone number label, user's name,
    * etc).
    */
-  public static CallerInfo getCallerInfoForCall(
+  static CallerInfo getCallerInfoForCall(
       Context context,
       DialerCall call,
       Object cookie,
@@ -81,7 +82,7 @@
     return info;
   }
 
-  public static CallerInfo buildCallerInfo(Context context, DialerCall call) {
+  static CallerInfo buildCallerInfo(Context context, DialerCall call) {
     CallerInfo info = new CallerInfo();
 
     // Store CNAP information retrieved from the Connection (we want to do this
@@ -91,6 +92,7 @@
     info.numberPresentation = call.getNumberPresentation();
     info.namePresentation = call.getCnapNamePresentation();
     info.callSubject = call.getCallSubject();
+    info.contactExists = false;
 
     String number = call.getNumber();
     if (!TextUtils.isEmpty(number)) {
@@ -109,9 +111,7 @@
     // Because the InCallUI is immediately launched before the call is connected, occasionally
     // a voicemail call will be passed to InCallUI as a "voicemail:" URI without a number.
     // This call should still be handled as a voicemail call.
-    if ((call.getHandle() != null
-            && PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme()))
-        || isVoiceMailNumber(context, call)) {
+    if (isVoiceMailNumber(context, call)) {
       info.markAsVoiceMail(context);
     }
 
@@ -145,11 +145,17 @@
     return cacheInfo;
   }
 
-  public static boolean isVoiceMailNumber(Context context, DialerCall call) {
+  public static boolean isVoiceMailNumber(Context context, @NonNull DialerCall call) {
+    if (call.getHandle() != null
+        && PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme())) {
+      return true;
+    }
+
     if (ContextCompat.checkSelfPermission(context, permission.READ_PHONE_STATE)
         != PackageManager.PERMISSION_GRANTED) {
       return false;
     }
+
     return TelecomUtil.isVoicemailNumber(context, call.getAccountHandle(), call.getNumber());
   }
 
diff --git a/java/com/android/incallui/ContactInfoCache.java b/java/com/android/incallui/ContactInfoCache.java
index 4d4d94a..c4e25e7 100644
--- a/java/com/android/incallui/ContactInfoCache.java
+++ b/java/com/android/incallui/ContactInfoCache.java
@@ -35,6 +35,7 @@
 import android.support.annotation.WorkerThread;
 import android.support.v4.os.UserManagerCompat;
 import android.telecom.TelecomManager;
+import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -74,10 +75,11 @@
   private final PhoneNumberService mPhoneNumberService;
   // Cache info map needs to be thread-safe since it could be modified by both main thread and
   // worker thread.
-  private final Map<String, ContactCacheEntry> mInfoMap = new ConcurrentHashMap<>();
+  private final ConcurrentHashMap<String, ContactCacheEntry> mInfoMap = new ConcurrentHashMap<>();
   private final Map<String, Set<ContactInfoCacheCallback>> mCallBacks = new ArrayMap<>();
   private Drawable mDefaultContactPhotoDrawable;
   private Drawable mConferencePhotoDrawable;
+  private int mQueryId;
 
   private ContactInfoCache(Context context) {
     mContext = context;
@@ -91,7 +93,7 @@
     return sCache;
   }
 
-  public static ContactCacheEntry buildCacheEntryFromCall(
+  static ContactCacheEntry buildCacheEntryFromCall(
       Context context, DialerCall call, boolean isIncoming) {
     final ContactCacheEntry entry = new ContactCacheEntry();
 
@@ -103,7 +105,7 @@
   }
 
   /** Populate a cache entry from a call (which got converted into a caller info). */
-  public static void populateCacheEntry(
+  private static void populateCacheEntry(
       @NonNull Context context,
       @NonNull CallerInfo info,
       @NonNull ContactCacheEntry cce,
@@ -153,7 +155,7 @@
       // (Typically, we promote the phone number up to the "name" slot
       // onscreen, and possibly display a descriptive string in the
       // "number" slot.)
-      if (TextUtils.isEmpty(number)) {
+      if (TextUtils.isEmpty(number) && TextUtils.isEmpty(info.cnapName)) {
         // No name *or* number! Display a generic "unknown" string
         // (or potentially some other default based on the presentation.)
         displayName = getPresentationString(context, presentation, info.callSubject);
@@ -236,6 +238,7 @@
     cce.label = label;
     cce.isSipCall = isSipCall;
     cce.userType = info.userType;
+    cce.originalPhoneNumber = info.phoneNumber;
 
     if (info.contactExists) {
       cce.contactLookupResult = ContactLookupResult.Type.LOCAL_CONTACT;
@@ -261,11 +264,11 @@
     return name;
   }
 
-  public ContactCacheEntry getInfo(String callId) {
+  ContactCacheEntry getInfo(String callId) {
     return mInfoMap.get(callId);
   }
 
-  public void maybeInsertCnapInformationIntoCache(
+  void maybeInsertCnapInformationIntoCache(
       Context context, final DialerCall call, final CallerInfo info) {
     final CachedNumberLookupService cachedNumberLookupService =
         PhoneNumberCache.get(context).getCachedNumberLookupService();
@@ -331,8 +334,13 @@
     final ContactCacheEntry cacheEntry = mInfoMap.get(callId);
     Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
 
-    // If we have a previously obtained intermediate result return that now
-    if (cacheEntry != null) {
+    // We need to force a new query if phone number has changed.
+    boolean forceQuery = needForceQuery(call, cacheEntry);
+    Log.d(TAG, "findInfo: callId = " + callId + "; forceQuery = " + forceQuery);
+
+    // If we have a previously obtained intermediate result return that now except needs
+    // force query.
+    if (cacheEntry != null && !forceQuery) {
       Log.d(
           TAG,
           "Contact lookup. In memory cache hit; lookup "
@@ -346,14 +354,19 @@
 
     // If the entry already exists, add callback
     if (callBacks != null) {
+      Log.d(TAG, "Another query is in progress, add callback only.");
       callBacks.add(callback);
-      return;
+      if (!forceQuery) {
+        Log.d(TAG, "No need to query again, just return and wait for existing query to finish");
+        return;
+      }
+    } else {
+      Log.d(TAG, "Contact lookup. In memory cache miss; searching provider.");
+      // New lookup
+      callBacks = new ArraySet<>();
+      callBacks.add(callback);
+      mCallBacks.put(callId, callBacks);
     }
-    Log.d(TAG, "Contact lookup. In memory cache miss; searching provider.");
-    // New lookup
-    callBacks = new ArraySet<>();
-    callBacks.add(callback);
-    mCallBacks.put(callId, callBacks);
 
     /**
      * Performs a query for caller information. Save any immediate data we get from the query. An
@@ -361,25 +374,47 @@
      * such as those for voicemail and emergency call information, will not perform an additional
      * asynchronous query.
      */
+    final CallerInfoQueryToken queryToken = new CallerInfoQueryToken(mQueryId, callId);
+    mQueryId++;
     final CallerInfo callerInfo =
         CallerInfoUtils.getCallerInfoForCall(
             mContext,
             call,
             new DialerCallCookieWrapper(callId, call.getNumberPresentation()),
-            new FindInfoCallback(isIncoming));
+            new FindInfoCallback(isIncoming, queryToken));
 
-    updateCallerInfoInCacheOnAnyThread(
-        callId, call.getNumberPresentation(), callerInfo, isIncoming, false);
-    sendInfoNotifications(callId, mInfoMap.get(callId));
+    if (cacheEntry != null) {
+      // We should not override the old cache item until the new query is
+      // back. We should only update the queryId. Otherwise, we may see
+      // flicker of the name and image (old cache -> new cache before query
+      // -> new cache after query)
+      cacheEntry.queryId = queryToken.mQueryId;
+      Log.d(TAG, "There is an existing cache. Do not override until new query is back");
+    } else {
+      ContactCacheEntry initialCacheEntry =
+          updateCallerInfoInCacheOnAnyThread(
+              callId, call.getNumberPresentation(), callerInfo, isIncoming, false, queryToken);
+      sendInfoNotifications(callId, initialCacheEntry);
+    }
   }
 
   @AnyThread
-  private void updateCallerInfoInCacheOnAnyThread(
+  private ContactCacheEntry updateCallerInfoInCacheOnAnyThread(
       String callId,
       int numberPresentation,
       CallerInfo callerInfo,
       boolean isIncoming,
-      boolean didLocalLookup) {
+      boolean didLocalLookup,
+      CallerInfoQueryToken queryToken) {
+    Log.d(
+        TAG,
+        "updateCallerInfoInCacheOnAnyThread: callId = "
+            + callId
+            + "; queryId = "
+            + queryToken.mQueryId
+            + "; didLocalLookup = "
+            + didLocalLookup);
+
     int presentationMode = numberPresentation;
     if (callerInfo.contactExists
         || callerInfo.isEmergencyNumber()
@@ -387,38 +422,57 @@
       presentationMode = TelecomManager.PRESENTATION_ALLOWED;
     }
 
-    synchronized (mInfoMap) {
-      ContactCacheEntry cacheEntry = mInfoMap.get(callId);
-      // Ensure we always have a cacheEntry. Replace the existing entry if
-      // it has no name or if we found a local contact.
-      if (cacheEntry == null
-          || TextUtils.isEmpty(cacheEntry.namePrimary)
-          || callerInfo.contactExists) {
-        cacheEntry = buildEntry(mContext, callerInfo, presentationMode, isIncoming);
-        mInfoMap.put(callId, cacheEntry);
-      }
-      if (didLocalLookup) {
-        // Before issuing a request for more data from other services, we only check that the
-        // contact wasn't found in the local DB.  We don't check the if the cache entry already
-        // has a name because we allow overriding cnap data with data from other services.
-        if (!callerInfo.contactExists && mPhoneNumberService != null) {
-          Log.d(TAG, "Contact lookup. Local contacts miss, checking remote");
-          final PhoneNumberServiceListener listener = new PhoneNumberServiceListener(callId);
-          mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, isIncoming);
-        } else if (cacheEntry.displayPhotoUri != null) {
-          Log.d(TAG, "Contact lookup. Local contact found, starting image load");
-          // Load the image with a callback to update the image state.
-          // When the load is finished, onImageLoadComplete() will be called.
-          cacheEntry.hasPhotoToLoad = true;
-          ContactsAsyncHelper.startObtainPhotoAsync(
-              TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
-              mContext,
-              cacheEntry.displayPhotoUri,
-              ContactInfoCache.this,
-              callId);
+    // We always replace the entry. The only exception is the same photo case.
+    ContactCacheEntry cacheEntry = buildEntry(mContext, callerInfo, presentationMode, isIncoming);
+    cacheEntry.queryId = queryToken.mQueryId;
+
+    ContactCacheEntry existingCacheEntry = mInfoMap.get(callId);
+    Log.d(TAG, "Existing cacheEntry in hashMap " + existingCacheEntry);
+
+    if (didLocalLookup) {
+      // Before issuing a request for more data from other services, we only check that the
+      // contact wasn't found in the local DB.  We don't check the if the cache entry already
+      // has a name because we allow overriding cnap data with data from other services.
+      if (!callerInfo.contactExists && mPhoneNumberService != null) {
+        Log.d(TAG, "Contact lookup. Local contacts miss, checking remote");
+        final PhoneNumberServiceListener listener =
+            new PhoneNumberServiceListener(callId, queryToken.mQueryId);
+        cacheEntry.hasPendingQuery = true;
+        mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, isIncoming);
+      } else if (cacheEntry.displayPhotoUri != null) {
+        // When the difference between 2 numbers is only the prefix (e.g. + or IDD),
+        // we will still trigger force query so that the number can be updated on
+        // the calling screen. We need not query the image again if the previous
+        // query already has the image to avoid flickering.
+        if (existingCacheEntry != null
+            && existingCacheEntry.displayPhotoUri != null
+            && existingCacheEntry.displayPhotoUri.equals(cacheEntry.displayPhotoUri)
+            && existingCacheEntry.photo != null) {
+          Log.d(TAG, "Same picture. Do not need start image load.");
+          cacheEntry.photo = existingCacheEntry.photo;
+          cacheEntry.photoType = existingCacheEntry.photoType;
+          return cacheEntry;
         }
+
+        Log.d(TAG, "Contact lookup. Local contact found, starting image load");
+        // Load the image with a callback to update the image state.
+        // When the load is finished, onImageLoadComplete() will be called.
+        cacheEntry.hasPendingQuery = true;
+        ContactsAsyncHelper.startObtainPhotoAsync(
+            TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
+            mContext,
+            cacheEntry.displayPhotoUri,
+            ContactInfoCache.this,
+            queryToken);
       }
+      Log.d(TAG, "put entry into map: " + cacheEntry);
+      mInfoMap.put(callId, cacheEntry);
+    } else {
+      // Don't overwrite if there is existing cache.
+      Log.d(TAG, "put entry into map if not exists: " + cacheEntry);
+      mInfoMap.putIfAbsent(callId, cacheEntry);
     }
+    return cacheEntry;
   }
 
   /**
@@ -429,35 +483,42 @@
   @Override
   public void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
     Assert.isWorkerThread();
+    CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
+    final String callId = myCookie.mCallId;
+    final int queryId = myCookie.mQueryId;
+    if (!isWaitingForThisQuery(callId, queryId)) {
+      return;
+    }
     loadImage(photo, photoIcon, cookie);
   }
 
   private void loadImage(Drawable photo, Bitmap photoIcon, Object cookie) {
-    Log.d(this, "Image load complete with context: ", mContext);
+    Log.d(TAG, "Image load complete with context: ", mContext);
     // TODO: may be nice to update the image view again once the newer one
     // is available on contacts database.
-    String callId = (String) cookie;
+    CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
+    final String callId = myCookie.mCallId;
     ContactCacheEntry entry = mInfoMap.get(callId);
 
     if (entry == null) {
-      Log.e(this, "Image Load received for empty search entry.");
+      Log.e(TAG, "Image Load received for empty search entry.");
       clearCallbacks(callId);
       return;
     }
 
-    Log.d(this, "setting photo for entry: ", entry);
+    Log.d(TAG, "setting photo for entry: ", entry);
 
     // Conference call icons are being handled in CallCardPresenter.
     if (photo != null) {
-      Log.v(this, "direct drawable: ", photo);
+      Log.v(TAG, "direct drawable: ", photo);
       entry.photo = photo;
       entry.photoType = ContactPhotoType.CONTACT;
     } else if (photoIcon != null) {
-      Log.v(this, "photo icon: ", photoIcon);
+      Log.v(TAG, "photo icon: ", photoIcon);
       entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
       entry.photoType = ContactPhotoType.CONTACT;
     } else {
-      Log.v(this, "unknown photo");
+      Log.v(TAG, "unknown photo");
       entry.photo = null;
       entry.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
     }
@@ -471,9 +532,13 @@
   @Override
   public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
     Assert.isMainThread();
-    String callId = (String) cookie;
-    ContactCacheEntry entry = mInfoMap.get(callId);
-    sendImageNotifications(callId, entry);
+    CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
+    final String callId = myCookie.mCallId;
+    final int queryId = myCookie.mQueryId;
+    if (!isWaitingForThisQuery(callId, queryId)) {
+      return;
+    }
+    sendImageNotifications(callId, mInfoMap.get(callId));
 
     clearCallbacks(callId);
   }
@@ -482,6 +547,7 @@
   public void clearCache() {
     mInfoMap.clear();
     mCallBacks.clear();
+    mQueryId = 0;
   }
 
   private ContactCacheEntry buildEntry(
@@ -500,9 +566,6 @@
         cce.photo = getDefaultContactPhotoDrawable();
         cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
       }
-    } else if (info.contactDisplayPhotoUri == null) {
-      cce.photo = getDefaultContactPhotoDrawable();
-      cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
     } else {
       cce.displayPhotoUri = info.contactDisplayPhotoUri;
       cce.photo = null;
@@ -528,7 +591,9 @@
   }
 
   /** Sends the updated information to call the callbacks for the entry. */
+  @MainThread
   private void sendInfoNotifications(String callId, ContactCacheEntry entry) {
+    Assert.isMainThread();
     final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
     if (callBacks != null) {
       for (ContactInfoCacheCallback callBack : callBacks) {
@@ -537,7 +602,9 @@
     }
   }
 
+  @MainThread
   private void sendImageNotifications(String callId, ContactCacheEntry entry) {
+    Assert.isMainThread();
     final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
     if (callBacks != null && entry.photo != null) {
       for (ContactInfoCacheCallback callBack : callBacks) {
@@ -583,21 +650,26 @@
     public String location;
     public String label;
     public Drawable photo;
-    @ContactPhotoType public int photoType;
-    public boolean isSipCall;
+    @ContactPhotoType int photoType;
+    boolean isSipCall;
     // Note in cache entry whether this is a pending async loading action to know whether to
     // wait for its callback or not.
-    public boolean hasPhotoToLoad;
+    boolean hasPendingQuery;
     /** This will be used for the "view" notification. */
     public Uri contactUri;
     /** Either a display photo or a thumbnail URI. */
-    public Uri displayPhotoUri;
+    Uri displayPhotoUri;
 
     public Uri lookupUri; // Sent to NotificationMananger
     public String lookupKey;
     public int contactLookupResult = ContactLookupResult.Type.NOT_FOUND;
     public long userType = ContactsUtils.USER_TYPE_CURRENT;
-    public Uri contactRingtoneUri;
+    Uri contactRingtoneUri;
+    /** Query id to identify the query session. */
+    int queryId;
+    /** The phone number without any changes to display to the user (ex: cnap...) */
+    String originalPhoneNumber;
+    boolean isBusiness;
 
     @Override
     public String toString() {
@@ -631,6 +703,10 @@
           + userType
           + ", contactRingtoneUri="
           + contactRingtoneUri
+          + ", queryId="
+          + queryId
+          + ", originalPhoneNumber="
+          + originalPhoneNumber
           + '}';
     }
   }
@@ -648,16 +724,22 @@
   private class FindInfoCallback implements OnQueryCompleteListener {
 
     private final boolean mIsIncoming;
+    private final CallerInfoQueryToken mQueryToken;
 
-    public FindInfoCallback(boolean isIncoming) {
+    public FindInfoCallback(boolean isIncoming, CallerInfoQueryToken queryToken) {
       mIsIncoming = isIncoming;
+      mQueryToken = queryToken;
     }
 
     @Override
     public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
       Assert.isWorkerThread();
       DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
-      updateCallerInfoInCacheOnAnyThread(cw.callId, cw.numberPresentation, ci, mIsIncoming, true);
+      if (!isWaitingForThisQuery(cw.callId, mQueryToken.mQueryId)) {
+        return;
+      }
+      updateCallerInfoInCacheOnAnyThread(
+          cw.callId, cw.numberPresentation, ci, mIsIncoming, true, mQueryToken);
     }
 
     @Override
@@ -665,6 +747,9 @@
       Assert.isMainThread();
       DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
       String callId = cw.callId;
+      if (!isWaitingForThisQuery(cw.callId, mQueryToken.mQueryId)) {
+        return;
+      }
       ContactCacheEntry cacheEntry = mInfoMap.get(callId);
       // This may happen only when InCallPresenter attempt to cleanup.
       if (cacheEntry == null) {
@@ -673,7 +758,7 @@
         return;
       }
       sendInfoNotifications(callId, cacheEntry);
-      if (!cacheEntry.hasPhotoToLoad) {
+      if (!cacheEntry.hasPendingQuery) {
         if (callerInfo.contactExists) {
           Log.d(TAG, "Contact lookup done. Local contact found, no image.");
         } else {
@@ -691,13 +776,20 @@
       implements PhoneNumberService.NumberLookupListener, PhoneNumberService.ImageLookupListener {
 
     private final String mCallId;
+    private final int mQueryIdOfRemoteLookup;
 
-    PhoneNumberServiceListener(String callId) {
+    PhoneNumberServiceListener(String callId, int queryId) {
       mCallId = callId;
+      mQueryIdOfRemoteLookup = queryId;
     }
 
     @Override
     public void onPhoneNumberInfoComplete(final PhoneNumberService.PhoneNumberInfo info) {
+      Log.d(TAG, "PhoneNumberServiceListener.onPhoneNumberInfoComplete");
+      if (!isWaitingForThisQuery(mCallId, mQueryIdOfRemoteLookup)) {
+        return;
+      }
+
       // If we got a miss, this is the end of the lookup pipeline,
       // so clear the callbacks and return.
       if (info == null) {
@@ -705,11 +797,11 @@
         clearCallbacks(mCallId);
         return;
       }
-
       ContactCacheEntry entry = new ContactCacheEntry();
       entry.namePrimary = info.getDisplayName();
       entry.number = info.getNumber();
       entry.contactLookupResult = info.getLookupSource();
+      entry.isBusiness = info.isBusiness();
       final int type = info.getPhoneType();
       final String label = info.getPhoneLabel();
       if (type == Phone.TYPE_CUSTOM) {
@@ -718,33 +810,32 @@
         final CharSequence typeStr = Phone.getTypeLabel(mContext.getResources(), type, label);
         entry.label = typeStr == null ? null : typeStr.toString();
       }
-      synchronized (mInfoMap) {
-        final ContactCacheEntry oldEntry = mInfoMap.get(mCallId);
-        if (oldEntry != null) {
-          // Location is only obtained from local lookup so persist
-          // the value for remote lookups. Once we have a name this
-          // field is no longer used; it is persisted here in case
-          // the UI is ever changed to use it.
-          entry.location = oldEntry.location;
-          // Contact specific ringtone is obtained from local lookup.
-          entry.contactRingtoneUri = oldEntry.contactRingtoneUri;
-        }
-
-        // If no image and it's a business, switch to using the default business avatar.
-        if (info.getImageUrl() == null && info.isBusiness()) {
-          Log.d(TAG, "Business has no image. Using default.");
-          entry.photo = mContext.getResources().getDrawable(R.drawable.img_business);
-          entry.photoType = ContactPhotoType.BUSINESS;
-        }
-
-        mInfoMap.put(mCallId, entry);
+      final ContactCacheEntry oldEntry = mInfoMap.get(mCallId);
+      if (oldEntry != null) {
+        // Location is only obtained from local lookup so persist
+        // the value for remote lookups. Once we have a name this
+        // field is no longer used; it is persisted here in case
+        // the UI is ever changed to use it.
+        entry.location = oldEntry.location;
+        // Contact specific ringtone is obtained from local lookup.
+        entry.contactRingtoneUri = oldEntry.contactRingtoneUri;
       }
+
+      // If no image and it's a business, switch to using the default business avatar.
+      if (info.getImageUrl() == null && info.isBusiness()) {
+        Log.d(TAG, "Business has no image. Using default.");
+        entry.photo = mContext.getResources().getDrawable(R.drawable.img_business);
+        entry.photoType = ContactPhotoType.BUSINESS;
+      }
+
+      Log.d(TAG, "put entry into map: " + entry);
+      mInfoMap.put(mCallId, entry);
       sendInfoNotifications(mCallId, entry);
 
-      entry.hasPhotoToLoad = info.getImageUrl() != null;
+      entry.hasPendingQuery = info.getImageUrl() != null;
 
       // If there is no image then we should not expect another callback.
-      if (!entry.hasPhotoToLoad) {
+      if (!entry.hasPendingQuery) {
         // We're done, so clear callbacks
         clearCallbacks(mCallId);
       }
@@ -752,8 +843,59 @@
 
     @Override
     public void onImageFetchComplete(Bitmap bitmap) {
-      loadImage(null, bitmap, mCallId);
-      onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, mCallId);
+      Log.d(TAG, "PhoneNumberServiceListener.onImageFetchComplete");
+      if (!isWaitingForThisQuery(mCallId, mQueryIdOfRemoteLookup)) {
+        return;
+      }
+      CallerInfoQueryToken queryToken = new CallerInfoQueryToken(mQueryIdOfRemoteLookup, mCallId);
+      loadImage(null, bitmap, queryToken);
+      onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, queryToken);
+    }
+  }
+
+  private boolean needForceQuery(DialerCall call, ContactCacheEntry cacheEntry) {
+    if (call == null || call.isConferenceCall()) {
+      return false;
+    }
+
+    String newPhoneNumber = PhoneNumberUtils.stripSeparators(call.getNumber());
+    if (cacheEntry == null) {
+      // No info in the map yet so it is the 1st query
+      Log.d(TAG, "needForceQuery: first query");
+      return true;
+    }
+    String oldPhoneNumber = PhoneNumberUtils.stripSeparators(cacheEntry.originalPhoneNumber);
+
+    if (!TextUtils.equals(oldPhoneNumber, newPhoneNumber)) {
+      Log.d(TAG, "phone number has changed: " + oldPhoneNumber + " -> " + newPhoneNumber);
+      return true;
+    }
+
+    return false;
+  }
+
+  private static final class CallerInfoQueryToken {
+    final int mQueryId;
+    final String mCallId;
+
+    CallerInfoQueryToken(int queryId, String callId) {
+      mQueryId = queryId;
+      mCallId = callId;
+    }
+  }
+
+  /** Check if the queryId in the cached map is the same as the one from query result. */
+  private boolean isWaitingForThisQuery(String callId, int queryId) {
+    final ContactCacheEntry existingCacheEntry = mInfoMap.get(callId);
+    if (existingCacheEntry == null) {
+      // This might happen if lookup on background thread comes back before the initial entry is
+      // created.
+      Log.d(TAG, "Cached entry is null.");
+      return true;
+    } else {
+      int waitingQueryId = existingCacheEntry.queryId;
+      Log.d(TAG, "waitingQueryId = " + waitingQueryId + "; queryId = " + queryId);
+      return waitingQueryId == queryId;
     }
   }
 }
diff --git a/java/com/android/incallui/ExternalCallNotifier.java b/java/com/android/incallui/ExternalCallNotifier.java
index 466e12a..6ec94a6 100644
--- a/java/com/android/incallui/ExternalCallNotifier.java
+++ b/java/com/android/incallui/ExternalCallNotifier.java
@@ -41,6 +41,8 @@
 import com.android.contacts.common.preference.ContactsPreferences;
 import com.android.contacts.common.util.BitmapUtil;
 import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
 import com.android.incallui.call.DialerCall;
 import com.android.incallui.call.DialerCallDelegate;
 import com.android.incallui.call.ExternalCallList;
@@ -57,9 +59,9 @@
 public class ExternalCallNotifier implements ExternalCallList.ExternalCallListener {
 
   /** Tag used with the notification manager to uniquely identify external call notifications. */
-  private static final String NOTIFICATION_TAG = "EXTERNAL_CALL";
+  private static final int NOTIFICATION_ID = R.id.notification_external_call;
 
-  private static final int SUMMARY_ID = -1;
+  private static final String NOTIFICATION_GROUP = "ExternalCallNotifier";
   private final Context mContext;
   private final ContactInfoCache mContactInfoCache;
   private Map<Call, NotificationInfo> mNotifications = new ArrayMap<>();
@@ -186,14 +188,15 @@
 
     NotificationManager notificationManager =
         (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
-    notificationManager.cancel(NOTIFICATION_TAG, mNotifications.get(call).getNotificationId());
+    notificationManager.cancel(
+        String.valueOf(mNotifications.get(call).getNotificationId()), NOTIFICATION_ID);
 
     mNotifications.remove(call);
 
     if (mShowingSummary && mNotifications.size() <= 1) {
       // Where a summary notification is showing and there is now not enough notifications to
       // necessitate a summary, cancel the summary.
-      notificationManager.cancel(NOTIFICATION_TAG, SUMMARY_ID);
+      notificationManager.cancel(NOTIFICATION_GROUP, NOTIFICATION_ID);
       mShowingSummary = false;
 
       // If there is still a single call requiring a notification, re-post the notification as a
@@ -234,7 +237,7 @@
     builder.setOngoing(true);
     // Make the notification prioritized over the other normal notifications.
     builder.setPriority(Notification.PRIORITY_HIGH);
-    builder.setGroup(NOTIFICATION_TAG);
+    builder.setGroup(NOTIFICATION_GROUP);
 
     boolean isVideoCall = VideoProfile.isVideo(info.getCall().getDetails().getVideoState());
     // Set the content ("Ongoing call on another device")
@@ -249,6 +252,9 @@
     builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
     builder.addPerson(info.getPersonReference());
 
+    NotificationChannelManager.applyChannel(
+        builder, mContext, Channel.EXTERNAL_CALL, info.getCall().getDetails().getAccountHandle());
+
     // Where the external call supports being transferred to the local device, add an action
     // to the notification to initiate the call pull process.
     if (CallCompat.canPullExternalCall(info.getCall())) {
@@ -281,12 +287,19 @@
     publicBuilder.setSmallIcon(R.drawable.quantum_ic_call_white_24);
     publicBuilder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
 
+    NotificationChannelManager.applyChannel(
+        publicBuilder,
+        mContext,
+        Channel.EXTERNAL_CALL,
+        info.getCall().getDetails().getAccountHandle());
+
     builder.setPublicVersion(publicBuilder.build());
     Notification notification = builder.build();
 
     NotificationManager notificationManager =
         (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
-    notificationManager.notify(NOTIFICATION_TAG, info.getNotificationId(), notification);
+    notificationManager.notify(
+        String.valueOf(info.getNotificationId()), NOTIFICATION_ID, notification);
 
     if (!mShowingSummary && mNotifications.size() > 1) {
       // If the number of notifications shown is > 1, and we're not already showing a group summary,
@@ -297,10 +310,12 @@
       summary.setOngoing(true);
       // Make the notification prioritized over the other normal notifications.
       summary.setPriority(Notification.PRIORITY_HIGH);
-      summary.setGroup(NOTIFICATION_TAG);
+      summary.setGroup(NOTIFICATION_GROUP);
       summary.setGroupSummary(true);
       summary.setSmallIcon(R.drawable.quantum_ic_call_white_24);
-      notificationManager.notify(NOTIFICATION_TAG, SUMMARY_ID, summary.build());
+      NotificationChannelManager.applyChannel(
+          summary, mContext, Channel.EXTERNAL_CALL, info.getCall().getDetails().getAccountHandle());
+      notificationManager.notify(NOTIFICATION_GROUP, NOTIFICATION_ID, summary.build());
       mShowingSummary = true;
     }
   }
diff --git a/java/com/android/incallui/InCallActivity.java b/java/com/android/incallui/InCallActivity.java
index 3074159..7c43948 100644
--- a/java/com/android/incallui/InCallActivity.java
+++ b/java/com/android/incallui/InCallActivity.java
@@ -32,6 +32,7 @@
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.View;
+import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.compat.ActivityCompat;
 import com.android.dialer.logging.Logger;
@@ -44,7 +45,6 @@
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
 import com.android.incallui.call.DialerCall.State;
-import com.android.incallui.call.VideoUtils;
 import com.android.incallui.incall.bindings.InCallBindings;
 import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
 import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory;
@@ -89,11 +89,7 @@
   }
 
   public static Intent getIntent(
-      Context context,
-      boolean showDialpad,
-      boolean newOutgoingCall,
-      boolean isVideoCall,
-      boolean isForFullScreen) {
+      Context context, boolean showDialpad, boolean newOutgoingCall, boolean isForFullScreen) {
     Intent intent = new Intent(Intent.ACTION_MAIN, null);
     intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK);
     intent.setClass(context, InCallActivity.class);
@@ -192,7 +188,22 @@
   @Override
   public void finish() {
     if (shouldCloseActivityOnFinish()) {
-      super.finish();
+      // When user select incall ui from recents after the call is disconnected, it tries to launch
+      // a new InCallActivity but InCallPresenter is already teared down at this point, which causes
+      // crash.
+      // By calling finishAndRemoveTask() instead of finish() the task associated with
+      // InCallActivity is cleared completely. So system won't try to create a new InCallActivity in
+      // this case.
+      //
+      // Calling finish won't clear the task and normally when an activity finishes it shouldn't
+      // clear the task since there could be parent activity in the same task that's still alive.
+      // But InCallActivity is special since it's singleInstance which means it's root activity and
+      // only instance of activity in the task. So it should be safe to also remove task when
+      // finishing.
+      // It's also necessary in the sense of it's excluded from recents. So whenever the activity
+      // finishes, the task should also be removed since it doesn't make sense to go back to it in
+      // anyway anymore.
+      super.finishAndRemoveTask();
     }
   }
 
@@ -260,18 +271,12 @@
 
   @Override
   public boolean onKeyUp(int keyCode, KeyEvent event) {
-    if (common.onKeyUp(keyCode, event)) {
-      return true;
-    }
-    return super.onKeyUp(keyCode, event);
+    return common.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
   }
 
   @Override
   public boolean onKeyDown(int keyCode, KeyEvent event) {
-    if (common.onKeyDown(keyCode, event)) {
-      return true;
-    }
-    return super.onKeyDown(keyCode, event);
+    return common.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
   }
 
   public boolean isInCallScreenAnimating() {
@@ -411,13 +416,6 @@
     common.setExcludeFromRecents(exclude);
   }
 
-  public void onResolveIntent(
-      DialerCall outgoingCall, boolean isNewOutgoingCall, boolean didShowAccountSelectionDialog) {
-    if (didShowAccountSelectionDialog) {
-      hideMainInCallFragment();
-    }
-  }
-
   @Nullable
   public FragmentManager getDialpadFragmentManager() {
     InCallScreen inCallScreen = getInCallScreen();
@@ -488,7 +486,7 @@
     enableInCallOrientationEventListener(allowOrientationChange);
   }
 
-  private void hideMainInCallFragment() {
+  public void hideMainInCallFragment() {
     LogUtil.i("InCallActivity.hideMainInCallFragment", "");
     if (didShowInCallScreen || didShowVideoCallScreen) {
       FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
@@ -513,8 +511,8 @@
     }
 
     isInShowMainInCallFragment = true;
-    ShouldShowAnswerUiResult shouldShowAnswerUi = getShouldShowAnswerUi();
-    boolean shouldShowVideoUi = getShouldShowVideoUi();
+    ShouldShowUiResult shouldShowAnswerUi = getShouldShowAnswerUi();
+    ShouldShowUiResult shouldShowVideoUi = getShouldShowVideoUi();
     LogUtil.i(
         "InCallActivity.showMainInCallFragment",
         "shouldShowAnswerUi: %b, shouldShowVideoUi: %b, "
@@ -525,7 +523,7 @@
         didShowInCallScreen,
         didShowVideoCallScreen);
     // Only video call ui allows orientation change.
-    setAllowOrientationChange(shouldShowVideoUi);
+    setAllowOrientationChange(shouldShowVideoUi.shouldShow);
 
     FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
     boolean didChangeInCall;
@@ -535,9 +533,9 @@
       didChangeInCall = hideInCallScreenFragment(transaction);
       didChangeVideo = hideVideoCallScreenFragment(transaction);
       didChangeAnswer = showAnswerScreenFragment(transaction, shouldShowAnswerUi.call);
-    } else if (shouldShowVideoUi) {
+    } else if (shouldShowVideoUi.shouldShow) {
       didChangeInCall = hideInCallScreenFragment(transaction);
-      didChangeVideo = showVideoCallScreenFragment(transaction);
+      didChangeVideo = showVideoCallScreenFragment(transaction, shouldShowVideoUi.call);
       didChangeAnswer = hideAnswerScreenFragment(transaction);
     } else {
       didChangeInCall = showInCallScreenFragment(transaction);
@@ -552,17 +550,17 @@
     isInShowMainInCallFragment = false;
   }
 
-  private ShouldShowAnswerUiResult getShouldShowAnswerUi() {
+  private ShouldShowUiResult getShouldShowAnswerUi() {
     DialerCall call = CallList.getInstance().getIncomingCall();
     if (call != null) {
       LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found incoming call");
-      return new ShouldShowAnswerUiResult(true, call);
+      return new ShouldShowUiResult(true, call);
     }
 
     call = CallList.getInstance().getVideoUpgradeRequestCall();
     if (call != null) {
       LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found video upgrade request");
-      return new ShouldShowAnswerUiResult(true, call);
+      return new ShouldShowUiResult(true, call);
     }
 
     // Check if we're showing the answer screen and the call is disconnected. If this condition is
@@ -574,30 +572,30 @@
     }
     if (didShowAnswerScreen && (call == null || call.getState() == State.DISCONNECTED)) {
       LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found disconnecting incoming call");
-      return new ShouldShowAnswerUiResult(true, call);
+      return new ShouldShowUiResult(true, call);
     }
 
-    return new ShouldShowAnswerUiResult(false, null);
+    return new ShouldShowUiResult(false, null);
   }
 
-  private boolean getShouldShowVideoUi() {
+  private static ShouldShowUiResult getShouldShowVideoUi() {
     DialerCall call = CallList.getInstance().getFirstCall();
     if (call == null) {
       LogUtil.i("InCallActivity.getShouldShowVideoUi", "null call");
-      return false;
+      return new ShouldShowUiResult(false, null);
     }
 
-    if (VideoUtils.isVideoCall(call)) {
+    if (call.isVideoCall()) {
       LogUtil.i("InCallActivity.getShouldShowVideoUi", "found video call");
-      return true;
+      return new ShouldShowUiResult(true, call);
     }
 
-    if (VideoUtils.hasSentVideoUpgradeRequest(call)) {
+    if (call.hasSentVideoUpgradeRequest()) {
       LogUtil.i("InCallActivity.getShouldShowVideoUi", "upgrading to video");
-      return true;
+      return new ShouldShowUiResult(true, call);
     }
 
-    return false;
+    return new ShouldShowUiResult(false, null);
   }
 
   private boolean showAnswerScreenFragment(FragmentTransaction transaction, DialerCall call) {
@@ -607,14 +605,15 @@
       return false;
     }
 
-    boolean isVideoUpgradeRequest = VideoUtils.hasReceivedVideoUpgradeRequest(call);
-    int videoState = isVideoUpgradeRequest ? call.getRequestedVideoState() : call.getVideoState();
+    Assert.checkArgument(call != null, "didShowAnswerScreen was false but call was still null");
+
+    boolean isVideoUpgradeRequest = call.hasReceivedVideoUpgradeRequest();
 
     // Check if we're already showing an answer screen for this call.
     if (didShowAnswerScreen) {
       AnswerScreen answerScreen = getAnswerScreen();
       if (answerScreen.getCallId().equals(call.getId())
-          && answerScreen.getVideoState() == videoState
+          && answerScreen.isVideoCall() == call.isVideoCall()
           && answerScreen.isVideoUpgradeRequest() == isVideoUpgradeRequest) {
         return false;
       }
@@ -626,7 +625,7 @@
 
     // Show a new answer screen.
     AnswerScreen answerScreen =
-        AnswerBindings.createAnswerScreen(call.getId(), videoState, isVideoUpgradeRequest);
+        AnswerBindings.createAnswerScreen(call.getId(), call.isVideoCall(), isVideoUpgradeRequest);
     transaction.add(R.id.main, answerScreen.getAnswerScreenFragment(), TAG_ANSWER_SCREEN);
 
     Logger.get(this).logScreenView(ScreenEvent.Type.INCOMING_CALL, this);
@@ -675,12 +674,21 @@
     return true;
   }
 
-  private boolean showVideoCallScreenFragment(FragmentTransaction transaction) {
+  private boolean showVideoCallScreenFragment(FragmentTransaction transaction, DialerCall call) {
     if (didShowVideoCallScreen) {
-      return false;
+      VideoCallScreen videoCallScreen = getVideoCallScreen();
+      if (videoCallScreen.getCallId().equals(call.getId())) {
+        return false;
+      }
+      LogUtil.i(
+          "InCallActivity.showVideoCallScreenFragment",
+          "video call fragment exists but arguments do not match");
+      hideVideoCallScreenFragment(transaction);
     }
 
-    VideoCallScreen videoCallScreen = VideoBindings.createVideoCallScreen();
+    LogUtil.i("InCallActivity.showVideoCallScreenFragment", "call: %s", call);
+
+    VideoCallScreen videoCallScreen = VideoBindings.createVideoCallScreen(call.getId());
     transaction.add(R.id.main, videoCallScreen.getVideoCallScreenFragment(), TAG_VIDEO_CALL_SCREEN);
 
     Logger.get(this).logScreenView(ScreenEvent.Type.INCALL, this);
@@ -744,11 +752,11 @@
     return super.dispatchTouchEvent(event);
   }
 
-  private static class ShouldShowAnswerUiResult {
+  private static class ShouldShowUiResult {
     public final boolean shouldShow;
     public final DialerCall call;
 
-    ShouldShowAnswerUiResult(boolean shouldShow, DialerCall call) {
+    ShouldShowUiResult(boolean shouldShow, DialerCall call) {
       this.shouldShow = shouldShow;
       this.call = call;
     }
diff --git a/java/com/android/incallui/InCallActivityCommon.java b/java/com/android/incallui/InCallActivityCommon.java
index a2467dd..2cdb913 100644
--- a/java/com/android/incallui/InCallActivityCommon.java
+++ b/java/com/android/incallui/InCallActivityCommon.java
@@ -21,7 +21,6 @@
 import android.app.ActivityManager.TaskDescription;
 import android.app.AlertDialog;
 import android.app.Dialog;
-import android.app.DialogFragment;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnCancelListener;
@@ -99,6 +98,7 @@
   private String showPostCharWaitDialogCallId;
   private String showPostCharWaitDialogChars;
   private Dialog dialog;
+  private SelectPhoneAccountDialogFragment selectPhoneAccountDialogFragment;
   private InCallOrientationEventListener inCallOrientationEventListener;
   private Animation dialpadSlideInAnimation;
   private Animation dialpadSlideOutAnimation;
@@ -496,11 +496,15 @@
     }
   }
 
-  public void dismissPendingDialogs() {
+  void dismissPendingDialogs() {
     if (dialog != null) {
       dialog.dismiss();
       dialog = null;
     }
+    if (selectPhoneAccountDialogFragment != null) {
+      selectPhoneAccountDialogFragment.dismiss();
+      selectPhoneAccountDialogFragment = null;
+    }
   }
 
   private static boolean shouldShowDisconnectErrorDialog(@NonNull DisconnectCause cause) {
@@ -769,9 +773,7 @@
       outgoingCall = CallList.getInstance().getPendingOutgoingCall();
     }
 
-    boolean isNewOutgoingCall = false;
     if (intent.getBooleanExtra(INTENT_EXTRA_NEW_OUTGOING_CALL, false)) {
-      isNewOutgoingCall = true;
       intent.removeExtra(INTENT_EXTRA_NEW_OUTGOING_CALL);
 
       // InCallActivity is responsible for disconnecting a new outgoing call if there
@@ -789,16 +791,18 @@
     }
 
     boolean didShowAccountSelectionDialog = maybeShowAccountSelectionDialog();
-    inCallActivity.onResolveIntent(outgoingCall, isNewOutgoingCall, didShowAccountSelectionDialog);
+    if (didShowAccountSelectionDialog) {
+      inCallActivity.hideMainInCallFragment();
+    }
   }
 
   private boolean maybeShowAccountSelectionDialog() {
-    DialerCall call = CallList.getInstance().getWaitingForAccountCall();
-    if (call == null) {
+    DialerCall waitingForAccountCall = CallList.getInstance().getWaitingForAccountCall();
+    if (waitingForAccountCall == null) {
       return false;
     }
 
-    Bundle extras = call.getIntentExtras();
+    Bundle extras = waitingForAccountCall.getIntentExtras();
     List<PhoneAccountHandle> phoneAccountHandles;
     if (extras != null) {
       phoneAccountHandles =
@@ -807,14 +811,15 @@
       phoneAccountHandles = new ArrayList<>();
     }
 
-    DialogFragment dialogFragment =
+    selectPhoneAccountDialogFragment =
         SelectPhoneAccountDialogFragment.newInstance(
             R.string.select_phone_account_for_calls,
             true,
             phoneAccountHandles,
             selectAccountListener,
-            call.getId());
-    dialogFragment.show(inCallActivity.getFragmentManager(), TAG_SELECT_ACCOUNT_FRAGMENT);
+            waitingForAccountCall.getId());
+    selectPhoneAccountDialogFragment.show(
+        inCallActivity.getFragmentManager(), TAG_SELECT_ACCOUNT_FRAGMENT);
     return true;
   }
 }
diff --git a/java/com/android/incallui/InCallPresenter.java b/java/com/android/incallui/InCallPresenter.java
index 97105fb..0f3982c 100644
--- a/java/com/android/incallui/InCallPresenter.java
+++ b/java/com/android/incallui/InCallPresenter.java
@@ -42,23 +42,22 @@
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.logging.nano.InteractionEvent;
+import com.android.dialer.postcall.PostCall;
 import com.android.dialer.telecom.TelecomUtil;
 import com.android.dialer.util.TouchPointManager;
 import com.android.incallui.InCallOrientationEventListener.ScreenOrientation;
 import com.android.incallui.answerproximitysensor.PseudoScreenState;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
 import com.android.incallui.call.ExternalCallList;
-import com.android.incallui.call.InCallVideoCallCallbackNotifier;
 import com.android.incallui.call.TelecomAdapter;
-import com.android.incallui.call.VideoUtils;
 import com.android.incallui.latencyreport.LatencyReport;
 import com.android.incallui.legacyblocking.BlockedNumberContentObserver;
 import com.android.incallui.spam.SpamCallListListener;
 import com.android.incallui.util.TelecomCallUtil;
 import com.android.incallui.videosurface.bindings.VideoSurfaceBindings;
 import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+import com.android.incallui.videotech.VideoTech;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
@@ -74,8 +73,7 @@
  * presenters that want to listen in on the in-call state changes. TODO: This class has become more
  * of a state machine at this point. Consider renaming.
  */
-public class InCallPresenter
-    implements CallList.Listener, InCallVideoCallCallbackNotifier.SessionModificationListener {
+public class InCallPresenter implements CallList.Listener {
 
   private static final String EXTRA_FIRST_TIME_SHOWN =
       "com.android.incallui.intent.extra.FIRST_TIME_SHOWN";
@@ -173,7 +171,6 @@
   private ProximitySensor mProximitySensor;
   private final PseudoScreenState mPseudoScreenState = new PseudoScreenState();
   private boolean mServiceConnected;
-  private boolean mAccountSelectionCancelled;
   private InCallCameraManager mInCallCameraManager;
   private FilteredNumberAsyncQueryHandler mFilteredQueryHandler;
   private CallList.Listener mSpamCallListListener;
@@ -347,7 +344,6 @@
     mCallList.addListener(mSpamCallListListener);
 
     VideoPauseController.getInstance().setUp(this);
-    InCallVideoCallCallbackNotifier.getInstance().addSessionModificationListener(this);
 
     mFilteredQueryHandler = new FilteredNumberAsyncQueryHandler(context);
     mContext
@@ -376,7 +372,6 @@
 
     attemptCleanup();
     VideoPauseController.getInstance().tearDown();
-    InCallVideoCallCallbackNotifier.getInstance().removeSessionModificationListener(this);
   }
 
   private void attemptFinishActivity() {
@@ -385,12 +380,6 @@
     if (doFinish) {
       mInCallActivity.setExcludeFromRecents(true);
       mInCallActivity.finish();
-
-      if (mAccountSelectionCancelled) {
-        // This finish is a result of account selection cancellation
-        // do not include activity ending transition
-        mInCallActivity.overridePendingTransition(0, 0);
-      }
     }
   }
 
@@ -664,6 +653,19 @@
     InCallState newState = getPotentialStateFromCallList(callList);
     InCallState oldState = mInCallState;
     Log.d(this, "onCallListChange oldState= " + oldState + " newState=" + newState);
+
+    // If the user placed a call and was asked to choose the account, but then pressed "Home", the
+    // incall activity for that call will still exist (even if it's not visible). In the case of
+    // an incoming call in that situation, just disconnect that "waiting for account" call and
+    // dismiss the dialog. The same activity will be reused to handle the new incoming call. See
+    // b/33247755 for more details.
+    DialerCall waitingForAccountCall;
+    if (newState == InCallState.INCOMING
+        && (waitingForAccountCall = callList.getWaitingForAccountCall()) != null) {
+      waitingForAccountCall.disconnect();
+      mInCallActivity.dismissPendingDialogs();
+    }
+
     newState = startOrFinishUi(newState);
     Log.d(this, "onCallListChange newState changed to " + newState);
 
@@ -705,13 +707,13 @@
 
   @Override
   public void onUpgradeToVideo(DialerCall call) {
-    if (call.getSessionModificationState()
-            == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
+    if (call.getVideoTech().getSessionModificationState()
+            == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
         && mInCallState == InCallPresenter.InCallState.INCOMING) {
       LogUtil.i(
           "InCallPresenter.onUpgradeToVideo",
           "rejecting upgrade request due to existing incoming call");
-      call.declineUpgradeRequest();
+      call.getVideoTech().declineVideoRequest();
     }
 
     if (mInCallActivity != null) {
@@ -721,15 +723,15 @@
   }
 
   @Override
-  public void onSessionModificationStateChange(@SessionModificationState int newState) {
+  public void onSessionModificationStateChange(DialerCall call) {
+    int newState = call.getVideoTech().getSessionModificationState();
     LogUtil.i("InCallPresenter.onSessionModificationStateChange", "state: %d", newState);
     if (mProximitySensor == null) {
       LogUtil.i("InCallPresenter.onSessionModificationStateChange", "proximitySensor is null");
       return;
     }
     mProximitySensor.setIsAttemptingVideoCall(
-        VideoUtils.hasSentVideoUpgradeRequest(newState)
-            || VideoUtils.hasReceivedVideoUpgradeRequest(newState));
+        call.hasSentVideoUpgradeRequest() || call.hasReceivedVideoUpgradeRequest());
     if (mInCallActivity != null) {
       // Re-evaluate which fragment is being shown.
       mInCallActivity.onPrimaryCallStateChanged();
@@ -754,19 +756,10 @@
     if (call.isEmergencyCall()) {
       FilteredNumbersUtil.recordLastEmergencyCallTime(mContext);
     }
-  }
 
-  @Override
-  public void onUpgradeToVideoRequest(DialerCall call, int videoState) {
-    LogUtil.d(
-        "InCallPresenter.onUpgradeToVideoRequest",
-        "call = " + call + " video state = " + videoState);
-
-    if (call == null) {
-      return;
+    if (!call.getLogState().isIncoming && !mCallList.hasLiveCall()) {
+      PostCall.onCallDisconnected(mContext, call.getNumber(), call.getConnectTimeMillis());
     }
-
-    call.setRequestedVideoState(videoState);
   }
 
   /** Given the call list, return the state in which the in-call screen should be. */
@@ -916,6 +909,24 @@
         && !mInCallActivity.isFinishing());
   }
 
+  private boolean isActivityVisible() {
+    return mInCallActivity != null && mInCallActivity.isVisible();
+  }
+
+  boolean shouldShowFullScreenNotification() {
+    /**
+     * This is to cover the case where the incall activity is started but in the background, e.g.
+     * when the user pressed Home from the account selection dialog or an existing call. In the case
+     * that incall activity is already visible, there's no need to configure the notification with a
+     * full screen intent.
+     */
+    LogUtil.d(
+        "InCallPresenter.shouldShowFullScreenNotification",
+        "isActivityVisible: %b",
+        isActivityVisible());
+    return !isActivityVisible();
+  }
+
   /**
    * Determines if the In-Call app is currently changing configuration.
    *
@@ -1018,7 +1029,7 @@
     // present (e.g. a call was accepted by a bluetooth or wired headset), we want to
     // bring it up the UI regardless.
     if (!isShowingInCallUi() && mInCallState != InCallState.NO_CALLS) {
-      showInCall(showDialpad, false /* newOutgoingCall */, false /* isVideoCall */);
+      showInCall(showDialpad, false /* newOutgoingCall */);
     }
   }
 
@@ -1281,7 +1292,7 @@
 
     if (showCallUi || showAccountPicker) {
       Log.i(this, "Start in call UI");
-      showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */, false);
+      showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */);
     } else if (startIncomingCallSequence) {
       Log.i(this, "Start Full Screen in call UI");
 
@@ -1332,7 +1343,7 @@
         mCallList.getActiveCall() != null && mCallList.getIncomingCall() != null;
 
     if (isCallWaiting) {
-      showInCall(false, false, false /* isVideoCall */);
+      showInCall(false, false);
     } else {
       mStatusBarNotifier.updateNotification(mCallList);
     }
@@ -1403,11 +1414,11 @@
     }
   }
 
-  public void showInCall(boolean showDialpad, boolean newOutgoingCall, boolean isVideoCall) {
+  public void showInCall(boolean showDialpad, boolean newOutgoingCall) {
     Log.i(this, "Showing InCallActivity");
     mContext.startActivity(
         InCallActivity.getIntent(
-            mContext, showDialpad, newOutgoingCall, isVideoCall, false /* forFullScreen */));
+            mContext, showDialpad, newOutgoingCall, false /* forFullScreen */));
   }
 
   public void onServiceBind() {
@@ -1441,15 +1452,11 @@
     final PhoneAccountHandle accountHandle =
         intent.getParcelableExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
     final Point touchPoint = extras.getParcelable(TouchPointManager.TOUCH_POINT);
-    int videoState =
-        extras.getInt(
-            TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, VideoProfile.STATE_AUDIO_ONLY);
 
     InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(true, accountHandle);
 
     final Intent activityIntent =
-        InCallActivity.getIntent(
-            mContext, false, true, VideoUtils.isVideoCall(videoState), false /* forFullScreen */);
+        InCallActivity.getIntent(mContext, false, true, false /* forFullScreen */);
     activityIntent.putExtra(TouchPointManager.TOUCH_POINT, touchPoint);
     mContext.startActivity(activityIntent);
   }
diff --git a/java/com/android/incallui/NotificationBroadcastReceiver.java b/java/com/android/incallui/NotificationBroadcastReceiver.java
index 5c5d255..cef1895 100644
--- a/java/com/android/incallui/NotificationBroadcastReceiver.java
+++ b/java/com/android/incallui/NotificationBroadcastReceiver.java
@@ -27,7 +27,6 @@
 import com.android.dialer.logging.nano.DialerImpression;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.VideoUtils;
 
 /**
  * Accepts broadcast Intents which will be prepared by {@link StatusBarNotifier} and thus sent from
@@ -96,7 +95,7 @@
     } else {
       DialerCall call = callList.getVideoUpgradeRequestCall();
       if (call != null) {
-        call.acceptUpgradeRequest(call.getRequestedVideoState());
+        call.getVideoTech().acceptVideoRequest();
       }
     }
   }
@@ -109,7 +108,7 @@
     } else {
       DialerCall call = callList.getVideoUpgradeRequestCall();
       if (call != null) {
-        call.declineUpgradeRequest();
+        call.getVideoTech().declineVideoRequest();
       }
     }
   }
@@ -142,10 +141,7 @@
       if (call != null) {
         call.answer(videoState);
         InCallPresenter.getInstance()
-            .showInCall(
-                false /* showDialpad */,
-                false /* newOutgoingCall */,
-                VideoUtils.isVideoCall(videoState));
+            .showInCall(false /* showDialpad */, false /* newOutgoingCall */);
       }
     }
   }
diff --git a/java/com/android/incallui/ProximitySensor.java b/java/com/android/incallui/ProximitySensor.java
index 9122062..229b58c 100644
--- a/java/com/android/incallui/ProximitySensor.java
+++ b/java/com/android/incallui/ProximitySensor.java
@@ -28,7 +28,7 @@
 import com.android.incallui.InCallPresenter.InCallState;
 import com.android.incallui.InCallPresenter.InCallStateListener;
 import com.android.incallui.call.CallList;
-import com.android.incallui.call.VideoUtils;
+import com.android.incallui.call.DialerCall;
 
 /**
  * Class manages the proximity sensor for the in-call UI. We enable the proximity sensor while the
@@ -103,7 +103,8 @@
     boolean hasOngoingCall = InCallState.INCALL == newState && callList.hasLiveCall();
     boolean isOffhook = (InCallState.OUTGOING == newState) || hasOngoingCall;
 
-    boolean isVideoCall = VideoUtils.isVideoCall(callList.getActiveCall());
+    DialerCall activeCall = callList.getActiveCall();
+    boolean isVideoCall = activeCall != null && activeCall.isVideoCall();
 
     if (isOffhook != mIsPhoneOffhook || mIsVideoCall != isVideoCall) {
       mIsPhoneOffhook = isOffhook;
diff --git a/java/com/android/incallui/StatusBarNotifier.java b/java/com/android/incallui/StatusBarNotifier.java
index c722675..d6262be 100644
--- a/java/com/android/incallui/StatusBarNotifier.java
+++ b/java/com/android/incallui/StatusBarNotifier.java
@@ -24,8 +24,10 @@
 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST;
 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL;
 
+import android.Manifest;
 import android.app.ActivityManager;
 import android.app.Notification;
+import android.app.Notification.Builder;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.Context;
@@ -34,6 +36,7 @@
 import android.graphics.BitmapFactory;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
 import android.media.AudioAttributes;
 import android.net.Uri;
 import android.os.Build.VERSION;
@@ -41,10 +44,13 @@
 import android.support.annotation.ColorRes;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.annotation.RequiresPermission;
 import android.support.annotation.StringRes;
 import android.support.annotation.VisibleForTesting;
+import android.support.v4.os.BuildCompat;
 import android.telecom.Call.Details;
 import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.text.BidiFormatter;
 import android.text.Spannable;
@@ -54,10 +60,13 @@
 import android.text.style.ForegroundColorSpan;
 import com.android.contacts.common.ContactsUtils;
 import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
 import com.android.contacts.common.preference.ContactsPreferences;
 import com.android.contacts.common.util.BitmapUtil;
 import com.android.contacts.common.util.ContactDisplayUtils;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
 import com.android.dialer.util.DrawableConverter;
 import com.android.incallui.ContactInfoCache.ContactCacheEntry;
 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
@@ -65,11 +74,13 @@
 import com.android.incallui.async.PausableExecutorImpl;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
 import com.android.incallui.call.DialerCallListener;
 import com.android.incallui.ringtone.DialerRingtoneManager;
 import com.android.incallui.ringtone.InCallTonePlayer;
 import com.android.incallui.ringtone.ToneGeneratorFactory;
+import com.android.incallui.videotech.VideoTech;
+import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
 
 /** This class adds Notifications to the status bar for the in-call experience. */
@@ -79,9 +90,9 @@
   // Indicates that no notification is currently showing.
   private static final int NOTIFICATION_NONE = 0;
   // Notification for an active call. This is non-interruptive, but cannot be dismissed.
-  private static final int NOTIFICATION_IN_CALL = 1;
+  private static final int NOTIFICATION_IN_CALL = R.id.notification_ongoing_call;
   // Notification for incoming calls. This is interruptive and will show up as a HUN.
-  private static final int NOTIFICATION_INCOMING_CALL = 2;
+  private static final int NOTIFICATION_INCOMING_CALL = R.id.notification_incoming_call;
 
   private static final int PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN = 0;
   private static final int PENDING_INTENT_REQUEST_CODE_FULL_SCREEN = 1;
@@ -101,8 +112,9 @@
   private String mSavedContentTitle;
   private Uri mRingtone;
   private StatusBarCallListener mStatusBarCallListener;
+  private boolean mShowFullScreenIntent;
 
-  public StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
+  StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
     Objects.requireNonNull(context);
     mContext = context;
     mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
@@ -120,9 +132,9 @@
    * notifications.
    */
   static void clearAllCallNotifications(Context backupContext) {
-    Log.i(
-        StatusBarNotifier.class.getSimpleName(),
-        "Something terrible happened. Clear all InCall notifications");
+    LogUtil.i(
+        "StatusBarNotifier.clearAllCallNotifications",
+        "something terrible happened, clear all InCall notifications");
 
     NotificationManager notificationManager =
         backupContext.getSystemService(NotificationManager.class);
@@ -153,10 +165,17 @@
     return PendingIntent.getBroadcast(context, 0, intent, 0);
   }
 
+  private static void setColorized(@NonNull Builder builder) {
+    if (BuildCompat.isAtLeastO()) {
+      builder.setColorized(true);
+    }
+  }
+
   /** Creates notifications according to the state we receive from {@link InCallPresenter}. */
   @Override
+  @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
   public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
-    Log.d(this, "onStateChange");
+    LogUtil.d("StatusBarNotifier.onStateChange", "%s->%s", oldState, newState);
     updateNotification(callList);
   }
 
@@ -177,7 +196,8 @@
    *
    * @see #updateInCallNotification(CallList)
    */
-  public void updateNotification(CallList callList) {
+  @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
+  void updateNotification(CallList callList) {
     updateInCallNotification(callList);
   }
 
@@ -191,7 +211,7 @@
       setStatusBarCallListener(null);
     }
     if (mCurrentNotification != NOTIFICATION_NONE) {
-      Log.d(this, "cancelInCall()...");
+      LogUtil.d("StatusBarNotifier.cancelNotification", "cancel");
       mNotificationManager.cancel(mCurrentNotification);
     }
     mCurrentNotification = NOTIFICATION_NONE;
@@ -202,8 +222,9 @@
    * status bar notification based on the current telephony state, or cancels the notification if
    * the phone is totally idle.
    */
+  @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
   private void updateInCallNotification(CallList callList) {
-    Log.d(this, "updateInCallNotification...");
+    LogUtil.d("StatusBarNotifier.updateInCallNotification", "");
 
     final DialerCall call = getCallToShow(callList);
 
@@ -214,6 +235,7 @@
     }
   }
 
+  @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
   private void showNotification(final CallList callList, final DialerCall call) {
     final boolean isIncoming =
         (call.getState() == DialerCall.State.INCOMING
@@ -230,6 +252,7 @@
         isIncoming,
         new ContactInfoCacheCallback() {
           @Override
+          @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
           public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
             DialerCall call = callList.getCallById(callId);
             if (call != null) {
@@ -239,6 +262,7 @@
           }
 
           @Override
+          @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
           public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
             DialerCall call = callList.getCallById(callId);
             if (call != null) {
@@ -249,6 +273,7 @@
   }
 
   /** Sets up the main Ui for the notification */
+  @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
   private void buildAndSendNotification(
       CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo) {
     // This can get called to update an existing notification after contact information has come
@@ -268,8 +293,8 @@
     final String contentTitle = getContentTitle(contactInfo, call);
 
     final boolean isVideoUpgradeRequest =
-        call.getSessionModificationState()
-            == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
+        call.getVideoTech().getSessionModificationState()
+            == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
     final int notificationType;
     if (callState == DialerCall.State.INCOMING
         || callState == DialerCall.State.CALL_WAITING
@@ -286,7 +311,8 @@
         contentTitle,
         callState,
         notificationType,
-        contactInfo.contactRingtoneUri)) {
+        contactInfo.contactRingtoneUri,
+        InCallPresenter.getInstance().shouldShowFullScreenNotification())) {
       return;
     }
 
@@ -300,9 +326,10 @@
     Notification.Builder publicBuilder = new Notification.Builder(mContext);
     publicBuilder
         .setSmallIcon(iconResId)
-        .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
+        .setColor(mContext.getResources().getColor(R.color.dialer_theme_color, mContext.getTheme()))
         // Hide work call state for the lock screen notification
         .setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT));
+    setColorized(publicBuilder);
     setNotificationWhen(call, callState, publicBuilder);
 
     // Builder for the notification shown when the device is unlocked or the user has set their
@@ -311,28 +338,26 @@
     builder.setPublicVersion(publicBuilder.build());
 
     // Set up the main intent to send the user to the in-call screen
-    builder.setContentIntent(
-        createLaunchPendingIntent(false /* isFullScreen */, call.isVideoCall()));
+    builder.setContentIntent(createLaunchPendingIntent(false /* isFullScreen */));
 
     // Set the intent as a full screen intent as well if a call is incoming
+    PhoneAccountHandle accountHandle = call.getAccountHandle();
+    if (accountHandle == null) {
+      accountHandle = getAnyPhoneAccount();
+    }
     if (notificationType == NOTIFICATION_INCOMING_CALL) {
-      if (!InCallPresenter.getInstance().isActivityStarted()) {
+      NotificationChannelManager.applyChannel(
+          builder, mContext, Channel.INCOMING_CALL, accountHandle);
+      if (InCallPresenter.getInstance().shouldShowFullScreenNotification()) {
         configureFullScreenIntent(
-            builder,
-            createLaunchPendingIntent(true /* isFullScreen */, call.isVideoCall()),
-            callList,
-            call);
-      } else {
-        // If the incall screen is already up, we don't want to show HUN but regular notification
-        // should still be shown. In order to do that the previous one with full screen intent
-        // needs to be cancelled.
-        LogUtil.d(
-            "StatusBarNotifier.buildAndSendNotification",
-            "cancel previous incoming call notification");
-        mNotificationManager.cancel(NOTIFICATION_INCOMING_CALL);
+            builder, createLaunchPendingIntent(true /* isFullScreen */), callList, call);
       }
-      // Set the notification category for incoming calls
+      // Set the notification category and bump the priority for incoming calls
       builder.setCategory(Notification.CATEGORY_CALL);
+      builder.setPriority(Notification.PRIORITY_MAX);
+    } else {
+      NotificationChannelManager.applyChannel(
+          builder, mContext, Channel.ONGOING_CALL, accountHandle);
     }
 
     // Set the content
@@ -340,7 +365,9 @@
     builder.setSmallIcon(iconResId);
     builder.setContentTitle(contentTitle);
     builder.setLargeIcon(largeIcon);
-    builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
+    builder.setColor(
+        mContext.getResources().getColor(R.color.dialer_theme_color, mContext.getTheme()));
+    setColorized(builder);
 
     if (isVideoUpgradeRequest) {
       builder.setUsesChronometer(false);
@@ -367,15 +394,20 @@
       }
     }
     if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) {
-      Log.v(this, "Playing call waiting tone");
+      LogUtil.v("StatusBarNotifier.buildAndSendNotification", "playing call waiting tone");
       mDialerRingtoneManager.playCallWaitingTone();
     }
     if (mCurrentNotification != notificationType && mCurrentNotification != NOTIFICATION_NONE) {
-      Log.i(this, "Previous notification already showing - cancelling " + mCurrentNotification);
+      LogUtil.i(
+          "StatusBarNotifier.buildAndSendNotification",
+          "previous notification already showing - cancelling " + mCurrentNotification);
       mNotificationManager.cancel(mCurrentNotification);
     }
 
-    Log.i(this, "Displaying notification for " + notificationType);
+    LogUtil.i(
+        "StatusBarNotifier.buildAndSendNotification",
+        "displaying notification for " + notificationType);
+
     try {
       mNotificationManager.notify(notificationType, notification);
     } catch (RuntimeException e) {
@@ -385,14 +417,32 @@
       activityManager.getMemoryInfo(memoryInfo);
       throw new RuntimeException(
           String.format(
+              Locale.US,
               "Error displaying notification with photo type: %d (low memory? %b, availMem: %d)",
-              contactInfo.photoType, memoryInfo.lowMemory, memoryInfo.availMem),
+              contactInfo.photoType,
+              memoryInfo.lowMemory,
+              memoryInfo.availMem),
           e);
     }
     call.getLatencyReport().onNotificationShown();
     mCurrentNotification = notificationType;
   }
 
+  @Nullable
+  @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
+  private PhoneAccountHandle getAnyPhoneAccount() {
+    PhoneAccountHandle accountHandle;
+    TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
+    accountHandle = telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
+    if (accountHandle == null) {
+      List<PhoneAccountHandle> accountHandles = telecomManager.getCallCapablePhoneAccounts();
+      if (!accountHandles.isEmpty()) {
+        accountHandle = accountHandles.get(0);
+      }
+    }
+    return accountHandle;
+  }
+
   private void createIncomingCallNotification(
       DialerCall call, int state, Notification.Builder builder) {
     setNotificationWhen(call, state, builder);
@@ -438,7 +488,8 @@
       String contentTitle,
       int state,
       int notificationType,
-      Uri ringtone) {
+      Uri ringtone,
+      boolean showFullScreenIntent) {
 
     // The two are different:
     // if new title is not null, it should be different from saved version OR
@@ -454,13 +505,15 @@
             || (mCallState != state)
             || (mSavedLargeIcon != largeIcon)
             || contentTitleChanged
-            || !Objects.equals(mRingtone, ringtone);
+            || !Objects.equals(mRingtone, ringtone)
+            || mShowFullScreenIntent != showFullScreenIntent;
 
     // If we aren't showing a notification right now or the notification type is changing,
     // definitely do an update.
     if (mCurrentNotification != notificationType) {
       if (mCurrentNotification == NOTIFICATION_NONE) {
-        Log.d(this, "Showing notification for first time.");
+        LogUtil.d(
+            "StatusBarNotifier.checkForChangeAndSaveData", "showing notification for first time.");
       }
       retval = true;
     }
@@ -471,9 +524,11 @@
     mSavedLargeIcon = largeIcon;
     mSavedContentTitle = contentTitle;
     mRingtone = ringtone;
+    mShowFullScreenIntent = showFullScreenIntent;
 
     if (retval) {
-      Log.d(this, "Data changed.  Showing notification");
+      LogUtil.d(
+          "StatusBarNotifier.checkForChangeAndSaveData", "data changed.  Showing notification");
     }
 
     return retval;
@@ -520,8 +575,34 @@
     if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
       largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
     }
+    if (contactInfo.photo == null) {
+      int width =
+          (int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_width);
+      int height =
+          (int)
+              mContext.getResources().getDimension(android.R.dimen.notification_large_icon_height);
+      int contactType = LetterTileDrawable.TYPE_DEFAULT;
+      LetterTileDrawable lettertile = new LetterTileDrawable(mContext.getResources());
+
+      // TODO: Deduplicate across Dialer. b/36195917
+      if (CallerInfoUtils.isVoiceMailNumber(mContext, call)) {
+        contactType = LetterTileDrawable.TYPE_VOICEMAIL;
+      } else if (contactInfo.isBusiness) {
+        contactType = LetterTileDrawable.TYPE_BUSINESS;
+      } else if (call.getNumberPresentation() == TelecomManager.PRESENTATION_RESTRICTED) {
+        contactType = LetterTileDrawable.TYPE_GENERIC_AVATAR;
+      }
+      lettertile.setCanonicalDialerLetterTileDetails(
+          contactInfo.namePrimary == null ? contactInfo.number : contactInfo.namePrimary,
+          contactInfo.lookupKey,
+          LetterTileDrawable.SHAPE_CIRCLE,
+          contactType);
+      largeIcon = lettertile.getBitmap(width, height);
+    }
+
     if (call.isSpam()) {
-      Drawable drawable = mContext.getResources().getDrawable(R.drawable.blocked_contact);
+      Drawable drawable =
+          mContext.getResources().getDrawable(R.drawable.blocked_contact, mContext.getTheme());
       largeIcon = DrawableConverter.drawableToBitmap(drawable);
     }
     return largeIcon;
@@ -552,8 +633,8 @@
     // display that regardless of the state of the other calls.
     if (call.getState() == DialerCall.State.ONHOLD) {
       return R.drawable.ic_phone_paused_white_24dp;
-    } else if (call.getSessionModificationState()
-        == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+    } else if (call.getVideoTech().getSessionModificationState()
+        == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
       return R.drawable.ic_videocam;
     }
     return R.anim.on_going_call;
@@ -594,8 +675,8 @@
       resId = R.string.notification_on_hold;
     } else if (DialerCall.State.isDialing(call.getState())) {
       resId = R.string.notification_dialing;
-    } else if (call.getSessionModificationState()
-        == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+    } else if (call.getVideoTech().getSessionModificationState()
+        == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
       resId = R.string.notification_requesting_video_call;
     }
 
@@ -639,64 +720,98 @@
   }
 
   private void addAnswerAction(Notification.Builder builder) {
-    Log.d(this, "Will show \"answer\" action in the incoming call Notification");
+    LogUtil.d(
+        "StatusBarNotifier.addAnswerAction",
+        "will show \"answer\" action in the incoming call Notification");
     PendingIntent answerVoicePendingIntent =
         createNotificationPendingIntent(mContext, ACTION_ANSWER_VOICE_INCOMING_CALL);
+    // We put animation resources in "anim" folder instead of "drawable", which causes Android
+    // Studio to complain.
+    // TODO: Move "anim" resources to "drawable" as recommended in AnimationDrawable doc?
+    //noinspection ResourceType
     builder.addAction(
-        R.anim.on_going_call,
-        getActionText(R.string.notification_action_answer, R.color.notification_action_accept),
-        answerVoicePendingIntent);
+        new Notification.Action.Builder(
+                Icon.createWithResource(mContext, R.anim.on_going_call),
+                getActionText(
+                    R.string.notification_action_answer, R.color.notification_action_accept),
+                answerVoicePendingIntent)
+            .build());
   }
 
   private void addDismissAction(Notification.Builder builder) {
-    Log.d(this, "Will show \"decline\" action in the incoming call Notification");
+    LogUtil.d(
+        "StatusBarNotifier.addDismissAction",
+        "will show \"decline\" action in the incoming call Notification");
     PendingIntent declinePendingIntent =
         createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL);
     builder.addAction(
-        R.drawable.ic_close_dk,
-        getActionText(R.string.notification_action_dismiss, R.color.notification_action_dismiss),
-        declinePendingIntent);
+        new Notification.Action.Builder(
+                Icon.createWithResource(mContext, R.drawable.ic_close_dk),
+                getActionText(
+                    R.string.notification_action_dismiss, R.color.notification_action_dismiss),
+                declinePendingIntent)
+            .build());
   }
 
   private void addHangupAction(Notification.Builder builder) {
-    Log.d(this, "Will show \"hang-up\" action in the ongoing active call Notification");
+    LogUtil.d(
+        "StatusBarNotifier.addHangupAction",
+        "will show \"hang-up\" action in the ongoing active call Notification");
     PendingIntent hangupPendingIntent =
         createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL);
     builder.addAction(
-        R.drawable.ic_call_end_white_24dp,
-        getActionText(R.string.notification_action_end_call, R.color.notification_action_end_call),
-        hangupPendingIntent);
+        new Notification.Action.Builder(
+                Icon.createWithResource(mContext, R.drawable.ic_call_end_white_24dp),
+                getActionText(
+                    R.string.notification_action_end_call, R.color.notification_action_end_call),
+                hangupPendingIntent)
+            .build());
   }
 
   private void addVideoCallAction(Notification.Builder builder) {
-    Log.i(this, "Will show \"video\" action in the incoming call Notification");
+    LogUtil.i(
+        "StatusBarNotifier.addVideoCallAction",
+        "will show \"video\" action in the incoming call Notification");
     PendingIntent answerVideoPendingIntent =
         createNotificationPendingIntent(mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL);
     builder.addAction(
-        R.drawable.ic_videocam,
-        getActionText(
-            R.string.notification_action_answer_video, R.color.notification_action_answer_video),
-        answerVideoPendingIntent);
+        new Notification.Action.Builder(
+                Icon.createWithResource(mContext, R.drawable.ic_videocam),
+                getActionText(
+                    R.string.notification_action_answer_video,
+                    R.color.notification_action_answer_video),
+                answerVideoPendingIntent)
+            .build());
   }
 
   private void addAcceptUpgradeRequestAction(Notification.Builder builder) {
-    Log.i(this, "Will show \"accept upgrade\" action in the incoming call Notification");
+    LogUtil.i(
+        "StatusBarNotifier.addAcceptUpgradeRequestAction",
+        "will show \"accept upgrade\" action in the incoming call Notification");
     PendingIntent acceptVideoPendingIntent =
         createNotificationPendingIntent(mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST);
     builder.addAction(
-        R.drawable.ic_videocam,
-        getActionText(R.string.notification_action_accept, R.color.notification_action_accept),
-        acceptVideoPendingIntent);
+        new Notification.Action.Builder(
+                Icon.createWithResource(mContext, R.drawable.ic_videocam),
+                getActionText(
+                    R.string.notification_action_accept, R.color.notification_action_accept),
+                acceptVideoPendingIntent)
+            .build());
   }
 
   private void addDismissUpgradeRequestAction(Notification.Builder builder) {
-    Log.i(this, "Will show \"dismiss upgrade\" action in the incoming call Notification");
+    LogUtil.i(
+        "StatusBarNotifier.addDismissUpgradeRequestAction",
+        "will show \"dismiss upgrade\" action in the incoming call Notification");
     PendingIntent declineVideoPendingIntent =
         createNotificationPendingIntent(mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST);
     builder.addAction(
-        R.drawable.ic_videocam,
-        getActionText(R.string.notification_action_dismiss, R.color.notification_action_dismiss),
-        declineVideoPendingIntent);
+        new Notification.Action.Builder(
+                Icon.createWithResource(mContext, R.drawable.ic_videocam),
+                getActionText(
+                    R.string.notification_action_dismiss, R.color.notification_action_dismiss),
+                declineVideoPendingIntent)
+            .build());
   }
 
   /** Adds fullscreen intent to the builder. */
@@ -707,7 +822,7 @@
     // to the status bar).  Setting fullScreenIntent will cause
     // the InCallScreen to be launched immediately *unless* the
     // current foreground activity is marked as "immersive".
-    Log.d(this, "- Setting fullScreenIntent: " + intent);
+    LogUtil.d("StatusBarNotifier.configureFullScreenIntent", "setting fullScreenIntent: " + intent);
     builder.setFullScreenIntent(intent, true);
 
     // Ugly hack alert:
@@ -740,7 +855,9 @@
                 && callList.getBackgroundCall() != null));
 
     if (isCallWaiting) {
-      Log.i(this, "updateInCallNotification: call-waiting! force relaunch...");
+      LogUtil.i(
+          "StatusBarNotifier.configureFullScreenIntent",
+          "updateInCallNotification: call-waiting! force relaunch...");
       // Cancel the IN_CALL_NOTIFICATION immediately before
       // (re)posting it; this seems to force the
       // NotificationManager to launch the fullScreenIntent.
@@ -751,21 +868,15 @@
   private Notification.Builder getNotificationBuilder() {
     final Notification.Builder builder = new Notification.Builder(mContext);
     builder.setOngoing(true);
-
-    // Make the notification prioritized over the other normal notifications.
-    builder.setPriority(Notification.PRIORITY_HIGH);
+    builder.setOnlyAlertOnce(true);
 
     return builder;
   }
 
-  private PendingIntent createLaunchPendingIntent(boolean isFullScreen, boolean isVideoCall) {
+  private PendingIntent createLaunchPendingIntent(boolean isFullScreen) {
     Intent intent =
         InCallActivity.getIntent(
-            mContext,
-            false /* showDialpad */,
-            false /* newOutgoingCall */,
-            isVideoCall,
-            isFullScreen);
+            mContext, false /* showDialpad */, false /* newOutgoingCall */, isFullScreen);
 
     int requestCode = PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN;
     if (isFullScreen) {
@@ -832,8 +943,9 @@
      * bar notification as required.
      */
     @Override
-    public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {
-      if (state == DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST) {
+    public void onDialerCallSessionModificationStateChange() {
+      if (mDialerCall.getVideoTech().getSessionModificationState()
+          == VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST) {
         cleanup();
         updateNotification(CallList.getInstance());
       }
diff --git a/java/com/android/incallui/VideoCallPresenter.java b/java/com/android/incallui/VideoCallPresenter.java
index 971b695..20dc987 100644
--- a/java/com/android/incallui/VideoCallPresenter.java
+++ b/java/com/android/incallui/VideoCallPresenter.java
@@ -21,7 +21,6 @@
 import android.graphics.Point;
 import android.os.Handler;
 import android.support.annotation.Nullable;
-import android.telecom.Connection;
 import android.telecom.InCallService.VideoCall;
 import android.telecom.VideoProfile;
 import android.telecom.VideoProfile.CameraCapabilities;
@@ -36,17 +35,18 @@
 import com.android.incallui.InCallPresenter.IncomingCallListener;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCall.CameraDirection;
 import com.android.incallui.call.DialerCall.State;
 import com.android.incallui.call.InCallVideoCallCallbackNotifier;
 import com.android.incallui.call.InCallVideoCallCallbackNotifier.SurfaceChangeListener;
-import com.android.incallui.call.InCallVideoCallCallbackNotifier.VideoEventListener;
 import com.android.incallui.call.VideoUtils;
 import com.android.incallui.util.AccessibilityUtil;
 import com.android.incallui.video.protocol.VideoCallScreen;
 import com.android.incallui.video.protocol.VideoCallScreenDelegate;
 import com.android.incallui.videosurface.protocol.VideoSurfaceDelegate;
 import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.SessionModificationState;
 import java.util.Objects;
 
 /**
@@ -78,7 +78,6 @@
         InCallStateListener,
         InCallDetailsListener,
         SurfaceChangeListener,
-        VideoEventListener,
         InCallPresenter.InCallEventListener,
         VideoCallScreenDelegate {
 
@@ -90,32 +89,6 @@
   /** The current context. */
   private Context mContext;
 
-  @Override
-  public boolean shouldShowCameraPermissionDialog() {
-    if (mPrimaryCall == null) {
-      LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "null call");
-      return false;
-    }
-    if (mPrimaryCall.didShowCameraPermission()) {
-      LogUtil.i(
-          "VideoCallPresenter.shouldShowCameraPermissionDialog", "already shown for this call");
-      return false;
-    }
-    if (!ConfigProviderBindings.get(mContext)
-        .getBoolean("camera_permission_dialog_allowed", true)) {
-      LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "disabled by config");
-      return false;
-    }
-    return !VideoUtils.hasCameraPermission(mContext) || !VideoUtils.isCameraAllowedByUser(mContext);
-  }
-
-  @Override
-  public void onCameraPermissionDialogShown() {
-    if (mPrimaryCall != null) {
-      mPrimaryCall.setDidShowCameraPermission(true);
-    }
-  }
-
   /** The call the video surfaces are currently related to */
   private DialerCall mPrimaryCall;
   /**
@@ -231,49 +204,49 @@
     // this function should never be called with null call object, however if it happens we
     // should handle it gracefully.
     if (call == null) {
-      cameraDir = DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
+      cameraDir = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
       LogUtil.e(
           "VideoCallPresenter.updateCameraSelection",
           "call is null. Setting camera direction to default value (CAMERA_DIRECTION_UNKNOWN)");
     }
 
     // Clear camera direction if this is not a video call.
-    else if (VideoUtils.isAudioCall(call) && !isVideoUpgrade(call)) {
-      cameraDir = DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
-      call.getVideoSettings().setCameraDir(cameraDir);
+    else if (isAudioCall(call) && !isVideoUpgrade(call)) {
+      cameraDir = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
+      call.setCameraDir(cameraDir);
     }
 
     // If this is a waiting video call, default to active call's camera,
     // since we don't want to change the current camera for waiting call
     // without user's permission.
-    else if (VideoUtils.isVideoCall(activeCall) && VideoUtils.isIncomingVideoCall(call)) {
-      cameraDir = activeCall.getVideoSettings().getCameraDir();
+    else if (isVideoCall(activeCall) && isIncomingVideoCall(call)) {
+      cameraDir = activeCall.getCameraDir();
     }
 
     // Infer the camera direction from the video state and store it,
     // if this is an outgoing video call.
-    else if (VideoUtils.isOutgoingVideoCall(call) && !isCameraDirectionSet(call)) {
+    else if (isOutgoingVideoCall(call) && !isCameraDirectionSet(call)) {
       cameraDir = toCameraDirection(call.getVideoState());
-      call.getVideoSettings().setCameraDir(cameraDir);
+      call.setCameraDir(cameraDir);
     }
 
     // Use the stored camera dir if this is an outgoing video call for which camera direction
     // is set.
-    else if (VideoUtils.isOutgoingVideoCall(call)) {
-      cameraDir = call.getVideoSettings().getCameraDir();
+    else if (isOutgoingVideoCall(call)) {
+      cameraDir = call.getCameraDir();
     }
 
     // Infer the camera direction from the video state and store it,
     // if this is an active video call and camera direction is not set.
-    else if (VideoUtils.isActiveVideoCall(call) && !isCameraDirectionSet(call)) {
+    else if (isActiveVideoCall(call) && !isCameraDirectionSet(call)) {
       cameraDir = toCameraDirection(call.getVideoState());
-      call.getVideoSettings().setCameraDir(cameraDir);
+      call.setCameraDir(cameraDir);
     }
 
     // Use the stored camera dir if this is an active video call for which camera direction
     // is set.
-    else if (VideoUtils.isActiveVideoCall(call)) {
-      cameraDir = call.getVideoSettings().getCameraDir();
+    else if (isActiveVideoCall(call)) {
+      cameraDir = call.getCameraDir();
     }
 
     // For all other cases infer the camera direction but don't store it in the call object.
@@ -289,20 +262,18 @@
     final InCallCameraManager cameraManager =
         InCallPresenter.getInstance().getInCallCameraManager();
     cameraManager.setUseFrontFacingCamera(
-        cameraDir == DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING);
+        cameraDir == CameraDirection.CAMERA_DIRECTION_FRONT_FACING);
   }
 
   private static int toCameraDirection(int videoState) {
     return VideoProfile.isTransmissionEnabled(videoState)
             && !VideoProfile.isBidirectional(videoState)
-        ? DialerCall.VideoSettings.CAMERA_DIRECTION_BACK_FACING
-        : DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING;
+        ? CameraDirection.CAMERA_DIRECTION_BACK_FACING
+        : CameraDirection.CAMERA_DIRECTION_FRONT_FACING;
   }
 
   private static boolean isCameraDirectionSet(DialerCall call) {
-    return VideoUtils.isVideoCall(call)
-        && call.getVideoSettings().getCameraDir()
-            != DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
+    return isVideoCall(call) && call.getCameraDir() != CameraDirection.CAMERA_DIRECTION_UNKNOWN;
   }
 
   private static String toSimpleString(DialerCall call) {
@@ -350,7 +321,6 @@
 
     // Register for surface and video events from {@link InCallVideoCallListener}s.
     InCallVideoCallCallbackNotifier.getInstance().addSurfaceChangeListener(this);
-    InCallVideoCallCallbackNotifier.getInstance().addVideoEventListener(this);
     mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY;
     mCurrentCallState = DialerCall.State.INVALID;
 
@@ -379,7 +349,6 @@
     InCallPresenter.getInstance().getLocalVideoSurfaceTexture().setDelegate(null);
 
     InCallVideoCallCallbackNotifier.getInstance().removeSurfaceChangeListener(this);
-    InCallVideoCallCallbackNotifier.getInstance().removeVideoEventListener(this);
 
     // Ensure that the call's camera direction is updated (most likely to UNKNOWN). Normally this
     // happens after any call state changes but we're unregistering from InCallPresenter above so
@@ -447,7 +416,7 @@
     showVideoUi(
         mPrimaryCall.getVideoState(),
         mPrimaryCall.getState(),
-        mPrimaryCall.getSessionModificationState(),
+        mPrimaryCall.getVideoTech().getSessionModificationState(),
         mPrimaryCall.isRemotelyHeld());
     InCallPresenter.getInstance().getInCallCameraManager().onCameraPermissionGranted();
   }
@@ -521,7 +490,7 @@
       // change the camera or UI unless the waiting VT call becomes active.
       primary = callList.getActiveCall();
       currentCall = callList.getIncomingCall();
-      if (!VideoUtils.isActiveVideoCall(primary)) {
+      if (!isActiveVideoCall(primary)) {
         primary = callList.getIncomingCall();
       }
     } else if (newState == InCallPresenter.InCallState.OUTGOING) {
@@ -564,10 +533,10 @@
     cancelAutoFullScreen();
     if (mPrimaryCall != null) {
       updateFullscreenAndGreenScreenMode(
-          mPrimaryCall.getState(), mPrimaryCall.getSessionModificationState());
+          mPrimaryCall.getState(), mPrimaryCall.getVideoTech().getSessionModificationState());
     } else {
       updateFullscreenAndGreenScreenMode(
-          State.INVALID, DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+          State.INVALID, VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
     }
   }
 
@@ -622,7 +591,7 @@
       updateCameraSelection(call);
       String newCameraId = cameraManager.getActiveCameraId();
 
-      if (!Objects.equals(prevCameraId, newCameraId) && VideoUtils.isActiveVideoCall(call)) {
+      if (!Objects.equals(prevCameraId, newCameraId) && isActiveVideoCall(call)) {
         enableCamera(call.getVideoCall(), true);
       }
     }
@@ -631,7 +600,7 @@
     showVideoUi(
         call.getVideoState(),
         call.getState(),
-        call.getSessionModificationState(),
+        call.getVideoTech().getSessionModificationState(),
         call.isRemotelyHeld());
   }
 
@@ -711,12 +680,13 @@
     checkForVideoStateChange(call);
     checkForCallStateChange(call);
     checkForOrientationAllowedChange(call);
-    updateFullscreenAndGreenScreenMode(call.getState(), call.getSessionModificationState());
+    updateFullscreenAndGreenScreenMode(
+        call.getState(), call.getVideoTech().getSessionModificationState());
   }
 
   private void checkForOrientationAllowedChange(@Nullable DialerCall call) {
     InCallPresenter.getInstance()
-        .setInCallAllowsOrientationChange(VideoUtils.isVideoCall(call) || isVideoUpgrade(call));
+        .setInCallAllowsOrientationChange(isVideoCall(call) || isVideoUpgrade(call));
   }
 
   private void updateFullscreenAndGreenScreenMode(
@@ -775,7 +745,8 @@
   private boolean isCameraRequired() {
     return mPrimaryCall != null
         && isCameraRequired(
-            mPrimaryCall.getVideoState(), mPrimaryCall.getSessionModificationState());
+            mPrimaryCall.getVideoState(),
+            mPrimaryCall.getVideoTech().getSessionModificationState());
   }
 
   /**
@@ -799,7 +770,10 @@
     }
 
     showVideoUi(
-        newVideoState, call.getState(), call.getSessionModificationState(), call.isRemotelyHeld());
+        newVideoState,
+        call.getState(),
+        call.getVideoTech().getSessionModificationState(),
+        call.isRemotelyHeld());
 
     // Communicate the current camera to telephony and make a request for the camera
     // capabilities.
@@ -814,7 +788,9 @@
       Assert.checkState(
           mDeviceOrientation != InCallOrientationEventListener.SCREEN_ORIENTATION_UNKNOWN);
       videoCall.setDeviceOrientation(mDeviceOrientation);
-      enableCamera(videoCall, isCameraRequired(newVideoState, call.getSessionModificationState()));
+      enableCamera(
+          videoCall,
+          isCameraRequired(newVideoState, call.getVideoTech().getSessionModificationState()));
     }
     int previousVideoState = mCurrentVideoState;
     mCurrentVideoState = newVideoState;
@@ -822,7 +798,7 @@
 
     // adjustVideoMode may be called if we are already in a 1-way video state.  In this case
     // we do not want to trigger auto-fullscreen mode.
-    if (!VideoUtils.isVideoCall(previousVideoState) && VideoUtils.isVideoCall(newVideoState)) {
+    if (!isVideoCall(previousVideoState) && isVideoCall(newVideoState)) {
       maybeAutoEnterFullscreen(call);
     }
   }
@@ -832,7 +808,7 @@
       return false;
     }
 
-    if (VideoUtils.isVideoCall(call)) {
+    if (isVideoCall(call)) {
       return true;
     }
 
@@ -877,7 +853,7 @@
     showVideoUi(
         VideoProfile.STATE_AUDIO_ONLY,
         DialerCall.State.ACTIVE,
-        DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST,
+        VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST,
         false /* isRemotelyHeld */);
     enableCamera(mVideoCall, false);
     InCallPresenter.getInstance().setFullScreen(false);
@@ -918,20 +894,6 @@
   }
 
   /**
-   * Handles peer video pause state changes.
-   *
-   * @param call The call which paused or un-pausedvideo transmission.
-   * @param paused {@code True} when the video transmission is paused, {@code false} when video
-   *     transmission resumes.
-   */
-  @Override
-  public void onPeerPauseStateChanged(DialerCall call, boolean paused) {
-    if (!call.equals(mPrimaryCall)) {
-      return;
-    }
-  }
-
-  /**
    * Handles peer video dimension changes.
    *
    * @param call The call which experienced a peer video dimension change.
@@ -959,17 +921,6 @@
   }
 
   /**
-   * Handles any video quality changes in the call.
-   *
-   * @param call The call which experienced a video quality change.
-   * @param videoQuality The new video call quality.
-   */
-  @Override
-  public void onVideoQualityChanged(DialerCall call, int videoQuality) {
-    // No-op
-  }
-
-  /**
    * Handles a change to the dimensions of the local camera. Receiving the camera capabilities
    * triggers the creation of the video
    *
@@ -1024,42 +975,6 @@
   }
 
   /**
-   * Called when call session event is raised.
-   *
-   * @param event The call session event.
-   */
-  @Override
-  public void onCallSessionEvent(int event) {
-    switch (event) {
-      case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE:
-        LogUtil.v("VideoCallPresenter.onCallSessionEvent", "rx_pause");
-        break;
-      case Connection.VideoProvider.SESSION_EVENT_RX_RESUME:
-        LogUtil.v("VideoCallPresenter.onCallSessionEvent", "rx_resume");
-        break;
-      case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE:
-        LogUtil.v("VideoCallPresenter.onCallSessionEvent", "camera_failure");
-        break;
-      case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY:
-        LogUtil.v("VideoCallPresenter.onCallSessionEvent", "camera_ready");
-        break;
-      default:
-        LogUtil.v("VideoCallPresenter.onCallSessionEvent", "unknown event = : " + event);
-        break;
-    }
-  }
-
-  /**
-   * Handles a change to the call data usage
-   *
-   * @param dataUsage call data usage value
-   */
-  @Override
-  public void onCallDataUsageChange(long dataUsage) {
-    LogUtil.v("VideoCallPresenter.onCallDataUsageChange", "dataUsage=" + dataUsage);
-  }
-
-  /**
    * Handles changes to the device orientation.
    *
    * @param orientation The screen orientation of the device (one of: {@link
@@ -1106,7 +1021,7 @@
       return;
     }
 
-    if (!VideoUtils.isVideoCall(call) || call.getState() == DialerCall.State.INCOMING) {
+    if (!isVideoCall(call) || call.getState() == DialerCall.State.INCOMING) {
       LogUtil.i("VideoCallPresenter.maybeExitFullscreen", "exiting fullscreen");
       InCallPresenter.getInstance().setFullScreen(false);
     }
@@ -1126,7 +1041,7 @@
 
     if (call == null
         || call.getState() != DialerCall.State.ACTIVE
-        || !VideoUtils.isBidirectionalVideoCall(call)
+        || !isBidirectionalVideoCall(call)
         || InCallPresenter.getInstance().isFullscreen()
         || (mContext != null && AccessibilityUtil.isTouchExplorationEnabled(mContext))) {
       // Ensure any previously scheduled attempt to enter fullscreen is cancelled.
@@ -1156,6 +1071,32 @@
     mHandler.removeCallbacks(mAutoFullscreenRunnable);
   }
 
+  @Override
+  public boolean shouldShowCameraPermissionDialog() {
+    if (mPrimaryCall == null) {
+      LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "null call");
+      return false;
+    }
+    if (mPrimaryCall.didShowCameraPermission()) {
+      LogUtil.i(
+          "VideoCallPresenter.shouldShowCameraPermissionDialog", "already shown for this call");
+      return false;
+    }
+    if (!ConfigProviderBindings.get(mContext)
+        .getBoolean("camera_permission_dialog_allowed", true)) {
+      LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "disabled by config");
+      return false;
+    }
+    return !VideoUtils.hasCameraPermission(mContext) || !VideoUtils.isCameraAllowedByUser(mContext);
+  }
+
+  @Override
+  public void onCameraPermissionDialogShown() {
+    if (mPrimaryCall != null) {
+      mPrimaryCall.setDidShowCameraPermission(true);
+    }
+  }
+
   private void updateRemoteVideoSurfaceDimensions() {
     Activity activity = mVideoCallScreen.getVideoCallScreenFragment().getActivity();
     if (activity != null) {
@@ -1166,8 +1107,8 @@
   }
 
   private static boolean isVideoUpgrade(DialerCall call) {
-    return VideoUtils.hasSentVideoUpgradeRequest(call)
-        || VideoUtils.hasReceivedVideoUpgradeRequest(call);
+    return call != null
+        && (call.hasSentVideoUpgradeRequest() || call.hasReceivedVideoUpgradeRequest());
   }
 
   private static boolean isVideoUpgrade(@SessionModificationState int state) {
@@ -1286,4 +1227,48 @@
     /** The surface has been set on the {@link VideoCall}. */
     private static final int SURFACE_SET = 3;
   }
+
+  private static boolean isBidirectionalVideoCall(DialerCall call) {
+    return CompatUtils.isVideoCompatible() && VideoProfile.isBidirectional(call.getVideoState());
+  }
+
+  private static boolean isIncomingVideoCall(DialerCall call) {
+    if (!isVideoCall(call)) {
+      return false;
+    }
+    final int state = call.getState();
+    return (state == DialerCall.State.INCOMING) || (state == DialerCall.State.CALL_WAITING);
+  }
+
+  private static boolean isActiveVideoCall(DialerCall call) {
+    return isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
+  }
+
+  private static boolean isOutgoingVideoCall(DialerCall call) {
+    if (!isVideoCall(call)) {
+      return false;
+    }
+    final int state = call.getState();
+    return DialerCall.State.isDialing(state)
+        || state == DialerCall.State.CONNECTING
+        || state == DialerCall.State.SELECT_PHONE_ACCOUNT;
+  }
+
+  private static boolean isAudioCall(DialerCall call) {
+    if (!CompatUtils.isVideoCompatible()) {
+      return true;
+    }
+
+    return call != null && VideoProfile.isAudioOnly(call.getVideoState());
+  }
+
+  private static boolean isVideoCall(@Nullable DialerCall call) {
+    return call != null && call.isVideoCall();
+  }
+
+  private static boolean isVideoCall(int videoState) {
+    return CompatUtils.isVideoCompatible()
+        && (VideoProfile.isTransmissionEnabled(videoState)
+            || VideoProfile.isReceptionEnabled(videoState));
+  }
 }
diff --git a/java/com/android/incallui/VideoPauseController.java b/java/com/android/incallui/VideoPauseController.java
index 2b43577..2595e2f 100644
--- a/java/com/android/incallui/VideoPauseController.java
+++ b/java/com/android/incallui/VideoPauseController.java
@@ -17,14 +17,14 @@
 package com.android.incallui;
 
 import android.support.annotation.NonNull;
-import android.telecom.VideoProfile;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
 import com.android.incallui.InCallPresenter.InCallState;
 import com.android.incallui.InCallPresenter.InCallStateListener;
 import com.android.incallui.InCallPresenter.IncomingCallListener;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
 import com.android.incallui.call.DialerCall.State;
-import com.android.incallui.call.VideoUtils;
 import java.util.Objects;
 
 /**
@@ -32,12 +32,21 @@
  * to the background and subsequently brought back to the foreground.
  */
 class VideoPauseController implements InCallStateListener, IncomingCallListener {
-
-  private static final String TAG = "VideoPauseController";
   private static VideoPauseController sVideoPauseController;
   private InCallPresenter mInCallPresenter;
-  /** The current call context, if applicable. */
-  private CallContext mPrimaryCallContext = null;
+
+  /** The current call, if applicable. */
+  private DialerCall mPrimaryCall = null;
+
+  /**
+   * The cached state of primary call, updated after onStateChange has processed.
+   *
+   * <p>These values are stored to detect specific changes in state between onStateChange calls.
+   */
+  private int mPrevCallState = State.INVALID;
+
+  private boolean mWasVideoCall = false;
+
   /**
    * Tracks whether the application is in the background. {@code True} if the application is in the
    * background, {@code false} otherwise.
@@ -57,51 +66,9 @@
     return sVideoPauseController;
   }
 
-  /**
-   * Determines if a given call is the same one stored in a {@link CallContext}.
-   *
-   * @param call The call.
-   * @param callContext The call context.
-   * @return {@code true} if the {@link DialerCall} is the same as the one referenced in the {@link
-   *     CallContext}.
-   */
-  private static boolean areSame(DialerCall call, CallContext callContext) {
-    if (call == null && callContext == null) {
-      return true;
-    } else if (call == null || callContext == null) {
-      return false;
-    }
-    return call.equals(callContext.getCall());
-  }
-
-  /**
-   * Determines if a video call can be paused. Only a video call which is active can be paused.
-   *
-   * @param callContext The call context to check.
-   * @return {@code true} if the call is an active video call.
-   */
-  private static boolean canVideoPause(CallContext callContext) {
-    return isVideoCall(callContext) && callContext.getState() == DialerCall.State.ACTIVE;
-  }
-
-  /**
-   * Determines if a call referenced by a {@link CallContext} is a video call.
-   *
-   * @param callContext The call context.
-   * @return {@code true} if the call is a video call, {@code false} otherwise.
-   */
-  private static boolean isVideoCall(CallContext callContext) {
-    return callContext != null && VideoUtils.isVideoCall(callContext.getVideoState());
-  }
-
-  /**
-   * Determines if call is in incoming/waiting state.
-   *
-   * @param call The call context.
-   * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise.
-   */
-  private static boolean isIncomingCall(CallContext call) {
-    return call != null && isIncomingCall(call.getCall());
+  private boolean wasIncomingCall() {
+    return (mPrevCallState == DialerCall.State.CALL_WAITING
+        || mPrevCallState == DialerCall.State.INCOMING);
   }
 
   /**
@@ -119,11 +86,10 @@
   /**
    * Determines if a call is dialing.
    *
-   * @param call The call context.
    * @return {@code true} if the call is dialing, {@code false} otherwise.
    */
-  private static boolean isDialing(CallContext call) {
-    return call != null && DialerCall.State.isDialing(call.getState());
+  private boolean wasDialing() {
+    return DialerCall.State.isDialing(mPrevCallState);
   }
 
   /**
@@ -133,8 +99,8 @@
    * @param inCallPresenter The {@link com.android.incallui.InCallPresenter}.
    */
   public void setUp(@NonNull InCallPresenter inCallPresenter) {
-    log("setUp");
-    mInCallPresenter = Objects.requireNonNull(inCallPresenter);
+    LogUtil.enterBlock("VideoPauseController.setUp");
+    mInCallPresenter = Assert.isNotNull(inCallPresenter);
     mInCallPresenter.addListener(this);
     mInCallPresenter.addIncomingCallListener(this);
   }
@@ -144,7 +110,7 @@
    * state. Called from {@link com.android.incallui.InCallPresenter}.
    */
   public void tearDown() {
-    log("tearDown...");
+    LogUtil.enterBlock("VideoPauseController.tearDown");
     mInCallPresenter.removeListener(this);
     mInCallPresenter.removeIncomingCallListener(this);
     clear();
@@ -153,7 +119,9 @@
   /** Clears the internal state for the {@link VideoPauseController}. */
   private void clear() {
     mInCallPresenter = null;
-    mPrimaryCallContext = null;
+    mPrimaryCall = null;
+    mPrevCallState = State.INVALID;
+    mWasVideoCall = false;
     mIsInBackground = false;
   }
 
@@ -167,8 +135,6 @@
    */
   @Override
   public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
-    log("onStateChange, OldState=" + oldState + " NewState=" + newState);
-
     DialerCall call;
     if (newState == InCallState.INCOMING) {
       call = callList.getIncomingCall();
@@ -182,22 +148,26 @@
       call = callList.getActiveCall();
     }
 
-    boolean hasPrimaryCallChanged = !areSame(call, mPrimaryCallContext);
-    boolean canVideoPause = VideoUtils.canVideoPause(call);
-    log("onStateChange, hasPrimaryCallChanged=" + hasPrimaryCallChanged);
-    log("onStateChange, canVideoPause=" + canVideoPause);
-    log("onStateChange, IsInBackground=" + mIsInBackground);
+    boolean hasPrimaryCallChanged = !Objects.equals(call, mPrimaryCall);
+    boolean canVideoPause = videoCanPause(call);
+
+    LogUtil.i(
+        "VideoPauseController.onStateChange",
+        "hasPrimaryCallChanged: %b, videoCanPause: %b, isInBackground: %b",
+        hasPrimaryCallChanged,
+        canVideoPause,
+        mIsInBackground);
 
     if (hasPrimaryCallChanged) {
       onPrimaryCallChanged(call);
       return;
     }
 
-    if (isDialing(mPrimaryCallContext) && canVideoPause && mIsInBackground) {
+    if (wasDialing() && canVideoPause && mIsInBackground) {
       // Bring UI to foreground if outgoing request becomes active while UI is in
       // background.
       bringToForeground();
-    } else if (!isVideoCall(mPrimaryCallContext) && canVideoPause && mIsInBackground) {
+    } else if (!mWasVideoCall && canVideoPause && mIsInBackground) {
       // Bring UI to foreground if VoLTE call becomes active while UI is in
       // background.
       bringToForeground();
@@ -216,27 +186,26 @@
    * @param call The new primary call.
    */
   private void onPrimaryCallChanged(DialerCall call) {
-    log("onPrimaryCallChanged: New call = " + call);
-    log("onPrimaryCallChanged: Old call = " + mPrimaryCallContext);
-    log("onPrimaryCallChanged, IsInBackground=" + mIsInBackground);
+    LogUtil.i(
+        "VideoPauseController.onPrimaryCallChanged",
+        "new call: %s, old call: %s, mIsInBackground: %b",
+        call,
+        mPrimaryCall,
+        mIsInBackground);
 
-    if (areSame(call, mPrimaryCallContext)) {
+    if (Objects.equals(call, mPrimaryCall)) {
       throw new IllegalStateException();
     }
-    final boolean canVideoPause = VideoUtils.canVideoPause(call);
+    final boolean canVideoPause = videoCanPause(call);
 
-    if ((isIncomingCall(mPrimaryCallContext)
-            || isDialing(mPrimaryCallContext)
-            || (call != null && VideoProfile.isPaused(call.getVideoState())))
-        && canVideoPause
-        && !mIsInBackground) {
+    if ((wasIncomingCall() || wasDialing()) && canVideoPause && !mIsInBackground) {
       // Send resume request for the active call, if user rejects incoming call, ends dialing
       // call, or the call was previously in a paused state and UI is in the foreground.
       sendRequest(call, true);
-    } else if (isIncomingCall(call) && canVideoPause(mPrimaryCallContext)) {
+    } else if (isIncomingCall(call) && videoCanPause(mPrimaryCall)) {
       // Send pause request if there is an active video call, and we just received a new
       // incoming call.
-      sendRequest(mPrimaryCallContext.getCall(), false);
+      sendRequest(mPrimaryCall, false);
     }
 
     updatePrimaryCallContext(call);
@@ -251,9 +220,14 @@
    */
   @Override
   public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
-    log("onIncomingCall, OldState=" + oldState + " NewState=" + newState + " DialerCall=" + call);
+    LogUtil.i(
+        "VideoPauseController.onIncomingCall",
+        "oldState: %s, newState: %s, call: %s",
+        oldState,
+        newState,
+        call);
 
-    if (areSame(call, mPrimaryCallContext)) {
+    if (Objects.equals(call, mPrimaryCall)) {
       return;
     }
 
@@ -267,11 +241,13 @@
    */
   private void updatePrimaryCallContext(DialerCall call) {
     if (call == null) {
-      mPrimaryCallContext = null;
-    } else if (mPrimaryCallContext != null) {
-      mPrimaryCallContext.update(call);
+      mPrimaryCall = null;
+      mPrevCallState = State.INVALID;
+      mWasVideoCall = false;
     } else {
-      mPrimaryCallContext = new CallContext(call);
+      mPrimaryCall = call;
+      mPrevCallState = call.getState();
+      mWasVideoCall = call.isVideoCall();
     }
   }
 
@@ -301,13 +277,9 @@
    *     video provider if we are in a call.
    */
   private void onResume(boolean isInCall) {
-    log("onResume");
-
     mIsInBackground = false;
-    if (canVideoPause(mPrimaryCallContext) && isInCall) {
-      sendRequest(mPrimaryCallContext.getCall(), true);
-    } else {
-      log("onResume. Ignoring...");
+    if (isInCall) {
+      sendRequest(mPrimaryCall, true);
     }
   }
 
@@ -319,22 +291,20 @@
    *     video provider if we are in a call.
    */
   private void onPause(boolean isInCall) {
-    log("onPause");
-
     mIsInBackground = true;
-    if (canVideoPause(mPrimaryCallContext) && isInCall) {
-      sendRequest(mPrimaryCallContext.getCall(), false);
-    } else {
-      log("onPause, Ignoring...");
+    if (isInCall) {
+      sendRequest(mPrimaryCall, false);
     }
   }
 
   private void bringToForeground() {
+    LogUtil.enterBlock("VideoPauseController.bringToForeground");
     if (mInCallPresenter != null) {
-      log("Bringing UI to foreground");
       mInCallPresenter.bringToForeground(false);
     } else {
-      loge("InCallPresenter is null. Cannot bring UI to foreground");
+      LogUtil.e(
+          "VideoPauseController.bringToForeground",
+          "InCallPresenter is null. Cannot bring UI to foreground");
     }
   }
 
@@ -345,72 +315,18 @@
    * @param resume If true resume request will be sent, otherwise pause request.
    */
   private void sendRequest(DialerCall call, boolean resume) {
-    // Check if this call supports pause/un-pause.
-    if (!call.can(android.telecom.Call.Details.CAPABILITY_CAN_PAUSE_VIDEO)) {
+    if (call == null) {
       return;
     }
 
     if (resume) {
-      log("sending resume request, call=" + call);
-      call.getVideoCall().sendSessionModifyRequest(VideoUtils.makeVideoUnPauseProfile(call));
+      call.getVideoTech().unpause();
     } else {
-      log("sending pause request, call=" + call);
-      call.getVideoCall().sendSessionModifyRequest(VideoUtils.makeVideoPauseProfile(call));
+      call.getVideoTech().pause();
     }
   }
 
-  /**
-   * Logs a debug message.
-   *
-   * @param msg The message.
-   */
-  private void log(String msg) {
-    Log.d(this, TAG + msg);
-  }
-
-  /**
-   * Logs an error message.
-   *
-   * @param msg The message.
-   */
-  private void loge(String msg) {
-    Log.e(this, TAG + msg);
-  }
-
-  /** Keeps track of the current active/foreground call. */
-  private static class CallContext {
-
-    private int mState = State.INVALID;
-    private int mVideoState;
-    private DialerCall mCall;
-
-    public CallContext(@NonNull DialerCall call) {
-      Objects.requireNonNull(call);
-      update(call);
-    }
-
-    public void update(@NonNull DialerCall call) {
-      mCall = Objects.requireNonNull(call);
-      mState = call.getState();
-      mVideoState = call.getVideoState();
-    }
-
-    public int getState() {
-      return mState;
-    }
-
-    public int getVideoState() {
-      return mVideoState;
-    }
-
-    @Override
-    public String toString() {
-      return String.format(
-          "CallContext {CallId=%s, State=%s, VideoState=%d}", mCall.getId(), mState, mVideoState);
-    }
-
-    public DialerCall getCall() {
-      return mCall;
-    }
+  private static boolean videoCanPause(DialerCall call) {
+    return call != null && call.isVideoCall() && call.getState() == DialerCall.State.ACTIVE;
   }
 }
diff --git a/java/com/android/incallui/answer/bindings/AnswerBindings.java b/java/com/android/incallui/answer/bindings/AnswerBindings.java
index f7a7a0a..442e207 100644
--- a/java/com/android/incallui/answer/bindings/AnswerBindings.java
+++ b/java/com/android/incallui/answer/bindings/AnswerBindings.java
@@ -23,7 +23,7 @@
 public class AnswerBindings {
 
   public static AnswerScreen createAnswerScreen(
-      String callId, int videoState, boolean isVideoUpgradeRequest) {
-    return AnswerFragment.newInstance(callId, videoState, isVideoUpgradeRequest);
+      String callId, boolean isVideoCall, boolean isVideoUpgradeRequest) {
+    return AnswerFragment.newInstance(callId, isVideoCall, isVideoUpgradeRequest);
   }
 }
diff --git a/java/com/android/incallui/answer/impl/AnswerFragment.java b/java/com/android/incallui/answer/impl/AnswerFragment.java
index 98439ee..6874dae 100644
--- a/java/com/android/incallui/answer/impl/AnswerFragment.java
+++ b/java/com/android/incallui/answer/impl/AnswerFragment.java
@@ -37,7 +37,6 @@
 import android.support.annotation.VisibleForTesting;
 import android.support.transition.TransitionManager;
 import android.support.v4.app.Fragment;
-import android.telecom.VideoProfile;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -79,10 +78,11 @@
 import com.android.incallui.incall.protocol.PrimaryCallState;
 import com.android.incallui.incall.protocol.PrimaryInfo;
 import com.android.incallui.incall.protocol.SecondaryInfo;
-import com.android.incallui.maps.StaticMapBinding;
+import com.android.incallui.maps.MapsComponent;
 import com.android.incallui.sessiondata.AvatarPresenter;
 import com.android.incallui.sessiondata.MultimediaFragment;
 import com.android.incallui.util.AccessibilityUtil;
+import com.android.incallui.video.protocol.VideoCallScreen;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -101,7 +101,7 @@
   static final String ARG_CALL_ID = "call_id";
 
   @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-  static final String ARG_VIDEO_STATE = "video_state";
+  static final String ARG_IS_VIDEO_CALL = "is_video_call";
 
   @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
   static final String ARG_IS_VIDEO_UPGRADE_REQUEST = "is_video_upgrade_request";
@@ -143,7 +143,7 @@
   private CreateCustomSmsDialogFragment createCustomSmsDialogFragment;
   private SecondaryBehavior secondaryBehavior = SecondaryBehavior.REJECT_WITH_SMS;
   private ContactGridManager contactGridManager;
-  private AnswerVideoCallScreen answerVideoCallScreen;
+  private VideoCallScreen answerVideoCallScreen;
   private Handler handler = new Handler(Looper.getMainLooper());
 
   private enum SecondaryBehavior {
@@ -288,10 +288,10 @@
   }
 
   public static AnswerFragment newInstance(
-      String callId, int videoState, boolean isVideoUpgradeRequest) {
+      String callId, boolean isVideoCall, boolean isVideoUpgradeRequest) {
     Bundle bundle = new Bundle();
     bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId));
-    bundle.putInt(ARG_VIDEO_STATE, videoState);
+    bundle.putBoolean(ARG_IS_VIDEO_CALL, isVideoCall);
     bundle.putBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST, isVideoUpgradeRequest);
 
     AnswerFragment instance = new AnswerFragment();
@@ -306,18 +306,13 @@
   }
 
   @Override
-  public int getVideoState() {
-    return getArguments().getInt(ARG_VIDEO_STATE);
-  }
-
-  @Override
   public boolean isVideoUpgradeRequest() {
     return getArguments().getBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST);
   }
 
   @Override
   public void setTextResponses(List<String> textResponses) {
-    if (isVideoCall()) {
+    if (isVideoCall() || isVideoUpgradeRequest()) {
       LogUtil.i("AnswerFragment.setTextResponses", "no-op for video calls");
     } else if (textResponses == null) {
       LogUtil.i("AnswerFragment.setTextResponses", "no text responses, hiding secondary button");
@@ -336,7 +331,9 @@
 
   private void initSecondaryButton() {
     secondaryBehavior =
-        isVideoCall() ? SecondaryBehavior.ANSWER_VIDEO_AS_AUDIO : SecondaryBehavior.REJECT_WITH_SMS;
+        isVideoCall() || isVideoUpgradeRequest()
+            ? SecondaryBehavior.ANSWER_VIDEO_AS_AUDIO
+            : SecondaryBehavior.REJECT_WITH_SMS;
     secondaryBehavior.applyToView(secondaryButton);
 
     secondaryButton.setOnClickListener(
@@ -351,12 +348,9 @@
     secondaryButton.setAccessibilityDelegate(accessibilityDelegate);
 
     if (isVideoCall()) {
-      //noinspection WrongConstant
-      if (!isVideoUpgradeRequest() && VideoProfile.isTransmissionEnabled(getVideoState())) {
-        secondaryButton.setVisibility(View.VISIBLE);
-      } else {
-        secondaryButton.setVisibility(View.INVISIBLE);
-      }
+      secondaryButton.setVisibility(View.VISIBLE);
+    } else {
+      secondaryButton.setVisibility(View.INVISIBLE);
     }
   }
 
@@ -448,11 +442,11 @@
 
     MultimediaData multimediaData = getSessionData();
     if (multimediaData != null
-        && (!TextUtils.isEmpty(multimediaData.getSubject())
+        && (!TextUtils.isEmpty(multimediaData.getText())
             || (multimediaData.getImageUri() != null)
             || (multimediaData.getLocation() != null && canShowMap()))) {
       // Need message fragment
-      String subject = multimediaData.getSubject();
+      String subject = multimediaData.getText();
       Uri imageUri = multimediaData.getImageUri();
       Location location = multimediaData.getLocation();
       if (!(current instanceof MultimediaFragment)
@@ -487,11 +481,11 @@
   }
 
   private boolean shouldShowAvatar() {
-    return !isVideoCall();
+    return !isVideoCall() && !isVideoUpgradeRequest();
   }
 
   private boolean canShowMap() {
-    return StaticMapBinding.get(getActivity().getApplication()) != null;
+    return MapsComponent.get(getContext()).getMaps().isAvailable();
   }
 
   @Override
@@ -564,7 +558,7 @@
       LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
     Bundle arguments = getArguments();
     Assert.checkState(arguments.containsKey(ARG_CALL_ID));
-    Assert.checkState(arguments.containsKey(ARG_VIDEO_STATE));
+    Assert.checkState(arguments.containsKey(ARG_IS_VIDEO_CALL));
     Assert.checkState(arguments.containsKey(ARG_IS_VIDEO_UPGRADE_REQUEST));
 
     buttonAcceptClicked = false;
@@ -596,7 +590,6 @@
             });
     updateImportanceBadgeVisibility();
 
-    boolean isVideoCall = isVideoCall();
     contactGridManager = new ContactGridManager(view, null, 0, false /* showAnonymousAvatar */);
 
     Fragment answerMethod =
@@ -625,9 +618,9 @@
       flags |= STATUS_BAR_DISABLE_BACK | STATUS_BAR_DISABLE_HOME | STATUS_BAR_DISABLE_RECENT;
     }
     view.setSystemUiVisibility(flags);
-    if (isVideoCall) {
+    if (isVideoCall() || isVideoUpgradeRequest()) {
       if (VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) {
-        answerVideoCallScreen = new AnswerVideoCallScreen(this, view);
+        answerVideoCallScreen = new AnswerVideoCallScreen(getCallId(), this, view);
       } else {
         view.findViewById(R.id.videocall_video_off).setVisibility(View.VISIBLE);
       }
@@ -649,7 +642,7 @@
     updateUI();
 
     if (savedInstanceState == null || !savedInstanceState.getBoolean(STATE_HAS_ANIMATED_ENTRY)) {
-      ViewUtil.doOnPreDraw(view, false, this::animateEntry);
+      ViewUtil.doOnGlobalLayout(view, this::animateEntry);
     }
   }
 
@@ -667,7 +660,7 @@
 
     updateUI();
     if (answerVideoCallScreen != null) {
-      answerVideoCallScreen.onStart();
+      answerVideoCallScreen.onVideoScreenStart();
     }
   }
 
@@ -678,7 +671,7 @@
 
     handler.removeCallbacks(swipeHintRestoreTimer);
     if (answerVideoCallScreen != null) {
-      answerVideoCallScreen.onStop();
+      answerVideoCallScreen.onVideoScreenStop();
     }
   }
 
@@ -722,7 +715,7 @@
 
   @Override
   public boolean isVideoCall() {
-    return VideoUtils.isVideoCall(getVideoState());
+    return getArguments().getBoolean(ARG_IS_VIDEO_CALL);
   }
 
   @Override
@@ -775,14 +768,12 @@
     Animator dataContainer = createTranslation(rootView.findViewById(R.id.incall_data_container));
 
     AnimatorSet animatorSet = new AnimatorSet();
-    animatorSet
-        .play(alpha)
-        .with(topRow)
-        .with(contactName)
-        .with(bottomRow)
-        .with(important)
-        .with(dataContainer);
-    animatorSet.setDuration(getResources().getInteger(android.R.integer.config_longAnimTime));
+    AnimatorSet.Builder builder = animatorSet.play(alpha);
+    builder.with(topRow).with(contactName).with(bottomRow).with(important).with(dataContainer);
+    if (isShowingLocationUi()) {
+      builder.with(createTranslation(rootView.findViewById(R.id.incall_location_holder)));
+    }
+    animatorSet.setDuration(getResources().getInteger(R.integer.answer_animate_entry_millis));
     animatorSet.addListener(
         new AnimatorListenerAdapter() {
           @Override
@@ -803,14 +794,7 @@
   private void acceptCallByUser(boolean answerVideoAsAudio) {
     LogUtil.i("AnswerFragment.acceptCallByUser", answerVideoAsAudio ? " answerVideoAsAudio" : "");
     if (!buttonAcceptClicked) {
-      int desiredVideoState = getVideoState();
-      if (answerVideoAsAudio) {
-        desiredVideoState = VideoProfile.STATE_AUDIO_ONLY;
-      }
-
-      // Notify the lower layer first to start signaling ASAP.
-      answerScreenDelegate.onAnswer(desiredVideoState);
-
+      answerScreenDelegate.onAnswer(answerVideoAsAudio);
       buttonAcceptClicked = true;
     }
   }
diff --git a/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java b/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
index 0316a5f..06502da 100644
--- a/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
+++ b/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
@@ -32,12 +32,15 @@
 
 /** Shows a video preview for an incoming call. */
 public class AnswerVideoCallScreen implements VideoCallScreen {
+  @NonNull private final String callId;
   @NonNull private final Fragment fragment;
   @NonNull private final TextureView textureView;
   @NonNull private final VideoCallScreenDelegate delegate;
 
-  public AnswerVideoCallScreen(@NonNull Fragment fragment, @NonNull View view) {
-    this.fragment = fragment;
+  public AnswerVideoCallScreen(
+      @NonNull String callId, @NonNull Fragment fragment, @NonNull View view) {
+    this.callId = Assert.isNotNull(callId);
+    this.fragment = Assert.isNotNull(fragment);
 
     textureView =
         Assert.isNotNull((TextureView) view.findViewById(R.id.incoming_preview_texture_view));
@@ -53,13 +56,15 @@
     overlayView.setVisibility(View.VISIBLE);
   }
 
-  public void onStart() {
+  @Override
+  public void onVideoScreenStart() {
     LogUtil.i("AnswerVideoCallScreen.onStart", null);
     delegate.onVideoCallScreenUiReady();
     delegate.getLocalVideoSurfaceTexture().attachToTextureView(textureView);
   }
 
-  public void onStop() {
+  @Override
+  public void onVideoScreenStop() {
     LogUtil.i("AnswerVideoCallScreen.onStop", null);
     delegate.onVideoCallScreenUiUnready();
   }
@@ -98,6 +103,12 @@
     return fragment;
   }
 
+  @NonNull
+  @Override
+  public String getCallId() {
+    return callId;
+  }
+
   private void updatePreviewVideoScaling() {
     if (textureView.getWidth() == 0 || textureView.getHeight() == 0) {
       LogUtil.i(
diff --git a/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java b/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java
index 62845b7..ff20d3a 100644
--- a/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java
+++ b/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java
@@ -190,7 +190,7 @@
 
       case MotionEvent.ACTION_UP:
         isUp = true;
-        //fallthrough_intended
+        // fall through
       case MotionEvent.ACTION_CANCEL:
         boolean hintOnTheRight = targetedView == rightIcon;
         trackMovement(event);
diff --git a/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java b/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java
index 4052281..afa194f 100644
--- a/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java
+++ b/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java
@@ -44,4 +44,6 @@
    * @return true iff the current call is a video call.
    */
   boolean isVideoCall();
+
+  boolean isVideoUpgradeRequest();
 }
diff --git a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
index 0bc6581..6e8e1f7 100644
--- a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
+++ b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
@@ -60,7 +60,7 @@
 import com.android.incallui.answer.impl.classifier.FalsingManager;
 import com.android.incallui.answer.impl.hint.AnswerHint;
 import com.android.incallui.answer.impl.hint.AnswerHintFactory;
-import com.android.incallui.answer.impl.hint.EventPayloadLoaderImpl;
+import com.android.incallui.answer.impl.hint.PawImageLoaderImpl;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -228,7 +228,7 @@
     touchHandler = FlingUpDownTouchHandler.attach(view, this, falsingManager);
 
     answerHint =
-        new AnswerHintFactory(new EventPayloadLoaderImpl())
+        new AnswerHintFactory(new PawImageLoaderImpl())
             .create(getContext(), ANIMATE_DURATION_LONG_MILLIS, BOUNCE_ANIMATION_DELAY);
     answerHint.onCreateView(
         layoutInflater,
@@ -319,7 +319,7 @@
     if (contactPuckIcon == null) {
       return;
     }
-    if (getParent().isVideoCall()) {
+    if (getParent().isVideoCall() || getParent().isVideoUpgradeRequest()) {
       contactPuckIcon.setImageResource(R.drawable.quantum_ic_videocam_white_24);
     } else {
       contactPuckIcon.setImageResource(R.drawable.quantum_ic_call_white_24);
@@ -348,7 +348,8 @@
   }
 
   private boolean shouldShowPhotoInPuck() {
-    return getParent().isVideoCall() && contactPhoto != null;
+    return (getParent().isVideoCall() || getParent().isVideoUpgradeRequest())
+        && contactPhoto != null;
   }
 
   @Override
@@ -387,6 +388,10 @@
     // Since the animation progression is controlled by user gesture instead of real timeline, the
     // spec timeline can be divided into 9 slots. Each slot is equivalent to 83ms in the spec.
     // Therefore, we use 9 slots of 83ms to map user gesture into the spec timeline.
+    //
+    // See specs -
+    // Accept: https://direct.googleplex.com/#/spec/8510001
+    // Decline: https://direct.googleplex.com/#/spec/3850001
     final float progressSlots = 9;
 
     // Fade out the "swipe up to answer". It only takes 1 slot to complete the fade.
@@ -414,7 +419,7 @@
     contactPuckBackground.setColorFilter(destPuckColor);
 
     // Animate decline icon
-    if (isAcceptingFlow || getParent().isVideoCall()) {
+    if (isAcceptingFlow || getParent().isVideoCall() || getParent().isVideoUpgradeRequest()) {
       rotateToward(contactPuckIcon, 0f);
     } else {
       rotateToward(contactPuckIcon, positiveAdjustedProgress * ICON_END_CALL_ROTATION_DEGREES);
diff --git a/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml b/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml
index b5fa6da..dfbba1c 100644
--- a/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml
+++ b/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml
@@ -3,7 +3,7 @@
   xmlns:android="http://schemas.android.com/apk/res/android">
 
   <application>
-    <receiver android:name=".EventSecretCodeListener">
+    <receiver android:name=".PawSecretCodeListener">
       <intent-filter>
         <action android:name="android.provider.Telephony.SECRET_CODE" />
         <data android:scheme="android_secret_code" />
diff --git a/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java b/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java
index 45395a7..eaf5b74 100644
--- a/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java
+++ b/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java
@@ -28,7 +28,6 @@
 import com.android.dialer.common.ConfigProviderBindings;
 import com.android.dialer.common.LogUtil;
 import com.android.incallui.util.AccessibilityUtil;
-import java.util.Calendar;
 
 /**
  * Selects a AnswerHint to show. If there's no suitable hints {@link EmptyAnswerHint} will be used,
@@ -51,10 +50,10 @@
   @VisibleForTesting
   static final String ANSWERED_COUNT_PREFERENCE_KEY = "answer_hint_answered_count";
 
-  private final EventPayloadLoader eventPayloadLoader;
+  private final PawImageLoader pawImageLoader;
 
-  public AnswerHintFactory(@NonNull EventPayloadLoader eventPayloadLoader) {
-    this.eventPayloadLoader = Assert.isNotNull(eventPayloadLoader);
+  public AnswerHintFactory(@NonNull PawImageLoader pawImageLoader) {
+    this.pawImageLoader = Assert.isNotNull(pawImageLoader);
   }
 
   @NonNull
@@ -69,11 +68,9 @@
     }
 
     // Display the event answer hint if the payload is available.
-    Drawable eventPayload =
-        eventPayloadLoader.loadPayload(
-            context, System.currentTimeMillis(), Calendar.getInstance().getTimeZone());
+    Drawable eventPayload = pawImageLoader.loadPayload(context);
     if (eventPayload != null) {
-      return new EventAnswerHint(context, eventPayload, puckUpDuration, puckUpDelay);
+      return new PawAnswerHint(context, eventPayload, puckUpDuration, puckUpDelay);
     }
 
     return new EmptyAnswerHint();
diff --git a/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java b/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java
deleted file mode 100644
index bd8d736..0000000
--- a/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.incallui.answer.impl.hint;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.graphics.BitmapFactory;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.Build.VERSION_CODES;
-import android.preference.PreferenceManager;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import com.android.dialer.common.Assert;
-import com.android.dialer.common.ConfigProvider;
-import com.android.dialer.common.ConfigProviderBindings;
-import com.android.dialer.common.LogUtil;
-import java.io.InputStream;
-import java.util.TimeZone;
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.SecretKeyFactory;
-import javax.crypto.spec.PBEKeySpec;
-
-/** Decrypt the event payload to be shown if in a specific time range and the key is received. */
-@TargetApi(VERSION_CODES.M)
-public final class EventPayloadLoaderImpl implements EventPayloadLoader {
-
-  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-  static final String CONFIG_EVENT_KEY = "event_key";
-
-  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-  static final String CONFIG_EVENT_BINARY = "event_binary";
-
-  // Time is stored as a UTC UNIX timestamp in milliseconds, but interpreted as local time.
-  // For example, 946684800 (2000/1/1 00:00:00 @UTC) is the new year midnight at every timezone.
-  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-  static final String CONFIG_EVENT_START_UTC_AS_LOCAL_MILLIS = "event_time_start_millis";
-
-  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-  static final String CONFIG_EVENT_TIME_END_UTC_AS_LOCAL_MILLIS = "event_time_end_millis";
-
-  @Override
-  @Nullable
-  public Drawable loadPayload(
-      @NonNull Context context, long currentTimeUtcMillis, @NonNull TimeZone timeZone) {
-    Assert.isNotNull(context);
-    Assert.isNotNull(timeZone);
-    ConfigProvider configProvider = ConfigProviderBindings.get(context);
-
-    String pbeKey = configProvider.getString(CONFIG_EVENT_KEY, null);
-    if (pbeKey == null) {
-      return null;
-    }
-    long timeRangeStart = configProvider.getLong(CONFIG_EVENT_START_UTC_AS_LOCAL_MILLIS, 0);
-    long timeRangeEnd = configProvider.getLong(CONFIG_EVENT_TIME_END_UTC_AS_LOCAL_MILLIS, 0);
-
-    String eventBinary = configProvider.getString(CONFIG_EVENT_BINARY, null);
-    if (eventBinary == null) {
-      return null;
-    }
-
-    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
-    if (!preferences.getBoolean(
-        EventSecretCodeListener.EVENT_ENABLED_WITH_SECRET_CODE_KEY, false)) {
-      long localTimestamp = currentTimeUtcMillis + timeZone.getRawOffset();
-
-      if (localTimestamp < timeRangeStart) {
-        return null;
-      }
-
-      if (localTimestamp > timeRangeEnd) {
-        return null;
-      }
-    }
-
-    // Use openssl aes-128-cbc -in <input> -out <output> -pass <PBEKey> to generate the asset
-    try (InputStream input = context.getAssets().open(eventBinary)) {
-      byte[] encryptedFile = new byte[input.available()];
-      input.read(encryptedFile);
-
-      Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding", "BC");
-
-      byte[] salt = new byte[8];
-      System.arraycopy(encryptedFile, 8, salt, 0, 8);
-      SecretKey key =
-          SecretKeyFactory.getInstance("PBEWITHMD5AND128BITAES-CBC-OPENSSL", "BC")
-              .generateSecret(new PBEKeySpec(pbeKey.toCharArray(), salt, 100));
-      cipher.init(Cipher.DECRYPT_MODE, key);
-
-      byte[] decryptedFile = cipher.doFinal(encryptedFile, 16, encryptedFile.length - 16);
-
-      return new BitmapDrawable(
-          context.getResources(),
-          BitmapFactory.decodeByteArray(decryptedFile, 0, decryptedFile.length));
-    } catch (Exception e) {
-      // Avoid crashing dialer for any reason.
-      LogUtil.e("EventPayloadLoader.loadPayload", "error decrypting payload:", e);
-      return null;
-    }
-  }
-}
diff --git a/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java b/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java
deleted file mode 100644
index 7cf4054..0000000
--- a/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.incallui.answer.impl.hint;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
-import android.support.annotation.VisibleForTesting;
-import android.text.TextUtils;
-import android.widget.Toast;
-import com.android.dialer.common.ConfigProviderBindings;
-import com.android.dialer.common.LogUtil;
-import com.android.dialer.logging.Logger;
-import com.android.dialer.logging.nano.DialerImpression;
-
-/**
- * Listen to the broadcast when the user dials "*#*#[number]#*#*" to toggle the event answer hint.
- */
-public class EventSecretCodeListener extends BroadcastReceiver {
-
-  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-  static final String CONFIG_EVENT_SECRET_CODE = "event_secret_code";
-
-  public static final String EVENT_ENABLED_WITH_SECRET_CODE_KEY = "event_enabled_with_secret_code";
-
-  @Override
-  public void onReceive(Context context, Intent intent) {
-    String host = intent.getData().getHost();
-    String secretCode =
-        ConfigProviderBindings.get(context).getString(CONFIG_EVENT_SECRET_CODE, null);
-    if (secretCode == null) {
-      return;
-    }
-    if (!TextUtils.equals(secretCode, host)) {
-      return;
-    }
-    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
-    boolean wasEnabled = preferences.getBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, false);
-    if (wasEnabled) {
-      preferences.edit().putBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, false).apply();
-      Toast.makeText(context, R.string.event_deactivated, Toast.LENGTH_SHORT).show();
-      Logger.get(context).logImpression(DialerImpression.Type.EVENT_ANSWER_HINT_DEACTIVATED);
-      LogUtil.i("EventSecretCodeListener.onReceive", "EventAnswerHint disabled");
-    } else {
-      preferences.edit().putBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, true).apply();
-      Toast.makeText(context, R.string.event_activated, Toast.LENGTH_SHORT).show();
-      Logger.get(context).logImpression(DialerImpression.Type.EVENT_ANSWER_HINT_ACTIVATED);
-      LogUtil.i("EventSecretCodeListener.onReceive", "EventAnswerHint enabled");
-    }
-  }
-}
diff --git a/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java b/java/com/android/incallui/answer/impl/hint/PawAnswerHint.java
similarity index 95%
rename from java/com/android/incallui/answer/impl/hint/EventAnswerHint.java
rename to java/com/android/incallui/answer/impl/hint/PawAnswerHint.java
index 7ee327d..36b761f 100644
--- a/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java
+++ b/java/com/android/incallui/answer/impl/hint/PawAnswerHint.java
@@ -39,7 +39,7 @@
  * An Answer hint that animates a {@link Drawable} payload with animation similar to {@link
  * DotAnswerHint}.
  */
-public final class EventAnswerHint implements AnswerHint {
+public final class PawAnswerHint implements AnswerHint {
 
   private static final long FADE_IN_DELAY_SCALE_MILLIS = 380;
   private static final long FADE_IN_DURATION_SCALE_MILLIS = 200;
@@ -53,7 +53,8 @@
   private static final long FADE_OUT_DELAY_ALPHA_MILLIS = 130;
   private static final long FADE_OUT_DURATION_ALPHA_MILLIS = 170;
 
-  private static final float FADE_SCALE = 1.2f;
+  private static final float IMAGE_SCALE = 1.5f;
+  private static final float FADE_SCALE = 2.0f;
 
   private final Context context;
   private final Drawable payload;
@@ -65,7 +66,7 @@
   private View answerHintContainer;
   private AnimatorSet answerGestureHintAnim;
 
-  public EventAnswerHint(
+  public PawAnswerHint(
       @NonNull Context context,
       @NonNull Drawable payload,
       long puckUpDurationMillis,
@@ -80,9 +81,9 @@
   public void onCreateView(
       LayoutInflater inflater, ViewGroup container, View puck, TextView hintText) {
     this.puck = puck;
-    View view = inflater.inflate(R.layout.event_hint, container, true);
+    View view = inflater.inflate(R.layout.paw_hint, container, true);
     answerHintContainer = view.findViewById(R.id.answer_hint_container);
-    payloadView = view.findViewById(R.id.payload);
+    payloadView = view.findViewById(R.id.paw_image);
     hintText.setTextSize(
         TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimension(R.dimen.hint_text_size));
     ((ImageView) payloadView).setImageDrawable(payload);
@@ -143,7 +144,7 @@
         createUniformScaleAnimator(
             target,
             FADE_SCALE,
-            1.0f,
+            IMAGE_SCALE,
             FADE_IN_DURATION_SCALE_MILLIS,
             FADE_IN_DELAY_SCALE_MILLIS,
             new LinearInterpolator());
@@ -170,7 +171,7 @@
     Animator scale =
         createUniformScaleAnimator(
             target,
-            1.0f,
+            IMAGE_SCALE,
             FADE_SCALE,
             FADE_OUT_DURATION_SCALE_MILLIS,
             scaleDelay,
@@ -178,7 +179,7 @@
     Animator alpha =
         createAlphaAnimator(
             target,
-            01.0f,
+            1.0f,
             0.0f,
             FADE_OUT_DURATION_ALPHA_MILLIS,
             FADE_OUT_DELAY_ALPHA_MILLIS,
diff --git a/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java b/java/com/android/incallui/answer/impl/hint/PawImageLoader.java
similarity index 75%
rename from java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java
rename to java/com/android/incallui/answer/impl/hint/PawImageLoader.java
index 09e3bed..09e700f 100644
--- a/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java
+++ b/java/com/android/incallui/answer/impl/hint/PawImageLoader.java
@@ -20,11 +20,9 @@
 import android.graphics.drawable.Drawable;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import java.util.TimeZone;
 
-/** Loads a {@link Drawable} payload for the {@link EventAnswerHint} if it should be displayed. */
-public interface EventPayloadLoader {
+/** Loads a {@link Drawable} payload for the {@link PawAnswerHint} if it should be displayed. */
+public interface PawImageLoader {
   @Nullable
-  Drawable loadPayload(
-      @NonNull Context context, long currentTimeUtcMillis, @NonNull TimeZone timeZone);
+  Drawable loadPayload(@NonNull Context context);
 }
diff --git a/java/com/android/incallui/answer/impl/hint/PawImageLoaderImpl.java b/java/com/android/incallui/answer/impl/hint/PawImageLoaderImpl.java
new file mode 100644
index 0000000..485a9ae
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/PawImageLoaderImpl.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.hint;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
+import android.os.Build.VERSION_CODES;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.Assert;
+
+/** Decrypt the event payload to be shown if in a specific time range and the key is received. */
+@TargetApi(VERSION_CODES.M)
+public final class PawImageLoaderImpl implements PawImageLoader {
+
+  @Override
+  @Nullable
+  public Drawable loadPayload(@NonNull Context context) {
+    Assert.isNotNull(context);
+
+    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+    if (!preferences.getBoolean(PawSecretCodeListener.PAW_ENABLED_WITH_SECRET_CODE_KEY, false)) {
+      return null;
+    }
+    int drawableId = preferences.getInt(PawSecretCodeListener.PAW_DRAWABLE_ID_KEY, 0);
+    if (drawableId == 0) {
+      return null;
+    }
+    return context.getDrawable(drawableId);
+  }
+}
diff --git a/java/com/android/incallui/answer/impl/hint/PawSecretCodeListener.java b/java/com/android/incallui/answer/impl/hint/PawSecretCodeListener.java
new file mode 100644
index 0000000..b4fc19c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/PawSecretCodeListener.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.hint;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.widget.Toast;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression.Type;
+import java.util.Random;
+
+/**
+ * Listen to the broadcast when the user dials "*#*#[number]#*#*" to toggle the event answer hint.
+ */
+public class PawSecretCodeListener extends BroadcastReceiver {
+
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  static final String CONFIG_PAW_SECRET_CODE = "paw_secret_code";
+
+  public static final String PAW_ENABLED_WITH_SECRET_CODE_KEY = "paw_enabled_with_secret_code";
+  public static final String PAW_DRAWABLE_ID_KEY = "paw_drawable_id";
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    String host = intent.getData().getHost();
+    Assert.checkState(!TextUtils.isEmpty(host));
+    String secretCode =
+        ConfigProviderBindings.get(context).getString(CONFIG_PAW_SECRET_CODE, "729");
+    if (secretCode == null) {
+      return;
+    }
+    if (!TextUtils.equals(secretCode, host)) {
+      return;
+    }
+    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+    boolean wasEnabled = preferences.getBoolean(PAW_ENABLED_WITH_SECRET_CODE_KEY, false);
+    if (wasEnabled) {
+      preferences.edit().putBoolean(PAW_ENABLED_WITH_SECRET_CODE_KEY, false).apply();
+      Toast.makeText(context, R.string.event_deactivated, Toast.LENGTH_SHORT).show();
+      Logger.get(context).logImpression(Type.EVENT_ANSWER_HINT_DEACTIVATED);
+      LogUtil.i("PawSecretCodeListener.onReceive", "PawAnswerHint disabled");
+    } else {
+      int drawableId;
+      if (new Random().nextBoolean()) {
+        drawableId = R.drawable.cat_paw;
+      } else {
+        drawableId = R.drawable.dog_paw;
+      }
+      preferences
+          .edit()
+          .putBoolean(PAW_ENABLED_WITH_SECRET_CODE_KEY, true)
+          .putInt(PAW_DRAWABLE_ID_KEY, drawableId)
+          .apply();
+      Toast.makeText(context, R.string.event_activated, Toast.LENGTH_SHORT).show();
+      Logger.get(context).logImpression(Type.EVENT_ANSWER_HINT_ACTIVATED);
+      LogUtil.i("PawSecretCodeListener.onReceive", "PawAnswerHint enabled");
+    }
+  }
+}
diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp
new file mode 100644
index 0000000..f7ff6eb
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp
Binary files differ
diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp
new file mode 100644
index 0000000..3a23254
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp
Binary files differ
diff --git a/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml b/java/com/android/incallui/answer/impl/hint/res/layout/paw_hint.xml
similarity index 89%
rename from java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml
rename to java/com/android/incallui/answer/impl/hint/res/layout/paw_hint.xml
index d505014..c3b12a0 100644
--- a/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml
+++ b/java/com/android/incallui/answer/impl/hint/res/layout/paw_hint.xml
@@ -24,9 +24,10 @@
   android:clipToPadding="false"
   android:visibility="gone">
   <ImageView
-    android:id="@+id/payload"
-    android:layout_width="191dp"
-    android:layout_height="773dp"
+    android:id="@+id/paw_image"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:src="@drawable/cat_paw"
     android:layout_gravity="center"
     android:alpha="0"
     android:rotation="-30"
diff --git a/java/com/android/incallui/answer/impl/proguard.flags b/java/com/android/incallui/answer/impl/proguard.flags
new file mode 100644
index 0000000..0163528
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/proguard.flags
@@ -0,0 +1,5 @@
+# Used in com.android.dialer.answer.impl.SmsBottomSheetFragment
+-keep class android.support.design.widget.BottomSheetBehavior {
+    public <init>(android.content.Context, android.util.AttributeSet);
+    public <init>();
+}
\ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/res/values/dimens.xml b/java/com/android/incallui/answer/impl/res/values/dimens.xml
index c48b68f..8329707 100644
--- a/java/com/android/incallui/answer/impl/res/values/dimens.xml
+++ b/java/com/android/incallui/answer/impl/res/values/dimens.xml
@@ -22,4 +22,5 @@
   <dimen name="answer_avatar_size">0dp</dimen>
   <dimen name="answer_importance_margin_bottom">0dp</dimen>
   <bool name="answer_important_call_allowed">false</bool>
+  <integer name="answer_animate_entry_millis">1000</integer>
 </resources>
diff --git a/java/com/android/incallui/answer/protocol/AnswerScreen.java b/java/com/android/incallui/answer/protocol/AnswerScreen.java
index 0c374eb..f03efef 100644
--- a/java/com/android/incallui/answer/protocol/AnswerScreen.java
+++ b/java/com/android/incallui/answer/protocol/AnswerScreen.java
@@ -24,7 +24,7 @@
 
   String getCallId();
 
-  int getVideoState();
+  boolean isVideoCall();
 
   boolean isVideoUpgradeRequest();
 
diff --git a/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java b/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java
index 9934497..36b4e3a 100644
--- a/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java
+++ b/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java
@@ -27,7 +27,7 @@
 
   void onRejectCallWithMessage(String message);
 
-  void onAnswer(int videoState);
+  void onAnswer(boolean answerVideoAsAudio);
 
   void onReject();
 
diff --git a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
index edc3db3..6a2c4b4 100644
--- a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
+++ b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
@@ -23,7 +23,6 @@
 import com.android.dialer.common.ConfigProviderBindings;
 import com.android.dialer.common.LogUtil;
 import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
 import com.android.incallui.call.DialerCall.State;
 import com.android.incallui.call.DialerCallListener;
 
@@ -141,7 +140,7 @@
   public void onHandoverToWifiFailure() {}
 
   @Override
-  public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {}
+  public void onDialerCallSessionModificationStateChange() {}
 
   @Override
   public void onScreenOn() {
diff --git a/java/com/android/incallui/call/CallList.java b/java/com/android/incallui/call/CallList.java
index 862c71c..c88802f 100644
--- a/java/com/android/incallui/call/CallList.java
+++ b/java/com/android/incallui/call/CallList.java
@@ -38,10 +38,10 @@
 import com.android.dialer.shortcuts.ShortcutUsageReporter;
 import com.android.dialer.spam.Spam;
 import com.android.dialer.spam.SpamBindings;
-import com.android.incallui.call.DialerCall.SessionModificationState;
 import com.android.incallui.call.DialerCall.State;
 import com.android.incallui.latencyreport.LatencyReport;
 import com.android.incallui.util.TelecomCallUtil;
+import com.android.incallui.videotech.VideoTech;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.Map;
@@ -110,6 +110,8 @@
     Trace.beginSection("onCallAdded");
     final DialerCall call =
         new DialerCall(context, this, telecomCall, latencyReport, true /* registerCallback */);
+    logSecondIncomingCall(context, call);
+
     final DialerCallListenerImpl dialerCallListener = new DialerCallListenerImpl(call);
     call.addListener(dialerCallListener);
     LogUtil.d("CallList.onCallAdded", "callState=" + call.getState());
@@ -184,6 +186,30 @@
     Trace.endSection();
   }
 
+  private void logSecondIncomingCall(@NonNull Context context, @NonNull DialerCall incomingCall) {
+    DialerCall firstCall = getFirstCall();
+    if (firstCall != null) {
+      int impression = 0;
+      if (firstCall.isVideoCall()) {
+        if (incomingCall.isVideoCall()) {
+          impression = DialerImpression.Type.VIDEO_CALL_WITH_INCOMING_VIDEO_CALL;
+        } else {
+          impression = DialerImpression.Type.VIDEO_CALL_WITH_INCOMING_VOICE_CALL;
+        }
+      } else {
+        if (incomingCall.isVideoCall()) {
+          impression = DialerImpression.Type.VOICE_CALL_WITH_INCOMING_VIDEO_CALL;
+        } else {
+          impression = DialerImpression.Type.VOICE_CALL_WITH_INCOMING_VOICE_CALL;
+        }
+      }
+      Assert.checkArgument(impression != 0);
+      Logger.get(context)
+          .logCallImpression(
+              impression, incomingCall.getUniqueCallId(), incomingCall.getTimeAddedMs());
+    }
+  }
+
   private static boolean isPotentialEmergencyCallback(Context context, DialerCall call) {
     if (BuildCompat.isAtLeastO()) {
       return call.isPotentialEmergencyCallback();
@@ -440,8 +466,8 @@
    */
   public DialerCall getVideoUpgradeRequestCall() {
     for (DialerCall call : mCallById.values()) {
-      if (call.getSessionModificationState()
-          == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+      if (call.getVideoTech().getSessionModificationState()
+          == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
         return call;
       }
     }
@@ -637,17 +663,7 @@
    */
   public void notifyCallsOfDeviceRotation(int rotation) {
     for (DialerCall call : mCallById.values()) {
-      // First, ensure that the call videoState has video enabled (there is no need to set
-      // device orientation on a voice call which has not yet been upgraded to video).
-      // Second, ensure a VideoCall is set on the call so that the change can be sent to the
-      // provider (a VideoCall can be present for a call that does not currently have video,
-      // but can be upgraded to video).
-
-      // NOTE: is it necessary to use this order because getVideoCall references the class
-      // VideoProfile which is not available on APIs <23 (M).
-      if (VideoUtils.isVideoCall(call) && call.getVideoCall() != null) {
-        call.getVideoCall().setDeviceOrientation(rotation);
-      }
+      call.getVideoTech().setDeviceOrientation(rotation);
     }
   }
 
@@ -675,7 +691,7 @@
     void onUpgradeToVideo(DialerCall call);
 
     /** Called when the session modification state of a call changes. */
-    void onSessionModificationStateChange(@SessionModificationState int newState);
+    void onSessionModificationStateChange(DialerCall call);
 
     /**
      * Called anytime there are changes to the call list. The change can be switching call states,
@@ -754,9 +770,9 @@
     }
 
     @Override
-    public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {
+    public void onDialerCallSessionModificationStateChange() {
       for (Listener listener : mListeners) {
-        listener.onSessionModificationStateChange(state);
+        listener.onSessionModificationStateChange(mCall);
       }
     }
   }
diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java
index bd8f006..15a0233 100644
--- a/java/com/android/incallui/call/DialerCall.java
+++ b/java/com/android/incallui/call/DialerCall.java
@@ -24,6 +24,7 @@
 import android.os.Bundle;
 import android.os.Trace;
 import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.telecom.Call;
 import android.telecom.Call.Details;
@@ -47,10 +48,15 @@
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.ConfigProviderBindings;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
 import com.android.dialer.logging.nano.ContactLookupResult;
-import com.android.dialer.util.CallUtil;
 import com.android.incallui.latencyreport.LatencyReport;
 import com.android.incallui.util.TelecomCallUtil;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.VideoTechListener;
+import com.android.incallui.videotech.empty.EmptyVideoTech;
+import com.android.incallui.videotech.ims.ImsVideoTech;
+import com.android.incallui.videotech.rcs.RcsVideoShare;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -62,11 +68,16 @@
 import java.util.concurrent.TimeUnit;
 
 /** Describes a single call and its state. */
-public class DialerCall {
+public class DialerCall implements VideoTechListener {
 
   public static final int CALL_HISTORY_STATUS_UNKNOWN = 0;
   public static final int CALL_HISTORY_STATUS_PRESENT = 1;
   public static final int CALL_HISTORY_STATUS_NOT_PRESENT = 2;
+
+  // Hard coded property for {@code Call}. Upstreamed change from Motorola.
+  // TODO(b/35359461): Move it to Telecom in framework.
+  public static final int PROPERTY_CODEC_KNOWN = 0x04000000;
+
   private static final String ID_PREFIX = "DialerCall_";
   private static final String CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS =
       "emergency_callback_window_millis";
@@ -82,13 +93,13 @@
   private final LatencyReport mLatencyReport;
   private final String mId;
   private final List<String> mChildCallIds = new ArrayList<>();
-  private final VideoSettings mVideoSettings = new VideoSettings();
   private final LogState mLogState = new LogState();
   private final Context mContext;
   private final DialerCallDelegate mDialerCallDelegate;
   private final List<DialerCallListener> mListeners = new CopyOnWriteArrayList<>();
   private final List<CannedTextResponsesLoadedListener> mCannedTextResponsesLoadedListeners =
       new CopyOnWriteArrayList<>();
+  private final VideoTechManager mVideoTechManager;
 
   private boolean mIsEmergencyCall;
   private Uri mHandle;
@@ -98,13 +109,6 @@
   private boolean hasShownWiFiToLteHandoverToast;
   private boolean doNotShowDialogForHandoffToWifiFailure;
 
-  @SessionModificationState private int mSessionModificationState;
-  private int mVideoState;
-  /** mRequestedVideoState is used to store requested upgrade / downgrade video state */
-  private int mRequestedVideoState = VideoProfile.STATE_AUDIO_ONLY;
-
-  private InCallVideoCallCallback mVideoCallCallback;
-  private boolean mIsVideoCallCallbackRegistered;
   private String mChildNumber;
   private String mLastForwardedNumber;
   private String mCallSubject;
@@ -118,6 +122,7 @@
   private boolean didShowCameraPermission;
   private String callProviderLabel;
   private String callbackNumber;
+  private int mCameraDirection = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
 
   public static String getNumberFromHandle(Uri handle) {
     return handle == null ? "" : handle.getSchemeSpecificPart();
@@ -125,7 +130,7 @@
 
   /**
    * Whether the call is put on hold by remote party. This is different than the {@link
-   * State.ONHOLD} state which indicates that the call is being held locally on the device.
+   * State#ONHOLD} state which indicates that the call is being held locally on the device.
    */
   private boolean isRemotelyHeld;
 
@@ -189,7 +194,7 @@
         @Override
         public void onCallDestroyed(Call call) {
           LogUtil.v("TelecomCallCallback.onStateChanged", "call=" + call);
-          call.unregisterCallback(this);
+          unregisterCallback();
         }
 
         @Override
@@ -248,7 +253,10 @@
     mLatencyReport = latencyReport;
     mId = ID_PREFIX + Integer.toString(sIdCounter++);
 
-    updateFromTelecomCall(registerCallback);
+    // Must be after assigning mTelecomCall
+    mVideoTechManager = new VideoTechManager(this);
+
+    updateFromTelecomCall();
 
     if (registerCallback) {
       mTelecomCall.registerCallback(mTelecomCallCallback);
@@ -348,19 +356,24 @@
     return mTelecomCall.getDetails().getStatusHints();
   }
 
-  /**
-   * @return video settings of the call, null if the call is not a video call.
-   * @see VideoProfile
-   */
-  public VideoSettings getVideoSettings() {
-    return mVideoSettings;
+  public int getCameraDir() {
+    return mCameraDirection;
+  }
+
+  public void setCameraDir(int cameraDir) {
+    if (cameraDir == CameraDirection.CAMERA_DIRECTION_FRONT_FACING
+        || cameraDir == CameraDirection.CAMERA_DIRECTION_BACK_FACING) {
+      mCameraDirection = cameraDir;
+    } else {
+      mCameraDirection = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
+    }
   }
 
   private void update() {
     Trace.beginSection("Update");
     int oldState = getState();
     // We want to potentially register a video call callback here.
-    updateFromTelecomCall(true /* registerCallback */);
+    updateFromTelecomCall();
     if (oldState != getState() && getState() == DialerCall.State.DISCONNECTED) {
       for (DialerCallListener listener : mListeners) {
         listener.onDialerCallDisconnect();
@@ -373,21 +386,15 @@
     Trace.endSection();
   }
 
-  private void updateFromTelecomCall(boolean registerCallback) {
+  private void updateFromTelecomCall() {
     LogUtil.v("DialerCall.updateFromTelecomCall", mTelecomCall.toString());
+
+    mVideoTechManager.dispatchCallStateChanged(mTelecomCall.getState());
+
     final int translatedState = translateState(mTelecomCall.getState());
     if (mState != State.BLOCKED) {
       setState(translatedState);
       setDisconnectCause(mTelecomCall.getDetails().getDisconnectCause());
-      maybeCancelVideoUpgrade(mTelecomCall.getDetails().getVideoState());
-    }
-
-    if (registerCallback && mTelecomCall.getVideoCall() != null) {
-      if (mVideoCallCallback == null) {
-        mVideoCallCallback = new InCallVideoCallCallback(this);
-      }
-      mTelecomCall.getVideoCall().registerCallback(mVideoCallCallback);
-      mIsVideoCallCallbackRegistered = true;
     }
 
     mChildCallIds.clear();
@@ -428,19 +435,6 @@
         }
       }
     }
-
-    if (mSessionModificationState
-            == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
-        && isVideoCall()) {
-      // We find out in {@link InCallVideoCallCallback.onSessionModifyResponseReceived}
-      // whether the video upgrade request was accepted. We don't clear the session modification
-      // state right away though to avoid having the UI switch from video to voice to video.
-      // Once the underlying telecom call updates to video mode it's safe to clear the state.
-      LogUtil.i(
-          "DialerCall.updateFromTelecomCall",
-          "upgraded to video, clearing session modification state");
-      setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
-    }
   }
 
   /**
@@ -518,25 +512,6 @@
     }
   }
 
-  /**
-   * Determines if a received upgrade to video request should be cancelled. This can happen if
-   * another InCall UI responds to the upgrade to video request.
-   *
-   * @param newVideoState The new video state.
-   */
-  private void maybeCancelVideoUpgrade(int newVideoState) {
-    boolean isVideoStateChanged = mVideoState != newVideoState;
-
-    if (mSessionModificationState
-            == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
-        && isVideoStateChanged) {
-
-      LogUtil.i("DialerCall.maybeCancelVideoUpgrade", "cancelling upgrade notification");
-      setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
-    }
-    mVideoState = newVideoState;
-  }
-
   public String getId() {
     return mId;
   }
@@ -710,6 +685,7 @@
     return mTelecomCall.getDetails().hasProperty(property);
   }
 
+  @NonNull
   public String getUniqueCallId() {
     return uniqueCallId;
   }
@@ -733,15 +709,9 @@
     return mTelecomCall == null ? null : mTelecomCall.getDetails().getAccountHandle();
   }
 
-  /**
-   * @return The {@link VideoCall} instance associated with the {@link Call}. Will return {@code
-   *     null} until {@link #updateFromTelecomCall(boolean)} has registered a valid callback on the
-   *     {@link VideoCall}.
-   */
+  /** @return The {@link VideoCall} instance associated with the {@link Call}. */
   public VideoCall getVideoCall() {
-    return mTelecomCall == null || !mIsVideoCallCallbackRegistered
-        ? null
-        : mTelecomCall.getVideoCall();
+    return mTelecomCall == null ? null : mTelecomCall.getVideoCall();
   }
 
   public List<String> getChildCallIds() {
@@ -761,7 +731,15 @@
   }
 
   public boolean isVideoCall() {
-    return CallUtil.isVideoEnabled(mContext) && VideoUtils.isVideoCall(getVideoState());
+    return getVideoTech().isTransmittingOrReceiving();
+  }
+
+  public boolean hasReceivedVideoUpgradeRequest() {
+    return VideoUtils.hasReceivedVideoUpgradeRequest(getVideoTech().getSessionModificationState());
+  }
+
+  public boolean hasSentVideoUpgradeRequest() {
+    return VideoUtils.hasSentVideoUpgradeRequest(getVideoTech().getSessionModificationState());
   }
 
   /**
@@ -772,76 +750,6 @@
     mIsEmergencyCall = TelecomCallUtil.isEmergencyCall(mTelecomCall);
   }
 
-  /**
-   * Gets the video state which was requested via a session modification request.
-   *
-   * @return The video state.
-   */
-  public int getRequestedVideoState() {
-    return mRequestedVideoState;
-  }
-
-  /**
-   * Handles incoming session modification requests. Stores the pending video request and sets the
-   * session modification state to {@link
-   * DialerCall#SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST} so that we can keep
-   * track of the fact the request was received. Only upgrade requests require user confirmation and
-   * will be handled by this method. The remote user can turn off their own camera without
-   * confirmation.
-   *
-   * @param videoState The requested video state.
-   */
-  public void setRequestedVideoState(int videoState) {
-    LogUtil.v("DialerCall.setRequestedVideoState", "videoState: " + videoState);
-    if (videoState == getVideoState()) {
-      LogUtil.e("DialerCall.setRequestedVideoState", "clearing session modification state");
-      setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
-      return;
-    }
-
-    mRequestedVideoState = videoState;
-    setSessionModificationState(
-        DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
-    for (DialerCallListener listener : mListeners) {
-      listener.onDialerCallUpgradeToVideo();
-    }
-
-    LogUtil.i(
-        "DialerCall.setRequestedVideoState",
-        "mSessionModificationState: %d, videoState: %d",
-        mSessionModificationState,
-        videoState);
-    update();
-  }
-
-  /**
-   * Gets the current video session modification state.
-   *
-   * @return The session modification state.
-   */
-  @SessionModificationState
-  public int getSessionModificationState() {
-    return mSessionModificationState;
-  }
-
-  /**
-   * Set the session modification state. Used to keep track of pending video session modification
-   * operations and to inform listeners of these changes.
-   *
-   * @param state the new session modification state.
-   */
-  public void setSessionModificationState(@SessionModificationState int state) {
-    boolean hasChanged = mSessionModificationState != state;
-    if (hasChanged) {
-      LogUtil.i(
-          "DialerCall.setSessionModificationState", "%d -> %d", mSessionModificationState, state);
-      mSessionModificationState = state;
-      for (DialerCallListener listener : mListeners) {
-        listener.onDialerCallSessionModificationStateChange(state);
-      }
-    }
-  }
-
   public LogState getLogState() {
     return mLogState;
   }
@@ -862,24 +770,6 @@
   }
 
   /**
-   * Determines if the external call is pullable.
-   *
-   * <p>An external call is one which does not exist locally for the {@link
-   * android.telecom.ConnectionService} it is associated with. An external call may be "pullable",
-   * which means that the user can request it be transferred to the current device.
-   *
-   * <p>External calls are only supported in N and higher.
-   *
-   * @return {@code true} if the call is an external call, {@code false} otherwise.
-   */
-  public boolean isPullableExternalCall() {
-    return VERSION.SDK_INT >= VERSION_CODES.N
-        && (mTelecomCall.getDetails().getCallCapabilities()
-                & CallCompat.Details.CAPABILITY_CAN_PULL_CALL)
-            == CallCompat.Details.CAPABILITY_CAN_PULL_CALL;
-  }
-
-  /**
    * Determines if answering this call will cause an ongoing video call to be dropped.
    *
    * @return {@code true} if answering this call will drop an ongoing video call, {@code false}
@@ -922,7 +812,7 @@
     return String.format(
         Locale.US,
         "[%s, %s, %s, %s, children:%s, parent:%s, "
-            + "conferenceable:%s, videoState:%s, mSessionModificationState:%d, VideoSettings:%s]",
+            + "conferenceable:%s, videoState:%s, mSessionModificationState:%d, CameraDir:%s]",
         mId,
         State.toString(getState()),
         Details.capabilitiesToString(mTelecomCall.getDetails().getCallCapabilities()),
@@ -931,8 +821,8 @@
         getParentId(),
         this.mTelecomCall.getConferenceableCalls(),
         VideoProfile.videoStateToString(mTelecomCall.getDetails().getVideoState()),
-        mSessionModificationState,
-        getVideoSettings());
+        getVideoTech().getSessionModificationState(),
+        getCameraDir());
   }
 
   public String toSimpleString() {
@@ -1012,20 +902,6 @@
     mTelecomCall.unregisterCallback(mTelecomCallCallback);
   }
 
-  public void acceptUpgradeRequest(int videoState) {
-    LogUtil.i("DialerCall.acceptUpgradeRequest", "videoState: " + videoState);
-    VideoProfile videoProfile = new VideoProfile(videoState);
-    getVideoCall().sendSessionModifyResponse(videoProfile);
-    setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
-  }
-
-  public void declineUpgradeRequest() {
-    LogUtil.i("DialerCall.declineUpgradeRequest", "");
-    VideoProfile videoProfile = new VideoProfile(getVideoState());
-    getVideoCall().sendSessionModifyResponse(videoProfile);
-    setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
-  }
-
   public void phoneAccountSelected(PhoneAccountHandle accountHandle, boolean setDefault) {
     LogUtil.i(
         "DialerCall.phoneAccountSelected",
@@ -1064,6 +940,10 @@
     mTelecomCall.answer(videoState);
   }
 
+  public void answer() {
+    answer(mTelecomCall.getDetails().getVideoState());
+  }
+
   public void reject(boolean rejectWithMessage, String message) {
     LogUtil.i("DialerCall.reject", "");
     mTelecomCall.reject(rejectWithMessage, message);
@@ -1095,6 +975,10 @@
     return mContext.getSystemService(TelecomManager.class).getPhoneAccount(accountHandle);
   }
 
+  public VideoTech getVideoTech() {
+    return mVideoTechManager.getVideoTech();
+  }
+
   public String getCallbackNumber() {
     if (callbackNumber == null) {
       // Show the emergency callback number if either:
@@ -1146,6 +1030,39 @@
     return null;
   }
 
+  @Override
+  public void onVideoTechStateChanged() {
+    update();
+  }
+
+  @Override
+  public void onSessionModificationStateChanged() {
+    for (DialerCallListener listener : mListeners) {
+      listener.onDialerCallSessionModificationStateChange();
+    }
+  }
+
+  @Override
+  public void onCameraDimensionsChanged(int width, int height) {
+    InCallVideoCallCallbackNotifier.getInstance().cameraDimensionsChanged(this, width, height);
+  }
+
+  @Override
+  public void onPeerDimensionsChanged(int width, int height) {
+    InCallVideoCallCallbackNotifier.getInstance().peerDimensionsChanged(this, width, height);
+  }
+
+  @Override
+  public void onVideoUpgradeRequestReceived() {
+    LogUtil.enterBlock("DialerCall.onVideoUpgradeRequestReceived");
+
+    for (DialerCallListener listener : mListeners) {
+      listener.onDialerCallUpgradeToVideo();
+    }
+
+    update();
+  }
+
   /**
    * Specifies whether a number is in the call history or not. {@link #CALL_HISTORY_STATUS_UNKNOWN}
    * means there is no result.
@@ -1191,8 +1108,8 @@
         case CONFERENCED:
           return true;
         default:
+          return false;
       }
-      return false;
     }
 
     public static boolean isDialing(int state) {
@@ -1239,71 +1156,11 @@
     }
   }
 
-  /**
-   * Defines different states of session modify requests, which are used to upgrade to video, or
-   * downgrade to audio.
-   */
-  @Retention(RetentionPolicy.SOURCE)
-  @IntDef({
-    SESSION_MODIFICATION_STATE_NO_REQUEST,
-    SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE,
-    SESSION_MODIFICATION_STATE_REQUEST_FAILED,
-    SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
-    SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT,
-    SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED,
-    SESSION_MODIFICATION_STATE_REQUEST_REJECTED,
-    SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE
-  })
-  public @interface SessionModificationState {}
-
-  public static final int SESSION_MODIFICATION_STATE_NO_REQUEST = 0;
-  public static final int SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE = 1;
-  public static final int SESSION_MODIFICATION_STATE_REQUEST_FAILED = 2;
-  public static final int SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST = 3;
-  public static final int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT = 4;
-  public static final int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED = 5;
-  public static final int SESSION_MODIFICATION_STATE_REQUEST_REJECTED = 6;
-  public static final int SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE = 7;
-
-  public static class VideoSettings {
-
+  /** Camera direction constants */
+  public static class CameraDirection {
     public static final int CAMERA_DIRECTION_UNKNOWN = -1;
     public static final int CAMERA_DIRECTION_FRONT_FACING = CameraCharacteristics.LENS_FACING_FRONT;
     public static final int CAMERA_DIRECTION_BACK_FACING = CameraCharacteristics.LENS_FACING_BACK;
-
-    private int mCameraDirection = CAMERA_DIRECTION_UNKNOWN;
-
-    /**
-     * Gets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, the video
-     * state of the call should be used to infer the camera direction.
-     *
-     * @see {@link CameraCharacteristics#LENS_FACING_FRONT}
-     * @see {@link CameraCharacteristics#LENS_FACING_BACK}
-     */
-    public int getCameraDir() {
-      return mCameraDirection;
-    }
-
-    /**
-     * Sets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, the video
-     * state of the call should be used to infer the camera direction.
-     *
-     * @see {@link CameraCharacteristics#LENS_FACING_FRONT}
-     * @see {@link CameraCharacteristics#LENS_FACING_BACK}
-     */
-    public void setCameraDir(int cameraDirection) {
-      if (cameraDirection == CAMERA_DIRECTION_FRONT_FACING
-          || cameraDirection == CAMERA_DIRECTION_BACK_FACING) {
-        mCameraDirection = cameraDirection;
-      } else {
-        mCameraDirection = CAMERA_DIRECTION_UNKNOWN;
-      }
-    }
-
-    @Override
-    public String toString() {
-      return "(CameraDir:" + getCameraDir() + ")";
-    }
   }
 
   /**
@@ -1394,6 +1251,48 @@
     }
   }
 
+  private static class VideoTechManager {
+    private final EmptyVideoTech emptyVideoTech = new EmptyVideoTech();
+    private final VideoTech[] videoTechs;
+    private VideoTech savedTech;
+
+    VideoTechManager(DialerCall call) {
+      String phoneNumber = call.getNumber();
+
+      // Insert order here determines the priority of that video tech option
+      videoTechs =
+          new VideoTech[] {
+            new ImsVideoTech(call, call.mTelecomCall),
+            new RcsVideoShare(
+                EnrichedCallComponent.get(call.mContext).getEnrichedCallManager(),
+                call,
+                phoneNumber != null ? phoneNumber : "")
+          };
+    }
+
+    VideoTech getVideoTech() {
+      if (savedTech != null) {
+        return savedTech;
+      }
+
+      for (VideoTech tech : videoTechs) {
+        if (tech.isAvailable()) {
+          // Remember the first VideoTech that becomes available and always use it
+          savedTech = tech;
+          return savedTech;
+        }
+      }
+
+      return emptyVideoTech;
+    }
+
+    void dispatchCallStateChanged(int newState) {
+      for (VideoTech videoTech : videoTechs) {
+        videoTech.onCallStateChanged(newState);
+      }
+    }
+  }
+
   /** Called when canned text responses have been loaded. */
   public interface CannedTextResponsesLoadedListener {
     void onCannedTextResponsesLoaded(DialerCall call);
diff --git a/java/com/android/incallui/call/DialerCallListener.java b/java/com/android/incallui/call/DialerCallListener.java
index b426cd7..fece103 100644
--- a/java/com/android/incallui/call/DialerCallListener.java
+++ b/java/com/android/incallui/call/DialerCallListener.java
@@ -16,8 +16,6 @@
 
 package com.android.incallui.call;
 
-import com.android.incallui.call.DialerCall.SessionModificationState;
-
 /** Used to monitor state changes in a dialer call. */
 public interface DialerCallListener {
 
@@ -31,7 +29,7 @@
 
   void onDialerCallUpgradeToVideo();
 
-  void onDialerCallSessionModificationStateChange(@SessionModificationState int state);
+  void onDialerCallSessionModificationStateChange();
 
   void onWiFiToLteHandover();
 
diff --git a/java/com/android/incallui/call/InCallVideoCallCallback.java b/java/com/android/incallui/call/InCallVideoCallCallback.java
deleted file mode 100644
index f897ac9..0000000
--- a/java/com/android/incallui/call/InCallVideoCallCallback.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.incallui.call;
-
-import android.os.Handler;
-import android.support.annotation.Nullable;
-import android.telecom.Connection;
-import android.telecom.Connection.VideoProvider;
-import android.telecom.InCallService.VideoCall;
-import android.telecom.VideoProfile;
-import android.telecom.VideoProfile.CameraCapabilities;
-import com.android.dialer.common.LogUtil;
-import com.android.incallui.call.DialerCall.SessionModificationState;
-
-/** Implements the InCallUI VideoCall Callback. */
-public class InCallVideoCallCallback extends VideoCall.Callback implements Runnable {
-
-  private static final int CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS = 4000;
-
-  private final DialerCall call;
-  @Nullable private Handler handler;
-  @SessionModificationState private int newSessionModificationState;
-
-  public InCallVideoCallCallback(DialerCall call) {
-    this.call = call;
-  }
-
-  @Override
-  public void onSessionModifyRequestReceived(VideoProfile videoProfile) {
-    LogUtil.i(
-        "InCallVideoCallCallback.onSessionModifyRequestReceived", "videoProfile: " + videoProfile);
-    int previousVideoState = VideoUtils.getUnPausedVideoState(call.getVideoState());
-    int newVideoState = VideoUtils.getUnPausedVideoState(videoProfile.getVideoState());
-
-    boolean wasVideoCall = VideoUtils.isVideoCall(previousVideoState);
-    boolean isVideoCall = VideoUtils.isVideoCall(newVideoState);
-
-    if (wasVideoCall && !isVideoCall) {
-      LogUtil.v(
-          "InCallVideoCallCallback.onSessionModifyRequestReceived",
-          "call downgraded to " + newVideoState);
-    } else if (previousVideoState != newVideoState) {
-      InCallVideoCallCallbackNotifier.getInstance().upgradeToVideoRequest(call, newVideoState);
-    }
-  }
-
-  /**
-   * @param status Status of the session modify request. Valid values are {@link
-   *     Connection.VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, {@link
-   *     Connection.VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, {@link
-   *     Connection.VideoProvider#SESSION_MODIFY_REQUEST_INVALID}
-   * @param responseProfile The actual profile changes made by the peer device.
-   */
-  @Override
-  public void onSessionModifyResponseReceived(
-      int status, VideoProfile requestedProfile, VideoProfile responseProfile) {
-    LogUtil.i(
-        "InCallVideoCallCallback.onSessionModifyResponseReceived",
-        "status: %d, "
-            + "requestedProfile: %s, responseProfile: %s, current session modification state: %d",
-        status,
-        requestedProfile,
-        responseProfile,
-        call.getSessionModificationState());
-
-    if (call.getSessionModificationState()
-        == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE) {
-      if (handler == null) {
-        handler = new Handler();
-      } else {
-        handler.removeCallbacks(this);
-      }
-
-      newSessionModificationState = getDialerSessionModifyStateTelecomStatus(status);
-      if (status != VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
-        // This will update the video UI to display the error message.
-        call.setSessionModificationState(newSessionModificationState);
-      }
-
-      // Wait for 4 seconds and then clean the session modification state. This allows the video UI
-      // to stay up so that the user can read the error message.
-      //
-      // If the other person accepted the upgrade request then this will keep the video UI up until
-      // the call's video state change. Without this we would switch to the voice call and then
-      // switch back to video UI.
-      handler.postDelayed(this, CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS);
-    } else if (call.getSessionModificationState()
-        == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
-      call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
-    } else if (call.getSessionModificationState()
-        == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE) {
-      call.setSessionModificationState(getDialerSessionModifyStateTelecomStatus(status));
-    } else {
-      LogUtil.i(
-          "InCallVideoCallCallback.onSessionModifyResponseReceived",
-          "call is not waiting for " + "response, doing nothing");
-    }
-  }
-
-  @SessionModificationState
-  private int getDialerSessionModifyStateTelecomStatus(int telecomStatus) {
-    switch (telecomStatus) {
-      case VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS:
-        return DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST;
-      case VideoProvider.SESSION_MODIFY_REQUEST_FAIL:
-      case VideoProvider.SESSION_MODIFY_REQUEST_INVALID:
-        // Check if it's already video call, which means the request is not video upgrade request.
-        if (VideoUtils.isVideoCall(call.getVideoState())) {
-          return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
-        } else {
-          return DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED;
-        }
-      case VideoProvider.SESSION_MODIFY_REQUEST_TIMED_OUT:
-        return DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
-      case VideoProvider.SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE:
-        return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED;
-      default:
-        LogUtil.e(
-            "InCallVideoCallCallback.getDialerSessionModifyStateTelecomStatus",
-            "unknown status: %d",
-            telecomStatus);
-        return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
-    }
-  }
-
-  @Override
-  public void onCallSessionEvent(int event) {
-    InCallVideoCallCallbackNotifier.getInstance().callSessionEvent(event);
-  }
-
-  @Override
-  public void onPeerDimensionsChanged(int width, int height) {
-    InCallVideoCallCallbackNotifier.getInstance().peerDimensionsChanged(call, width, height);
-  }
-
-  @Override
-  public void onVideoQualityChanged(int videoQuality) {
-    InCallVideoCallCallbackNotifier.getInstance().videoQualityChanged(call, videoQuality);
-  }
-
-  /**
-   * Handles a change to the call data usage. No implementation as the in-call UI does not display
-   * data usage.
-   *
-   * @param dataUsage The updated data usage.
-   */
-  @Override
-  public void onCallDataUsageChanged(long dataUsage) {
-    LogUtil.v("InCallVideoCallCallback.onCallDataUsageChanged", "dataUsage = " + dataUsage);
-    InCallVideoCallCallbackNotifier.getInstance().callDataUsageChanged(dataUsage);
-  }
-
-  /**
-   * Handles changes to the camera capabilities. No implementation as the in-call UI does not make
-   * use of camera capabilities.
-   *
-   * @param cameraCapabilities The changed camera capabilities.
-   */
-  @Override
-  public void onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities) {
-    if (cameraCapabilities != null) {
-      InCallVideoCallCallbackNotifier.getInstance()
-          .cameraDimensionsChanged(
-              call, cameraCapabilities.getWidth(), cameraCapabilities.getHeight());
-    }
-  }
-
-  /**
-   * Called 4 seconds after the remote user responds to the video upgrade request. We use this to
-   * clear the session modify state.
-   */
-  @Override
-  public void run() {
-    if (call.getSessionModificationState() == newSessionModificationState) {
-      LogUtil.i("InCallVideoCallCallback.onSessionModifyResponseReceived", "clearing state");
-      call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
-    } else {
-      LogUtil.i(
-          "InCallVideoCallCallback.onSessionModifyResponseReceived",
-          "session modification state has changed, not clearing state");
-    }
-  }
-}
diff --git a/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java b/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
index 4a94926..1cb9f74 100644
--- a/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
+++ b/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
@@ -18,16 +18,12 @@
 
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import com.android.dialer.common.LogUtil;
 import java.util.Collections;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
-/**
- * Class used by {@link InCallService.VideoCallCallback} to notify interested parties of incoming
- * events.
- */
+/** Class used to notify interested parties of incoming video related events. */
 public class InCallVideoCallCallbackNotifier {
 
   /** Singleton instance of this class. */
@@ -37,12 +33,6 @@
    * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is load factor before
    * resizing, 1 means we only expect a single thread to access the map so make only a single shard
    */
-  private final Set<SessionModificationListener> mSessionModificationListeners =
-      Collections.newSetFromMap(
-          new ConcurrentHashMap<SessionModificationListener, Boolean>(8, 0.9f, 1));
-
-  private final Set<VideoEventListener> mVideoEventListeners =
-      Collections.newSetFromMap(new ConcurrentHashMap<VideoEventListener, Boolean>(8, 0.9f, 1));
   private final Set<SurfaceChangeListener> mSurfaceChangeListeners =
       Collections.newSetFromMap(new ConcurrentHashMap<SurfaceChangeListener, Boolean>(8, 0.9f, 1));
 
@@ -55,48 +45,6 @@
   }
 
   /**
-   * Adds a new {@link SessionModificationListener}.
-   *
-   * @param listener The listener.
-   */
-  public void addSessionModificationListener(@NonNull SessionModificationListener listener) {
-    Objects.requireNonNull(listener);
-    mSessionModificationListeners.add(listener);
-  }
-
-  /**
-   * Remove a {@link SessionModificationListener}.
-   *
-   * @param listener The listener.
-   */
-  public void removeSessionModificationListener(@Nullable SessionModificationListener listener) {
-    if (listener != null) {
-      mSessionModificationListeners.remove(listener);
-    }
-  }
-
-  /**
-   * Adds a new {@link VideoEventListener}.
-   *
-   * @param listener The listener.
-   */
-  public void addVideoEventListener(@NonNull VideoEventListener listener) {
-    Objects.requireNonNull(listener);
-    mVideoEventListeners.add(listener);
-  }
-
-  /**
-   * Remove a {@link VideoEventListener}.
-   *
-   * @param listener The listener.
-   */
-  public void removeVideoEventListener(@Nullable VideoEventListener listener) {
-    if (listener != null) {
-      mVideoEventListeners.remove(listener);
-    }
-  }
-
-  /**
    * Adds a new {@link SurfaceChangeListener}.
    *
    * @param listener The listener.
@@ -118,56 +66,6 @@
   }
 
   /**
-   * Inform listeners of an upgrade to video request for a call.
-   *
-   * @param call The call.
-   * @param videoState The video state we want to upgrade to.
-   */
-  public void upgradeToVideoRequest(DialerCall call, int videoState) {
-    LogUtil.v(
-        "InCallVideoCallCallbackNotifier.upgradeToVideoRequest",
-        "call = " + call + " new video state = " + videoState);
-    for (SessionModificationListener listener : mSessionModificationListeners) {
-      listener.onUpgradeToVideoRequest(call, videoState);
-    }
-  }
-
-  /**
-   * Inform listeners of a call session event.
-   *
-   * @param event The call session event.
-   */
-  public void callSessionEvent(int event) {
-    for (VideoEventListener listener : mVideoEventListeners) {
-      listener.onCallSessionEvent(event);
-    }
-  }
-
-  /**
-   * Inform listeners of a downgrade to audio.
-   *
-   * @param call The call.
-   * @param paused The paused state.
-   */
-  public void peerPausedStateChanged(DialerCall call, boolean paused) {
-    for (VideoEventListener listener : mVideoEventListeners) {
-      listener.onPeerPauseStateChanged(call, paused);
-    }
-  }
-
-  /**
-   * Inform listeners of any change in the video quality of the call
-   *
-   * @param call The call.
-   * @param videoQuality The updated video quality of the call.
-   */
-  public void videoQualityChanged(DialerCall call, int videoQuality) {
-    for (VideoEventListener listener : mVideoEventListeners) {
-      listener.onVideoQualityChanged(call, videoQuality);
-    }
-  }
-
-  /**
    * Inform listeners of a change to peer dimensions.
    *
    * @param call The call.
@@ -194,67 +92,6 @@
   }
 
   /**
-   * Inform listeners of a change to call data usage.
-   *
-   * @param dataUsage data usage value
-   */
-  public void callDataUsageChanged(long dataUsage) {
-    for (VideoEventListener listener : mVideoEventListeners) {
-      listener.onCallDataUsageChange(dataUsage);
-    }
-  }
-
-  /** Listener interface for any class that wants to be notified of upgrade to video request. */
-  public interface SessionModificationListener {
-
-    /**
-     * Called when a peer request is received to upgrade an audio-only call to a video call.
-     *
-     * @param call The call the request was received for.
-     * @param videoState The requested video state.
-     */
-    void onUpgradeToVideoRequest(DialerCall call, int videoState);
-  }
-
-  /**
-   * Listener interface for any class that wants to be notified of video events, including pause and
-   * un-pause of peer video, video quality changes.
-   */
-  public interface VideoEventListener {
-
-    /**
-     * Called when the peer pauses or un-pauses video transmission.
-     *
-     * @param call The call which paused or un-paused video transmission.
-     * @param paused {@code True} when the video transmission is paused, {@code false} otherwise.
-     */
-    void onPeerPauseStateChanged(DialerCall call, boolean paused);
-
-    /**
-     * Called when the video quality changes.
-     *
-     * @param call The call whose video quality changes.
-     * @param videoCallQuality - values are QUALITY_HIGH, MEDIUM, LOW and UNKNOWN.
-     */
-    void onVideoQualityChanged(DialerCall call, int videoCallQuality);
-
-    /*
-     * Called when call data usage value is requested or when call data usage value is updated
-     * because of a call state change
-     *
-     * @param dataUsage call data usage value
-     */
-    void onCallDataUsageChange(long dataUsage);
-
-    /**
-     * Called when call session event is raised.
-     *
-     * @param event The call session event.
-     */
-    void onCallSessionEvent(int event);
-  }
-
-  /**
    * Listener interface for any class that wants to be notified of changes to the video surfaces.
    */
   public interface SurfaceChangeListener {
diff --git a/java/com/android/incallui/call/VideoUtils.java b/java/com/android/incallui/call/VideoUtils.java
index 80fbfb1..b99b732 100644
--- a/java/com/android/incallui/call/VideoUtils.java
+++ b/java/com/android/incallui/call/VideoUtils.java
@@ -19,113 +19,24 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
 import android.support.v4.content.ContextCompat;
-import android.telecom.VideoProfile;
-import com.android.dialer.compat.CompatUtils;
 import com.android.dialer.util.DialerUtils;
-import com.android.incallui.call.DialerCall.SessionModificationState;
-import java.util.Objects;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.SessionModificationState;
 
 public class VideoUtils {
 
   private static final String PREFERENCE_CAMERA_ALLOWED_BY_USER = "camera_allowed_by_user";
 
-  public static boolean isVideoCall(@Nullable DialerCall call) {
-    return call != null && isVideoCall(call.getVideoState());
-  }
-
-  public static boolean isVideoCall(int videoState) {
-    return CompatUtils.isVideoCompatible()
-        && (VideoProfile.isTransmissionEnabled(videoState)
-            || VideoProfile.isReceptionEnabled(videoState));
-  }
-
-  public static boolean hasSentVideoUpgradeRequest(@Nullable DialerCall call) {
-    return call != null && hasSentVideoUpgradeRequest(call.getSessionModificationState());
-  }
-
   public static boolean hasSentVideoUpgradeRequest(@SessionModificationState int state) {
-    return state == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
-        || state == DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED
-        || state == DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED
-        || state == DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
-  }
-
-  public static boolean hasReceivedVideoUpgradeRequest(@Nullable DialerCall call) {
-    return call != null && hasReceivedVideoUpgradeRequest(call.getSessionModificationState());
+    return state == VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
+        || state == VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED
+        || state == VideoTech.SESSION_MODIFICATION_STATE_REQUEST_REJECTED
+        || state == VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
   }
 
   public static boolean hasReceivedVideoUpgradeRequest(@SessionModificationState int state) {
-    return state == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
-  }
-
-  public static boolean isBidirectionalVideoCall(DialerCall call) {
-    return CompatUtils.isVideoCompatible() && VideoProfile.isBidirectional(call.getVideoState());
-  }
-
-  public static boolean isTransmissionEnabled(DialerCall call) {
-    if (!CompatUtils.isVideoCompatible()) {
-      return false;
-    }
-
-    return VideoProfile.isTransmissionEnabled(call.getVideoState());
-  }
-
-  public static boolean isIncomingVideoCall(DialerCall call) {
-    if (!VideoUtils.isVideoCall(call)) {
-      return false;
-    }
-    final int state = call.getState();
-    return (state == DialerCall.State.INCOMING) || (state == DialerCall.State.CALL_WAITING);
-  }
-
-  public static boolean isActiveVideoCall(DialerCall call) {
-    return VideoUtils.isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
-  }
-
-  public static boolean isOutgoingVideoCall(DialerCall call) {
-    if (!VideoUtils.isVideoCall(call)) {
-      return false;
-    }
-    final int state = call.getState();
-    return DialerCall.State.isDialing(state)
-        || state == DialerCall.State.CONNECTING
-        || state == DialerCall.State.SELECT_PHONE_ACCOUNT;
-  }
-
-  public static boolean isAudioCall(DialerCall call) {
-    if (!CompatUtils.isVideoCompatible()) {
-      return true;
-    }
-
-    return call != null && VideoProfile.isAudioOnly(call.getVideoState());
-  }
-
-  // TODO (ims-vt) Check if special handling is needed for CONF calls.
-  public static boolean canVideoPause(DialerCall call) {
-    return isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
-  }
-
-  public static VideoProfile makeVideoPauseProfile(@NonNull DialerCall call) {
-    Objects.requireNonNull(call);
-    if (VideoProfile.isAudioOnly(call.getVideoState())) {
-      throw new IllegalStateException();
-    }
-    return new VideoProfile(getPausedVideoState(call.getVideoState()));
-  }
-
-  public static VideoProfile makeVideoUnPauseProfile(@NonNull DialerCall call) {
-    Objects.requireNonNull(call);
-    return new VideoProfile(getUnPausedVideoState(call.getVideoState()));
-  }
-
-  public static int getUnPausedVideoState(int videoState) {
-    return videoState & (~VideoProfile.STATE_PAUSED);
-  }
-
-  public static int getPausedVideoState(int videoState) {
-    return videoState | VideoProfile.STATE_PAUSED;
+    return state == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
   }
 
   public static boolean hasCameraPermissionAndAllowedByUser(@NonNull Context context) {
diff --git a/java/com/android/incallui/maps/StaticMapFactory.java b/java/com/android/incallui/calllocation/CallLocation.java
similarity index 64%
rename from java/com/android/incallui/maps/StaticMapFactory.java
rename to java/com/android/incallui/calllocation/CallLocation.java
index a350138..15a6a8e 100644
--- a/java/com/android/incallui/maps/StaticMapFactory.java
+++ b/java/com/android/incallui/calllocation/CallLocation.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2017 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,15 +14,19 @@
  * limitations under the License
  */
 
-package com.android.incallui.maps;
+package com.android.incallui.calllocation;
 
-import android.location.Location;
+import android.content.Context;
 import android.support.annotation.NonNull;
 import android.support.v4.app.Fragment;
 
-/** A Factory that can create Fragments for showing a static map */
-public interface StaticMapFactory {
+/** Used to show the user's location during an emergency call. */
+public interface CallLocation {
+
+  boolean canGetLocation(@NonNull Context context);
 
   @NonNull
-  Fragment getStaticMap(@NonNull Location location);
+  Fragment getLocationFragment(@NonNull Context context);
+
+  void close();
 }
diff --git a/java/com/android/incallui/calllocation/CallLocationComponent.java b/java/com/android/incallui/calllocation/CallLocationComponent.java
new file mode 100644
index 0000000..6b1faf2
--- /dev/null
+++ b/java/com/android/incallui/calllocation/CallLocationComponent.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation;
+
+import android.content.Context;
+import dagger.Subcomponent;
+import com.android.incallui.calllocation.stub.StubCallLocationModule;
+
+/** Subcomponent that can be used to access the call location implementation. */
+public class CallLocationComponent {
+  private static CallLocationComponent instance;
+  private CallLocation callLocation;
+
+  public CallLocation getCallLocation(){
+    if (callLocation == null) {
+        callLocation = new StubCallLocationModule.StubCallLocation();
+    }
+    return callLocation;
+  }
+
+  public static CallLocationComponent get(Context context) {
+    if (instance == null) {
+        instance = new CallLocationComponent();
+    }
+    return instance;
+  }
+
+  /** Used to refer to the root application component. */
+  public interface HasComponent {
+    CallLocationComponent callLocationComponent();
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/AndroidManifest.xml b/java/com/android/incallui/calllocation/impl/AndroidManifest.xml
new file mode 100644
index 0000000..550c580
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2016 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<manifest
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.android.incallui.calllocation.impl">
+
+  <application>
+    <meta-data
+      android:name="com.google.android.gms.version"
+      android:value="@integer/google_play_services_version"/>
+  </application>
+</manifest>
diff --git a/java/com/android/voicemailomtp/NeededForTesting.java b/java/com/android/incallui/calllocation/impl/AuthException.java
similarity index 66%
copy from java/com/android/voicemailomtp/NeededForTesting.java
copy to java/com/android/incallui/calllocation/impl/AuthException.java
index 20517fe..26def2f 100644
--- a/java/com/android/voicemailomtp/NeededForTesting.java
+++ b/java/com/android/incallui/calllocation/impl/AuthException.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2017 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,12 +14,12 @@
  * limitations under the License
  */
 
-package com.android.voicemailomtp;
+package com.android.incallui.calllocation.impl;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
+/** For detecting backend authorization errors */
+public class AuthException extends Exception {
 
-@Retention(RetentionPolicy.SOURCE)
-public @interface NeededForTesting {
-
+  public AuthException(String detailMessage) {
+    super(detailMessage);
+  }
 }
diff --git a/java/com/android/incallui/calllocation/impl/CallLocationImpl.java b/java/com/android/incallui/calllocation/impl/CallLocationImpl.java
new file mode 100644
index 0000000..20f5ffb
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/CallLocationImpl.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.dialer.common.Assert;
+import com.android.incallui.calllocation.CallLocation;
+import javax.inject.Inject;
+
+/** Uses Google Play Services to show the user's location during an emergency call. */
+public class CallLocationImpl implements CallLocation {
+
+  private LocationHelper locationHelper;
+  private LocationFragment locationFragment;
+
+  @Inject
+  public CallLocationImpl() {}
+
+  @MainThread
+  @Override
+  public boolean canGetLocation(@NonNull Context context) {
+    Assert.isMainThread();
+    return LocationHelper.canGetLocation(context);
+  }
+
+  @MainThread
+  @NonNull
+  @Override
+  public Fragment getLocationFragment(@NonNull Context context) {
+    Assert.isMainThread();
+    if (locationFragment == null) {
+      locationFragment = new LocationFragment();
+      locationHelper = new LocationHelper(context);
+      locationHelper.addLocationListener(locationFragment.getPresenter());
+    }
+    return locationFragment;
+  }
+
+  @MainThread
+  @Override
+  public void close() {
+    Assert.isMainThread();
+    if (locationFragment != null) {
+      locationHelper.removeLocationListener(locationFragment.getPresenter());
+      locationHelper.close();
+      locationFragment = null;
+      locationHelper = null;
+    }
+  }
+}
diff --git a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java b/java/com/android/incallui/calllocation/impl/CallLocationModule.java
similarity index 66%
copy from java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
copy to java/com/android/incallui/calllocation/impl/CallLocationModule.java
index 39c55d0..73e8555 100644
--- a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
+++ b/java/com/android/incallui/calllocation/impl/CallLocationModule.java
@@ -14,19 +14,16 @@
  * limitations under the License
  */
 
-package com.android.dialer.enrichedcall;
+package com.android.incallui.calllocation.impl;
 
+import com.android.incallui.calllocation.CallLocation;
+import dagger.Binds;
 import dagger.Module;
-import dagger.Provides;
-import javax.inject.Singleton;
 
-/** Module which binds {@link EnrichedCallManagerStub}. */
+/** This module provides an instance of call location. */
 @Module
-public class StubEnrichedCallModule {
+public abstract class CallLocationModule {
 
-  @Provides
-  @Singleton
-  static EnrichedCallManager provideEnrichedCallManager() {
-    return new EnrichedCallManagerStub();
-  }
+  @Binds
+  public abstract CallLocation bindCallLocation(CallLocationImpl callLocation);
 }
diff --git a/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java b/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java
new file mode 100644
index 0000000..801b0d3
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.net.TrafficStats;
+import android.os.AsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.calllocation.impl.LocationPresenter.LocationUi;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.net.URL;
+
+class DownloadMapImageTask extends AsyncTask<Location, Void, Drawable> {
+
+  private static final String STATIC_MAP_SRC_NAME = "src";
+
+  private final WeakReference<LocationUi> mUiReference;
+
+  public DownloadMapImageTask(WeakReference<LocationUi> uiReference) {
+    mUiReference = uiReference;
+  }
+
+  @Override
+  protected Drawable doInBackground(Location... locations) {
+    LocationUi ui = mUiReference.get();
+    if (ui == null) {
+      return null;
+    }
+    if (locations == null || locations.length == 0) {
+      LogUtil.e("DownloadMapImageTask.doInBackground", "No location provided");
+      return null;
+    }
+
+    try {
+      URL mapUrl = new URL(LocationUrlBuilder.getStaticMapUrl(ui.getContext(), locations[0]));
+      InputStream content = (InputStream) mapUrl.getContent();
+
+      TrafficStats.setThreadStatsTag(TrafficStatsTags.DOWNLOAD_LOCATION_MAP_TAG);
+      return Drawable.createFromStream(content, STATIC_MAP_SRC_NAME);
+    } catch (Exception ex) {
+      LogUtil.e("DownloadMapImageTask.doInBackground", "Exception!!!", ex);
+      return null;
+    } finally {
+      TrafficStats.clearThreadStatsTag();
+    }
+  }
+
+  @Override
+  protected void onPostExecute(Drawable mapImage) {
+    LocationUi ui = mUiReference.get();
+    if (ui == null) {
+      return;
+    }
+
+    try {
+      ui.setMap(mapImage);
+    } catch (Exception ex) {
+      LogUtil.e("DownloadMapImageTask.onPostExecute", "Exception!!!", ex);
+    }
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java b/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java
new file mode 100644
index 0000000..18a80b8
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Settings.Secure;
+import android.provider.Settings.SettingNotFoundException;
+import com.android.dialer.common.LogUtil;
+
+/**
+ * Helper class to check if Google Location Services is enabled. This class is based on
+ * https://docs.google.com/a/google.com/document/d/1sGm8pHgGY1QmxbLCwTZuWQASEDN7CFW9EPSZXAuGQfo
+ */
+public class GoogleLocationSettingHelper {
+
+  /** User has disagreed to use location for Google services. */
+  public static final int USE_LOCATION_FOR_SERVICES_OFF = 0;
+  /** User has agreed to use location for Google services. */
+  public static final int USE_LOCATION_FOR_SERVICES_ON = 1;
+  /** The user has neither agreed nor disagreed to use location for Google services yet. */
+  public static final int USE_LOCATION_FOR_SERVICES_NOT_SET = 2;
+
+  private static final String GOOGLE_SETTINGS_AUTHORITY = "com.google.settings";
+  private static final Uri GOOGLE_SETTINGS_CONTENT_URI =
+      Uri.parse("content://" + GOOGLE_SETTINGS_AUTHORITY + "/partner");
+  private static final String NAME = "name";
+  private static final String VALUE = "value";
+  private static final String USE_LOCATION_FOR_SERVICES = "use_location_for_services";
+
+  /** Determine if Google apps need to conform to the USE_LOCATION_FOR_SERVICES setting. */
+  public static boolean isEnforceable(Context context) {
+    final ResolveInfo ri =
+        context
+            .getPackageManager()
+            .resolveActivity(
+                new Intent("com.google.android.gsf.GOOGLE_APPS_LOCATION_SETTINGS"),
+                PackageManager.MATCH_DEFAULT_ONLY);
+    return ri != null;
+  }
+
+  /**
+   * Get the current value for the 'Use value for location' setting.
+   *
+   * @return One of {@link #USE_LOCATION_FOR_SERVICES_NOT_SET}, {@link
+   *     #USE_LOCATION_FOR_SERVICES_OFF} or {@link #USE_LOCATION_FOR_SERVICES_ON}.
+   */
+  private static int getUseLocationForServices(Context context) {
+    final ContentResolver resolver = context.getContentResolver();
+    Cursor c = null;
+    String stringValue = null;
+    try {
+      c =
+          resolver.query(
+              GOOGLE_SETTINGS_CONTENT_URI,
+              new String[] {VALUE},
+              NAME + "=?",
+              new String[] {USE_LOCATION_FOR_SERVICES},
+              null);
+      if (c != null && c.moveToNext()) {
+        stringValue = c.getString(0);
+      }
+    } catch (final RuntimeException e) {
+      LogUtil.e(
+          "GoogleLocationSettingHelper.getUseLocationForServices",
+          "Failed to get 'Use My Location' setting",
+          e);
+    } finally {
+      if (c != null) {
+        c.close();
+      }
+    }
+    if (stringValue == null) {
+      return USE_LOCATION_FOR_SERVICES_NOT_SET;
+    }
+    int value;
+    try {
+      value = Integer.parseInt(stringValue);
+    } catch (final NumberFormatException nfe) {
+      value = USE_LOCATION_FOR_SERVICES_NOT_SET;
+    }
+    return value;
+  }
+
+  /** Whether or not the system location setting is enable */
+  public static boolean isSystemLocationSettingEnabled(Context context) {
+    try {
+      return Secure.getInt(context.getContentResolver(), Secure.LOCATION_MODE)
+          != Secure.LOCATION_MODE_OFF;
+    } catch (SettingNotFoundException e) {
+      LogUtil.e(
+          "GoogleLocationSettingHelper.isSystemLocationSettingEnabled",
+          "Failed to get System Location setting",
+          e);
+      return false;
+    }
+  }
+
+  /** Convenience method that returns true is GLS is ON or if it's not enforceable. */
+  public static boolean isGoogleLocationServicesEnabled(Context context) {
+    return !isEnforceable(context)
+        || getUseLocationForServices(context) == USE_LOCATION_FOR_SERVICES_ON;
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/HttpFetcher.java b/java/com/android/incallui/calllocation/impl/HttpFetcher.java
new file mode 100644
index 0000000..7bfbaa6
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/HttpFetcher.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import static com.android.dialer.util.DialerUtils.closeQuietly;
+
+import android.content.Context;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.os.SystemClock;
+import android.util.Pair;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.MoreStrings;
+import com.google.android.common.http.UrlRules;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.ProtocolException;
+import java.net.URL;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/** Utility for making http requests. */
+public class HttpFetcher {
+
+  // Phone number
+  public static final String PARAM_ID = "id";
+  // auth token
+  public static final String PARAM_ACCESS_TOKEN = "access_token";
+  private static final String TAG = HttpFetcher.class.getSimpleName();
+
+  /**
+   * Send a http request to the given url.
+   *
+   * @param urlString The url to request.
+   * @return The response body as a byte array. Or {@literal null} if status code is not 2xx.
+   * @throws java.io.IOException when an error occurs.
+   */
+  public static byte[] sendRequestAsByteArray(
+      Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
+      throws IOException, AuthException {
+    Objects.requireNonNull(urlString);
+
+    URL url = reWriteUrl(context, urlString);
+    if (url == null) {
+      return null;
+    }
+
+    HttpURLConnection conn = null;
+    InputStream is = null;
+    boolean isError = false;
+    final long start = SystemClock.uptimeMillis();
+    try {
+      conn = (HttpURLConnection) url.openConnection();
+      setMethodAndHeaders(conn, requestMethod, headers);
+      int responseCode = conn.getResponseCode();
+      LogUtil.i("HttpFetcher.sendRequestAsByteArray", "response code: " + responseCode);
+      // All 2xx codes are successful.
+      if (responseCode / 100 == 2) {
+        is = conn.getInputStream();
+      } else {
+        is = conn.getErrorStream();
+        isError = true;
+      }
+
+      final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      final byte[] buffer = new byte[1024];
+      int bytesRead;
+
+      while ((bytesRead = is.read(buffer)) != -1) {
+        baos.write(buffer, 0, bytesRead);
+      }
+
+      if (isError) {
+        handleBadResponse(url.toString(), baos.toByteArray());
+        if (responseCode == 401) {
+          throw new AuthException("Auth error");
+        }
+        return null;
+      }
+
+      byte[] response = baos.toByteArray();
+      LogUtil.i("HttpFetcher.sendRequestAsByteArray", "received " + response.length + " bytes");
+      long end = SystemClock.uptimeMillis();
+      LogUtil.i("HttpFetcher.sendRequestAsByteArray", "fetch took " + (end - start) + " ms");
+      return response;
+    } finally {
+      closeQuietly(is);
+      if (conn != null) {
+        conn.disconnect();
+      }
+    }
+  }
+
+  /**
+   * Send a http request to the given url.
+   *
+   * @return The response body as a InputStream. Or {@literal null} if status code is not 2xx.
+   * @throws java.io.IOException when an error occurs.
+   */
+  public static InputStream sendRequestAsInputStream(
+      Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
+      throws IOException, AuthException {
+    Objects.requireNonNull(urlString);
+
+    URL url = reWriteUrl(context, urlString);
+    if (url == null) {
+      return null;
+    }
+
+    HttpURLConnection httpUrlConnection = null;
+    boolean isSuccess = false;
+    try {
+      httpUrlConnection = (HttpURLConnection) url.openConnection();
+      setMethodAndHeaders(httpUrlConnection, requestMethod, headers);
+      int responseCode = httpUrlConnection.getResponseCode();
+      LogUtil.i("HttpFetcher.sendRequestAsInputStream", "response code: " + responseCode);
+
+      if (responseCode == 401) {
+        throw new AuthException("Auth error");
+      } else if (responseCode / 100 == 2) { // All 2xx codes are successful.
+        InputStream is = httpUrlConnection.getInputStream();
+        if (is != null) {
+          is = new HttpInputStreamWrapper(httpUrlConnection, is);
+          isSuccess = true;
+          return is;
+        }
+      }
+
+      return null;
+    } finally {
+      if (httpUrlConnection != null && !isSuccess) {
+        httpUrlConnection.disconnect();
+      }
+    }
+  }
+
+  /**
+   * Set http method and headers.
+   *
+   * @param conn The connection to add headers to.
+   * @param requestMethod request method
+   * @param headers http headers where the first item in the pair is the key and second item is the
+   *     value.
+   */
+  private static void setMethodAndHeaders(
+      HttpURLConnection conn, String requestMethod, List<Pair<String, String>> headers)
+      throws ProtocolException {
+    conn.setRequestMethod(requestMethod);
+    if (headers != null) {
+      for (Pair<String, String> pair : headers) {
+        conn.setRequestProperty(pair.first, pair.second);
+      }
+    }
+  }
+
+  private static String obfuscateUrl(String urlString) {
+    final Uri uri = Uri.parse(urlString);
+    final Builder builder =
+        new Builder().scheme(uri.getScheme()).authority(uri.getAuthority()).path(uri.getPath());
+    final Set<String> names = uri.getQueryParameterNames();
+    for (String name : names) {
+      if (PARAM_ACCESS_TOKEN.equals(name)) {
+        builder.appendQueryParameter(name, "token");
+      } else {
+        final String value = uri.getQueryParameter(name);
+        if (PARAM_ID.equals(name)) {
+          builder.appendQueryParameter(name, MoreStrings.toSafeString(value));
+        } else {
+          builder.appendQueryParameter(name, value);
+        }
+      }
+    }
+    return builder.toString();
+  }
+
+  /** Same as {@link #getRequestAsString(Context, String, String, List)} with null headers. */
+  public static String getRequestAsString(Context context, String urlString)
+      throws IOException, AuthException {
+    return getRequestAsString(context, urlString, "GET" /* Default to get. */, null);
+  }
+
+  /**
+   * Send a http request to the given url.
+   *
+   * @param context The android context.
+   * @param urlString The url to request.
+   * @param headers Http headers to pass in the request. {@literal null} is allowed.
+   * @return The response body as a String. Or {@literal null} if status code is not 2xx.
+   * @throws java.io.IOException when an error occurs.
+   */
+  public static String getRequestAsString(
+      Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
+      throws IOException, AuthException {
+    final byte[] byteArr = sendRequestAsByteArray(context, urlString, requestMethod, headers);
+    if (byteArr == null) {
+      // Encountered error response... just return.
+      return null;
+    }
+    final String response = new String(byteArr);
+    LogUtil.i("HttpFetcher.getRequestAsString", "response body: " + response);
+    return response;
+  }
+
+  /**
+   * Lookup up url re-write rules from gServices and apply to the given url.
+   *
+   * <p>https://wiki.corp.google.com/twiki/bin/view/Main/AndroidGservices#URL_Rewriting_Rules
+   *
+   * @return The new url.
+   */
+  private static URL reWriteUrl(Context context, String url) {
+    final UrlRules rules = UrlRules.getRules(context.getContentResolver());
+    final UrlRules.Rule rule = rules.matchRule(url);
+    final String newUrl = rule.apply(url);
+
+    if (newUrl == null) {
+      if (LogUtil.isDebugEnabled()) {
+        // Url is blocked by re-write.
+        LogUtil.i(
+            "HttpFetcher.reWriteUrl",
+            "url " + obfuscateUrl(url) + " is blocked.  Ignoring request.");
+      }
+      return null;
+    }
+
+    if (LogUtil.isDebugEnabled()) {
+      LogUtil.i("HttpFetcher.reWriteUrl", "fetching " + obfuscateUrl(newUrl));
+      if (!newUrl.equals(url)) {
+        LogUtil.i(
+            "HttpFetcher.reWriteUrl",
+            "Original url: " + obfuscateUrl(url) + ", after re-write: " + obfuscateUrl(newUrl));
+      }
+    }
+
+    URL urlObject = null;
+    try {
+      urlObject = new URL(newUrl);
+    } catch (MalformedURLException e) {
+      LogUtil.e("HttpFetcher.reWriteUrl", "failed to parse url: " + url, e);
+    }
+    return urlObject;
+  }
+
+  private static void handleBadResponse(String url, byte[] response) {
+    LogUtil.i("HttpFetcher.handleBadResponse", "Got bad response code from url: " + url);
+    LogUtil.i("HttpFetcher.handleBadResponse", new String(response));
+  }
+
+  /** Disconnect {@link HttpURLConnection} when InputStream is closed */
+  private static class HttpInputStreamWrapper extends FilterInputStream {
+
+    final HttpURLConnection mHttpUrlConnection;
+    final long mStartMillis = SystemClock.uptimeMillis();
+
+    public HttpInputStreamWrapper(HttpURLConnection conn, InputStream in) {
+      super(in);
+      mHttpUrlConnection = conn;
+    }
+
+    @Override
+    public void close() throws IOException {
+      super.close();
+      mHttpUrlConnection.disconnect();
+      if (LogUtil.isDebugEnabled()) {
+        long endMillis = SystemClock.uptimeMillis();
+        LogUtil.i("HttpFetcher.close", "fetch took " + (endMillis - mStartMillis) + " ms");
+      }
+    }
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationFragment.java b/java/com/android/incallui/calllocation/impl/LocationFragment.java
new file mode 100644
index 0000000..b152cd6
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationFragment.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.animation.LayoutTransition;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.baseui.BaseFragment;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Fragment which shows location during E911 calls, to supplement the user with accurate location
+ * information in case the user is asked for their location by the emergency responder.
+ *
+ * <p>If location data is inaccurate, stale, or unavailable, this should not be shown.
+ */
+public class LocationFragment extends BaseFragment<LocationPresenter, LocationPresenter.LocationUi>
+    implements LocationPresenter.LocationUi {
+
+  private static final String ADDRESS_DELIMITER = ",";
+
+  // Indexes used to animate fading between views
+  private static final int LOADING_VIEW_INDEX = 0;
+  private static final int LOCATION_VIEW_INDEX = 1;
+  private static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5);
+
+  private ViewAnimator viewAnimator;
+  private ImageView locationMap;
+  private TextView addressLine1;
+  private TextView addressLine2;
+  private TextView latLongLine;
+  private Location location;
+  private ViewGroup locationLayout;
+
+  private boolean isMapSet;
+  private boolean isAddressSet;
+  private boolean isLocationSet;
+  private boolean hasTimeoutStarted;
+
+  private final Handler handler = new Handler();
+  private final Runnable dataTimeoutRunnable =
+      () -> {
+        LogUtil.i(
+            "LocationFragment.dataTimeoutRunnable",
+            "timed out so animate any future layout changes");
+        locationLayout.setLayoutTransition(new LayoutTransition());
+        showLocationNow();
+      };
+
+  @Override
+  public LocationPresenter createPresenter() {
+    return new LocationPresenter();
+  }
+
+  @Override
+  public LocationPresenter.LocationUi getUi() {
+    return this;
+  }
+
+  @Override
+  public View onCreateView(
+      LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+    final View view = inflater.inflate(R.layout.location_fragment, container, false);
+    viewAnimator = (ViewAnimator) view.findViewById(R.id.location_view_animator);
+    locationMap = (ImageView) view.findViewById(R.id.location_map);
+    addressLine1 = (TextView) view.findViewById(R.id.address_line_one);
+    addressLine2 = (TextView) view.findViewById(R.id.address_line_two);
+    latLongLine = (TextView) view.findViewById(R.id.lat_long_line);
+    locationLayout = (ViewGroup) view.findViewById(R.id.location_layout);
+    view.setOnClickListener(
+        v -> {
+          LogUtil.enterBlock("LocationFragment.onCreateView");
+          launchMap();
+        });
+    return view;
+  }
+
+  @Override
+  public void onDestroy() {
+    super.onDestroy();
+    handler.removeCallbacks(dataTimeoutRunnable);
+  }
+
+  @Override
+  public void setMap(Drawable mapImage) {
+    LogUtil.enterBlock("LocationFragment.setMap");
+    isMapSet = true;
+    locationMap.setVisibility(View.VISIBLE);
+    locationMap.setImageDrawable(mapImage);
+    displayWhenReady();
+  }
+
+  @Override
+  public void setAddress(String address) {
+    LogUtil.i("LocationFragment.setAddress", address);
+    isAddressSet = true;
+    addressLine1.setVisibility(View.VISIBLE);
+    addressLine2.setVisibility(View.VISIBLE);
+    if (TextUtils.isEmpty(address)) {
+      addressLine1.setText(null);
+      addressLine2.setText(null);
+    } else {
+
+      // Split the address after the first delimiter for display, if present.
+      // For example, "1600 Amphitheatre Parkway, Mountain View, CA 94043"
+      //     => "1600 Amphitheatre Parkway"
+      //     => "Mountain View, CA 94043"
+      int splitIndex = address.indexOf(ADDRESS_DELIMITER);
+      if (splitIndex >= 0) {
+        updateText(addressLine1, address.substring(0, splitIndex).trim());
+        updateText(addressLine2, address.substring(splitIndex + 1).trim());
+      } else {
+        updateText(addressLine1, address);
+        updateText(addressLine2, null);
+      }
+    }
+    displayWhenReady();
+  }
+
+  @Override
+  public void setLocation(Location location) {
+    LogUtil.i("LocationFragment.setLocation", String.valueOf(location));
+    isLocationSet = true;
+    this.location = location;
+
+    if (location != null) {
+      latLongLine.setVisibility(View.VISIBLE);
+      latLongLine.setText(
+          getContext()
+              .getString(
+                  R.string.lat_long_format, location.getLatitude(), location.getLongitude()));
+    }
+    displayWhenReady();
+  }
+
+  private void displayWhenReady() {
+    // Show the location if all data has loaded, otherwise prime the timeout
+    if (isMapSet && isAddressSet && isLocationSet) {
+      showLocationNow();
+    } else if (!hasTimeoutStarted) {
+      handler.postDelayed(dataTimeoutRunnable, TIMEOUT_MILLIS);
+      hasTimeoutStarted = true;
+    }
+  }
+
+  private void showLocationNow() {
+    handler.removeCallbacks(dataTimeoutRunnable);
+    if (viewAnimator.getDisplayedChild() != LOCATION_VIEW_INDEX) {
+      viewAnimator.setDisplayedChild(LOCATION_VIEW_INDEX);
+    }
+  }
+
+  @Override
+  public Context getContext() {
+    return getActivity();
+  }
+
+  private void launchMap() {
+    if (location != null) {
+      startActivity(
+          LocationUrlBuilder.getShowMapIntent(
+              location, addressLine1.getText(), addressLine2.getText()));
+    }
+  }
+
+  private static void updateText(TextView view, String text) {
+    if (!Objects.equals(text, view.getText())) {
+      view.setText(text);
+    }
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationHelper.java b/java/com/android/incallui/calllocation/impl/LocationHelper.java
new file mode 100644
index 0000000..645e9b8
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationHelper.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.location.Location;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.MainThread;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.PermissionsUtil;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
+import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
+import com.google.android.gms.common.api.ResultCallback;
+import com.google.android.gms.common.api.Status;
+import com.google.android.gms.location.LocationListener;
+import com.google.android.gms.location.LocationRequest;
+import com.google.android.gms.location.LocationServices;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Uses the Fused location service to get location and pass updates on to listeners. */
+public class LocationHelper {
+
+  private static final int MIN_UPDATE_INTERVAL_MS = 30 * 1000;
+  private static final int LAST_UPDATE_THRESHOLD_MS = 60 * 1000;
+  private static final int LOCATION_ACCURACY_THRESHOLD_METERS = 100;
+
+  private final LocationHelperInternal locationHelperInternal;
+  private final List<LocationListener> listeners = new ArrayList<>();
+
+  @MainThread
+  LocationHelper(Context context) {
+    Assert.isMainThread();
+    Assert.checkArgument(canGetLocation(context));
+    locationHelperInternal = new LocationHelperInternal(context);
+  }
+
+  static boolean canGetLocation(Context context) {
+    if (!PermissionsUtil.hasLocationPermissions(context)) {
+      LogUtil.i("LocationHelper.canGetLocation", "no location permissions.");
+      return false;
+    }
+
+    // Ensure that both system location setting is on and google location services are enabled.
+    if (!GoogleLocationSettingHelper.isGoogleLocationServicesEnabled(context)
+        || !GoogleLocationSettingHelper.isSystemLocationSettingEnabled(context)) {
+      LogUtil.i("LocationHelper.canGetLocation", "location service is disabled.");
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Whether the location is valid. We consider it valid if it was recorded within the specified
+   * time threshold of the present and has an accuracy less than the specified distance threshold.
+   *
+   * @param location The location to determine the validity of.
+   * @return {@code true} if the location is valid, and {@code false} otherwise.
+   */
+  static boolean isValidLocation(Location location) {
+    if (location != null) {
+      long locationTimeMs = location.getTime();
+      long elapsedTimeMs = System.currentTimeMillis() - locationTimeMs;
+      if (elapsedTimeMs > LAST_UPDATE_THRESHOLD_MS) {
+        LogUtil.i("LocationHelper.isValidLocation", "stale location, age: " + elapsedTimeMs);
+        return false;
+      }
+      if (location.getAccuracy() > LOCATION_ACCURACY_THRESHOLD_METERS) {
+        LogUtil.i("LocationHelper.isValidLocation", "poor accuracy: " + location.getAccuracy());
+        return false;
+      }
+      return true;
+    }
+    LogUtil.i("LocationHelper.isValidLocation", "no location");
+    return false;
+  }
+
+  @MainThread
+  void addLocationListener(LocationListener listener) {
+    Assert.isMainThread();
+    listeners.add(listener);
+  }
+
+  @MainThread
+  void removeLocationListener(LocationListener listener) {
+    Assert.isMainThread();
+    listeners.remove(listener);
+  }
+
+  @MainThread
+  void close() {
+    Assert.isMainThread();
+    LogUtil.enterBlock("LocationHelper.close");
+    listeners.clear();
+
+    if (locationHelperInternal != null) {
+      locationHelperInternal.close();
+    }
+  }
+
+  @MainThread
+  void onLocationChanged(Location location, boolean isConnected) {
+    Assert.isMainThread();
+    LogUtil.i("LocationHelper.onLocationChanged", "location: " + location);
+
+    for (LocationListener listener : listeners) {
+      listener.onLocationChanged(location);
+    }
+  }
+
+  /**
+   * This class contains all the asynchronous callbacks. It only posts location changes back to the
+   * outer class on the main thread.
+   */
+  private class LocationHelperInternal
+      implements ConnectionCallbacks, OnConnectionFailedListener, LocationListener {
+
+    private final GoogleApiClient apiClient;
+    private final ConnectivityManager connectivityManager;
+    private final Handler mainThreadHandler = new Handler();
+
+    @MainThread
+    LocationHelperInternal(Context context) {
+      Assert.isMainThread();
+      apiClient =
+          new GoogleApiClient.Builder(context)
+              .addApi(LocationServices.API)
+              .addConnectionCallbacks(this)
+              .addOnConnectionFailedListener(this)
+              .build();
+
+      LogUtil.i("LocationHelperInternal", "Connecting to location service...");
+      apiClient.connect();
+
+      connectivityManager =
+          (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+    }
+
+    void close() {
+      if (apiClient.isConnected()) {
+        LogUtil.i("LocationHelperInternal", "disconnecting");
+        LocationServices.FusedLocationApi.removeLocationUpdates(apiClient, this);
+        apiClient.disconnect();
+      }
+    }
+
+    @Override
+    public void onConnected(Bundle bundle) {
+      LogUtil.enterBlock("LocationHelperInternal.onConnected");
+      LocationRequest locationRequest =
+          LocationRequest.create()
+              .setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY)
+              .setInterval(MIN_UPDATE_INTERVAL_MS)
+              .setFastestInterval(MIN_UPDATE_INTERVAL_MS);
+
+      LocationServices.FusedLocationApi.requestLocationUpdates(apiClient, locationRequest, this)
+          .setResultCallback(
+              new ResultCallback<Status>() {
+                @Override
+                public void onResult(Status status) {
+                  if (status.getStatus().isSuccess()) {
+                    onLocationChanged(LocationServices.FusedLocationApi.getLastLocation(apiClient));
+                  }
+                }
+              });
+    }
+
+    @Override
+    public void onConnectionSuspended(int i) {
+      // Do nothing.
+    }
+
+    @Override
+    public void onConnectionFailed(ConnectionResult result) {
+      // Do nothing.
+    }
+
+    @Override
+    public void onLocationChanged(Location location) {
+      // Post new location on main thread
+      mainThreadHandler.post(
+          new Runnable() {
+            @Override
+            public void run() {
+              LocationHelper.this.onLocationChanged(location, isConnected());
+            }
+          });
+    }
+
+    /** @return Whether the phone is connected to data. */
+    private boolean isConnected() {
+      if (connectivityManager == null) {
+        return false;
+      }
+      NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+      return networkInfo != null && networkInfo.isConnectedOrConnecting();
+    }
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationPresenter.java b/java/com/android/incallui/calllocation/impl/LocationPresenter.java
new file mode 100644
index 0000000..a56fd3b
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationPresenter.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.os.AsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.baseui.Presenter;
+import com.android.incallui.baseui.Ui;
+import com.google.android.gms.location.LocationListener;
+import java.lang.ref.WeakReference;
+import java.util.Objects;
+
+/**
+ * Presenter for the {@code LocationFragment}.
+ *
+ * <p>Performs lookup for the address and map image to show.
+ */
+public class LocationPresenter extends Presenter<LocationPresenter.LocationUi>
+    implements LocationListener {
+
+  private Location mLastLocation;
+  private AsyncTask mDownloadMapTask;
+  private AsyncTask mReverseGeocodeTask;
+
+  LocationPresenter() {}
+
+  @Override
+  public void onUiReady(LocationUi ui) {
+    LogUtil.i("LocationPresenter.onUiReady", "");
+    super.onUiReady(ui);
+    updateLocation(mLastLocation, true);
+  }
+
+  @Override
+  public void onUiUnready(LocationUi ui) {
+    LogUtil.i("LocationPresenter.onUiUnready", "");
+    super.onUiUnready(ui);
+
+    if (mDownloadMapTask != null) {
+      mDownloadMapTask.cancel(true);
+    }
+    if (mReverseGeocodeTask != null) {
+      mReverseGeocodeTask.cancel(true);
+    }
+  }
+
+  @Override
+  public void onLocationChanged(Location location) {
+    LogUtil.i("LocationPresenter.onLocationChanged", "");
+    updateLocation(location, false);
+  }
+
+  private void updateLocation(Location location, boolean forceUpdate) {
+    LogUtil.i("LocationPresenter.updateLocation", "location: " + location);
+    if (forceUpdate || !Objects.equals(mLastLocation, location)) {
+      mLastLocation = location;
+      if (LocationHelper.isValidLocation(location)) {
+        LocationUi ui = getUi();
+        mDownloadMapTask = new DownloadMapImageTask(new WeakReference<>(ui)).execute(location);
+        mReverseGeocodeTask = new ReverseGeocodeTask(new WeakReference<>(ui)).execute(location);
+        if (ui != null) {
+          ui.setLocation(location);
+        } else {
+          LogUtil.i("LocationPresenter.updateLocation", "no Ui");
+        }
+      }
+    }
+  }
+
+  /** UI interface */
+  public interface LocationUi extends Ui {
+
+    void setAddress(String address);
+
+    void setMap(Drawable mapImage);
+
+    void setLocation(Location location);
+
+    Context getContext();
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java b/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java
new file mode 100644
index 0000000..a57bdf6
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.location.Location;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import java.util.Locale;
+
+class LocationUrlBuilder {
+
+  // Static Map API path constants.
+  private static final String HTTPS_SCHEME = "https";
+  private static final String MAPS_API_DOMAIN = "maps.googleapis.com";
+  private static final String MAPS_PATH = "maps";
+  private static final String API_PATH = "api";
+  private static final String STATIC_MAP_PATH = "staticmap";
+  private static final String GEOCODE_PATH = "geocode";
+  private static final String GEOCODE_OUTPUT_TYPE = "json";
+
+  // Static Map API parameter constants.
+  private static final String KEY_PARAM_KEY = "key";
+  private static final String CENTER_PARAM_KEY = "center";
+  private static final String ZOOM_PARAM_KEY = "zoom";
+  private static final String SCALE_PARAM_KEY = "scale";
+  private static final String SIZE_PARAM_KEY = "size";
+  private static final String MARKERS_PARAM_KEY = "markers";
+
+  private static final String ZOOM_PARAM_VALUE = Integer.toString(16);
+
+  private static final String LAT_LONG_DELIMITER = ",";
+
+  private static final String MARKER_DELIMITER = "|";
+  private static final String MARKER_STYLE_DELIMITER = ":";
+  private static final String MARKER_STYLE_COLOR = "color";
+  private static final String MARKER_STYLE_COLOR_RED = "red";
+
+  private static final String LAT_LNG_PARAM_KEY = "latlng";
+
+  private static final String ANDROID_API_KEY_VALUE = "AIzaSyAXdDnif6B7sBYxU8hzw9qAp3pRPVHs060";
+  private static final String BROWSER_API_KEY_VALUE = "AIzaSyBfLlvWYndiQ3RFEHli65qGQH36QIxdyCI";
+
+  /**
+   * Generates the URL to a static map image for the given location.
+   *
+   * <p>This image has the following characteristics:
+   *
+   * <p>- It is centered at the given latitude and longitutde. - It is scaled according to the
+   * device's pixel density. - There is a red marker at the given latitude and longitude.
+   *
+   * <p>Source: https://developers.google.com/maps/documentation/staticmaps/
+   *
+   * @param contxt The context.
+   * @param Location A location.
+   * @return The URL of a static map image url of the given location.
+   */
+  public static String getStaticMapUrl(Context context, Location location) {
+    final Uri.Builder builder = new Uri.Builder();
+    Resources res = context.getResources();
+    String size =
+        res.getDimensionPixelSize(R.dimen.location_map_width)
+            + "x"
+            + res.getDimensionPixelSize(R.dimen.location_map_height);
+
+    builder
+        .scheme(HTTPS_SCHEME)
+        .authority(MAPS_API_DOMAIN)
+        .appendPath(MAPS_PATH)
+        .appendPath(API_PATH)
+        .appendPath(STATIC_MAP_PATH)
+        .appendQueryParameter(CENTER_PARAM_KEY, getFormattedLatLng(location))
+        .appendQueryParameter(ZOOM_PARAM_KEY, ZOOM_PARAM_VALUE)
+        .appendQueryParameter(SIZE_PARAM_KEY, size)
+        .appendQueryParameter(SCALE_PARAM_KEY, Float.toString(res.getDisplayMetrics().density))
+        .appendQueryParameter(MARKERS_PARAM_KEY, getMarkerUrlParamValue(location))
+        .appendQueryParameter(KEY_PARAM_KEY, ANDROID_API_KEY_VALUE);
+
+    return builder.build().toString();
+  }
+
+  /**
+   * Generates the URL for a request to reverse geocode the given location.
+   *
+   * <p>Source: https://developers.google.com/maps/documentation/geocoding/#ReverseGeocoding
+   *
+   * @param Location A location.
+   */
+  public static String getReverseGeocodeUrl(Location location) {
+    final Uri.Builder builder = new Uri.Builder();
+
+    builder
+        .scheme(HTTPS_SCHEME)
+        .authority(MAPS_API_DOMAIN)
+        .appendPath(MAPS_PATH)
+        .appendPath(API_PATH)
+        .appendPath(GEOCODE_PATH)
+        .appendPath(GEOCODE_OUTPUT_TYPE)
+        .appendQueryParameter(LAT_LNG_PARAM_KEY, getFormattedLatLng(location))
+        .appendQueryParameter(KEY_PARAM_KEY, BROWSER_API_KEY_VALUE);
+
+    return builder.build().toString();
+  }
+
+  public static Intent getShowMapIntent(
+      Location location, @Nullable CharSequence addressLine1, @Nullable CharSequence addressLine2) {
+
+    String latLong = getFormattedLatLng(location);
+    String url = String.format(Locale.US, "geo: %s?q=%s", latLong, latLong);
+
+    // Add a map label
+    if (addressLine1 != null) {
+      if (addressLine2 != null) {
+        url +=
+            String.format(Locale.US, "(%s, %s)", addressLine1.toString(), addressLine2.toString());
+      } else {
+        url += String.format(Locale.US, "(%s)", addressLine1.toString());
+      }
+    } else {
+      // TODO: i18n
+      url +=
+          String.format(
+              Locale.US,
+              "(Latitude: %f, Longitude: %f)",
+              location.getLatitude(),
+              location.getLongitude());
+    }
+
+    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+    intent.setPackage("com.google.android.apps.maps");
+    return intent;
+  }
+
+  /**
+   * Returns a comma-separated latitude and longitude pair, formatted for use as a URL parameter
+   * value.
+   *
+   * @param location A location.
+   * @return The comma-separated latitude and longitude pair of that location.
+   */
+  @VisibleForTesting
+  static String getFormattedLatLng(Location location) {
+    return location.getLatitude() + LAT_LONG_DELIMITER + location.getLongitude();
+  }
+
+  /**
+   * Returns the URL parameter value for the marker, specifying its style and position.
+   *
+   * @param location A location.
+   * @return The URL parameter value for the marker.
+   */
+  @VisibleForTesting
+  static String getMarkerUrlParamValue(Location location) {
+    return MARKER_STYLE_COLOR
+        + MARKER_STYLE_DELIMITER
+        + MARKER_STYLE_COLOR_RED
+        + MARKER_DELIMITER
+        + getFormattedLatLng(location);
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java b/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java
new file mode 100644
index 0000000..eb5957b
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.impl;
+
+import android.location.Location;
+import android.net.TrafficStats;
+import android.os.AsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.calllocation.impl.LocationPresenter.LocationUi;
+import java.lang.ref.WeakReference;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+class ReverseGeocodeTask extends AsyncTask<Location, Void, String> {
+
+  // Below are the JSON keys for the reverse geocode response.
+  // Source: https://developers.google.com/maps/documentation/geocoding/#ReverseGeocoding
+  private static final String JSON_KEY_RESULTS = "results";
+  private static final String JSON_KEY_ADDRESS = "formatted_address";
+  private static final String JSON_KEY_ADDRESS_COMPONENTS = "address_components";
+  private static final String JSON_KEY_PREMISE = "premise";
+  private static final String JSON_KEY_TYPES = "types";
+  private static final String JSON_KEY_LONG_NAME = "long_name";
+  private static final String JSON_KEY_SHORT_NAME = "short_name";
+
+  private WeakReference<LocationUi> mUiReference;
+
+  public ReverseGeocodeTask(WeakReference<LocationUi> uiReference) {
+    mUiReference = uiReference;
+  }
+
+  @Override
+  protected String doInBackground(Location... locations) {
+    LocationUi ui = mUiReference.get();
+    if (ui == null) {
+      return null;
+    }
+    if (locations == null || locations.length == 0) {
+      LogUtil.e("ReverseGeocodeTask.onLocationChanged", "No location provided");
+      return null;
+    }
+
+    try {
+      String address = null;
+      String url = LocationUrlBuilder.getReverseGeocodeUrl(locations[0]);
+
+      TrafficStats.setThreadStatsTag(TrafficStatsTags.REVERSE_GEOCODE_TAG);
+      String jsonResponse = HttpFetcher.getRequestAsString(ui.getContext(), url);
+
+      // Parse the JSON response for the formatted address of the first result.
+      JSONObject responseObject = new JSONObject(jsonResponse);
+      if (responseObject != null) {
+        JSONArray results = responseObject.optJSONArray(JSON_KEY_RESULTS);
+        if (results != null && results.length() > 0) {
+          JSONObject topResult = results.optJSONObject(0);
+          if (topResult != null) {
+            address = topResult.getString(JSON_KEY_ADDRESS);
+
+            // Strip off the Premise component from the address, if present.
+            JSONArray components = topResult.optJSONArray(JSON_KEY_ADDRESS_COMPONENTS);
+            if (components != null) {
+              boolean stripped = false;
+              for (int i = 0; !stripped && i < components.length(); i++) {
+                JSONObject component = components.optJSONObject(i);
+                JSONArray types = component.optJSONArray(JSON_KEY_TYPES);
+                if (types != null) {
+                  for (int j = 0; !stripped && j < types.length(); j++) {
+                    if (JSON_KEY_PREMISE.equals(types.getString(j))) {
+                      String premise = null;
+                      if (component.has(JSON_KEY_SHORT_NAME)
+                          && address.startsWith(component.getString(JSON_KEY_SHORT_NAME))) {
+                        premise = component.getString(JSON_KEY_SHORT_NAME);
+                      } else if (component.has(JSON_KEY_LONG_NAME)
+                          && address.startsWith(component.getString(JSON_KEY_LONG_NAME))) {
+                        premise = component.getString(JSON_KEY_SHORT_NAME);
+                      }
+                      if (premise != null) {
+                        int index = address.indexOf(',', premise.length());
+                        if (index > 0 && index < address.length()) {
+                          address = address.substring(index + 1).trim();
+                        }
+                        stripped = true;
+                        break;
+                      }
+                    }
+                  }
+                }
+              }
+            }
+
+            // Strip off the country, if its USA.  Note: unfortunately the country in the formatted
+            // address field doesn't match the country in the address component fields (USA != US)
+            // so we can't easily strip off the country for all cases, thus this hack.
+            if (address.endsWith(", USA")) {
+              address = address.substring(0, address.length() - 5);
+            }
+          }
+        }
+      }
+
+      return address;
+    } catch (AuthException ex) {
+      LogUtil.e("ReverseGeocodeTask.onLocationChanged", "AuthException", ex);
+      return null;
+    } catch (JSONException ex) {
+      LogUtil.e("ReverseGeocodeTask.onLocationChanged", "JSONException", ex);
+      return null;
+    } catch (Exception ex) {
+      LogUtil.e("ReverseGeocodeTask.onLocationChanged", "Exception!!!", ex);
+      return null;
+    } finally {
+      TrafficStats.clearThreadStatsTag();
+    }
+  }
+
+  @Override
+  protected void onPostExecute(String address) {
+    LocationUi ui = mUiReference.get();
+    if (ui == null) {
+      return;
+    }
+
+    try {
+      ui.setAddress(address);
+    } catch (Exception ex) {
+      LogUtil.e("ReverseGeocodeTask.onPostExecute", "Exception!!!", ex);
+    }
+  }
+}
diff --git a/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java b/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java
similarity index 60%
copy from java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java
copy to java/com/android/incallui/calllocation/impl/TrafficStatsTags.java
index e1f2cdc..02cc2e0 100644
--- a/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java
+++ b/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java
@@ -14,17 +14,16 @@
  * limitations under the License
  */
 
-package com.android.dialer.buildtype;
+package com.android.incallui.calllocation.impl;
 
-import com.android.dialer.proguard.UsedByReflection;
+/** Constants used for logging */
+public class TrafficStatsTags {
 
-/** Gets the build type. */
-@UsedByReflection(value = "BuildType.java")
-public class BuildTypeAccessorImpl implements BuildTypeAccessor {
+  /**
+   * Must be greater than {@link com.android.contacts.common.util.TrafficStatsTags#TAG_MAX}, to
+   * respect the namespace of the tags in ContactsCommon.
+   */
+  public static final int DOWNLOAD_LOCATION_MAP_TAG = 0xd000;
 
-  @Override
-  @BuildType.Type
-  public int getBuildType() {
-    return BuildType.DOGFOOD;
-  }
+  public static final int REVERSE_GEOCODE_TAG = 0xd001;
 }
diff --git a/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml b/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml
new file mode 100644
index 0000000..a6bd075
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+~ Copyright (C) 2015 The Android Open Source Project
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~      http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License
+-->
+
+<ViewAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:tools="http://schemas.android.com/tools"
+  android:id="@+id/location_view_animator"
+  android:layout_width="match_parent"
+  android:layout_height="wrap_content"
+  android:layout_marginTop="16dp"
+  android:layout_marginBottom="16dp"
+  android:background="@android:color/white"
+  android:elevation="2dp"
+  android:inAnimation="@android:anim/fade_in"
+  android:measureAllChildren="true"
+  android:outAnimation="@android:anim/fade_out">
+
+  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/location_loading_layout"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center_vertical"
+    android:orientation="vertical">
+
+    <ProgressBar
+      android:id="@+id/location_loading_spinner"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_marginTop="28dp"
+      android:layout_marginBottom="12dp"
+      android:layout_gravity="center_horizontal"/>
+
+    <TextView
+      android:id="@+id/location_loading_text"
+      style="@style/LocationLoadingTextStyle"
+      android:layout_width="match_parent"
+      android:layout_height="24sp"
+      android:layout_marginBottom="20dp"
+      android:layout_marginStart="24dp"
+      android:layout_marginEnd="24dp"
+      android:gravity="center"
+      android:text="@string/location_loading"/>
+
+  </LinearLayout>
+
+  <GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/location_layout"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:columnCount="2"
+    android:orientation="horizontal">
+
+    <TextView
+      android:id="@+id/location_address_title"
+      style="@style/LocationAddressTitleTextStyle"
+      android:layout_width="0dp"
+      android:layout_height="20sp"
+      android:layout_marginTop="16dp"
+      android:layout_marginBottom="4dp"
+      android:layout_marginStart="16dp"
+      android:layout_columnWeight="1"
+      android:text="@string/location_title"/>
+
+    <ImageView
+      android:id="@+id/location_map"
+      android:layout_width="@dimen/location_map_width"
+      android:layout_height="@dimen/location_map_height"
+      android:layout_margin="16dp"
+      android:layout_gravity="end|center_vertical"
+      android:layout_rowSpan="4"
+      android:contentDescription="@string/location_map_description"
+      android:scaleType="centerCrop"
+      android:visibility="invisible"
+      tools:src="?android:colorPrimaryDark"
+      tools:visibility="visible"/>
+
+    <TextView
+      android:id="@+id/address_line_one"
+      style="@style/LocationAddressTextStyle"
+      android:layout_width="0dp"
+      android:layout_height="24sp"
+      android:layout_marginStart="16dp"
+      android:layout_columnWeight="1"
+      android:ellipsize="end"
+      android:lines="1"
+      android:visibility="invisible"
+      tools:text="1600 Amphitheatre Pkwy And a bit"
+      tools:visibility="visible"/>
+
+    <TextView
+      android:id="@+id/address_line_two"
+      style="@style/LocationAddressTextStyle"
+      android:layout_width="0dp"
+      android:layout_height="24sp"
+      android:layout_marginStart="16dp"
+      android:layout_columnWeight="1"
+      android:ellipsize="end"
+      android:lines="1"
+      android:visibility="invisible"
+      tools:text="Mountain View, CA 94043"
+      tools:visibility="visible"/>
+
+    <TextView
+      android:id="@+id/lat_long_line"
+      style="@style/LocationLatLongTextStyle"
+      android:layout_width="0dp"
+      android:layout_height="24sp"
+      android:layout_marginBottom="12dp"
+      android:layout_marginStart="16dp"
+      android:layout_columnWeight="1"
+      android:ellipsize="end"
+      android:lines="1"
+      android:visibility="invisible"
+      tools:text="Lat: 37.421719, Long: -122.085297"
+      tools:visibility="visible"/>
+
+  </GridLayout>
+
+</ViewAnimator>
diff --git a/java/com/android/incallui/calllocation/impl/res/values/dimens.xml b/java/com/android/incallui/calllocation/impl/res/values/dimens.xml
new file mode 100644
index 0000000..1f41816
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/values/dimens.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2014 Google Inc. All Rights Reserved. -->
+<resources>
+  <dimen name="location_map_width">92dp</dimen>
+  <dimen name="location_map_height">92dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/calllocation/impl/res/values/strings.xml b/java/com/android/incallui/calllocation/impl/res/values/strings.xml
new file mode 100644
index 0000000..ef7c162
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/values/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+  <!-- Description for location map shown during emergency calls. [CHAR LIMIT=NONE] -->
+  <string name="location_map_description">Emergency Location Map</string>
+
+  <!-- Label for the address and map shown during emergency calls. [CHAR LIMIT=20] -->
+  <string name="location_title">You are here</string>
+
+  <string name="lat_long_format"><xliff:g id="latitude">%f</xliff:g>, <xliff:g id="longitude">%f</xliff:g></string>
+
+  <!-- Progress indicator loading text. [CHAR LIMIT=20] -->
+  <string name="location_loading">Finding your location</string>
+
+</resources>
diff --git a/java/com/android/incallui/calllocation/impl/res/values/styles.xml b/java/com/android/incallui/calllocation/impl/res/values/styles.xml
new file mode 100644
index 0000000..866a4ed
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/values/styles.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2015 Google Inc. All Rights Reserved. -->
+<resources>
+
+  <style name="LocationAddressTitleTextStyle">
+    <item name="android:textSize">14sp</item>
+    <item name="android:textColor">#dd000000</item>
+    <item name="android:fontFamily">sans-serif-medium</item>
+  </style>
+
+  <style name="LocationAddressTextStyle">
+    <item name="android:textSize">16sp</item>
+    <item name="android:textColor">#dd000000</item>
+    <item name="android:fontFamily">sans-serif</item>
+  </style>
+
+  <style name="LocationLatLongTextStyle">
+    <item name="android:textSize">14sp</item>
+    <item name="android:textColor">#88000000</item>
+    <item name="android:fontFamily">sans-serif</item>
+  </style>
+
+  <style name="LocationLoadingTextStyle">
+    <item name="android:textSize">14sp</item>
+    <item name="android:textColor">#dd000000</item>
+    <item name="android:fontFamily">sans-serif</item>
+  </style>
+</resources>
diff --git a/java/com/android/incallui/calllocation/stub/StubCallLocationModule.java b/java/com/android/incallui/calllocation/stub/StubCallLocationModule.java
new file mode 100644
index 0000000..fc198c7
--- /dev/null
+++ b/java/com/android/incallui/calllocation/stub/StubCallLocationModule.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.calllocation.stub;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.dialer.common.Assert;
+import com.android.incallui.calllocation.CallLocation;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Inject;
+
+/** This module provides an instance of call location. */
+@Module
+public abstract class StubCallLocationModule {
+
+  @Binds
+  public abstract CallLocation bindCallLocation(StubCallLocation callLocation);
+
+  static public class StubCallLocation implements CallLocation {
+    @Inject
+    public StubCallLocation() {}
+
+    @Override
+    public boolean canGetLocation(@NonNull Context context) {
+      return false;
+    }
+
+    @Override
+    @NonNull
+    public Fragment getLocationFragment(@NonNull Context context) {
+      return null;
+    }
+
+    @Override
+    public void close() {
+    }
+  }
+}
diff --git a/java/com/android/incallui/commontheme/res/anim/blinking.xml b/java/com/android/incallui/commontheme/res/anim/blinking.xml
new file mode 100644
index 0000000..aaec18c
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/anim/blinking.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+  <alpha
+    android:duration="@android:integer/config_mediumAnimTime"
+    android:fromAlpha="0.0"
+    android:interpolator="@android:anim/linear_interpolator"
+    android:repeatCount="infinite"
+    android:repeatMode="reverse"
+    android:toAlpha="1.0"/>
+</set>
\ No newline at end of file
diff --git a/java/com/android/incallui/contactgrid/BottomRow.java b/java/com/android/incallui/contactgrid/BottomRow.java
index aaf7e82..6ddce45 100644
--- a/java/com/android/incallui/contactgrid/BottomRow.java
+++ b/java/com/android/incallui/contactgrid/BottomRow.java
@@ -31,10 +31,10 @@
  * Gets the content of the bottom row. For example:
  *
  * <ul>
- * <li>Mobile +1 (650) 253-0000
- * <li>[HD icon] 00:15
- * <li>Call ended
- * <li>Hanging up
+ *   <li>Mobile +1 (650) 253-0000
+ *   <li>[HD attempting icon]/[HD icon] 00:15
+ *   <li>Call ended
+ *   <li>Hanging up
  * </ul>
  */
 public class BottomRow {
@@ -45,6 +45,7 @@
     @Nullable public final CharSequence label;
     public final boolean isTimerVisible;
     public final boolean isWorkIconVisible;
+    public final boolean isHdAttemptinIconVisible;
     public final boolean isHdIconVisible;
     public final boolean isForwardIconVisible;
     public final boolean isSpamIconVisible;
@@ -54,6 +55,7 @@
         @Nullable CharSequence label,
         boolean isTimerVisible,
         boolean isWorkIconVisible,
+        boolean isHdAttemptinIconVisible,
         boolean isHdIconVisible,
         boolean isForwardIconVisible,
         boolean isSpamIconVisible,
@@ -61,6 +63,7 @@
       this.label = label;
       this.isTimerVisible = isTimerVisible;
       this.isWorkIconVisible = isWorkIconVisible;
+      this.isHdAttemptinIconVisible = isHdAttemptinIconVisible;
       this.isHdIconVisible = isHdIconVisible;
       this.isForwardIconVisible = isForwardIconVisible;
       this.isSpamIconVisible = isSpamIconVisible;
@@ -76,6 +79,7 @@
     boolean isForwardIconVisible = state.isForwardedNumber;
     boolean isWorkIconVisible = state.isWorkCall;
     boolean isHdIconVisible = state.isHdAudioCall && !isForwardIconVisible;
+    boolean isHdAttemptingIconVisible = state.isHdAttempting;
     boolean isSpamIconVisible = false;
     boolean shouldPopulateAccessibilityEvent = true;
 
@@ -110,6 +114,7 @@
         label,
         isTimerVisible,
         isWorkIconVisible,
+        isHdAttemptingIconVisible,
         isHdIconVisible,
         isForwardIconVisible,
         isSpamIconVisible,
diff --git a/java/com/android/incallui/contactgrid/ContactGridManager.java b/java/com/android/incallui/contactgrid/ContactGridManager.java
index 81c2251..a0b687c 100644
--- a/java/com/android/incallui/contactgrid/ContactGridManager.java
+++ b/java/com/android/incallui/contactgrid/ContactGridManager.java
@@ -18,10 +18,14 @@
 
 import android.content.Context;
 import android.os.SystemClock;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.telecom.TelecomManager;
 import android.text.TextUtils;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
 import android.widget.Chronometer;
 import android.widget.ImageView;
 import android.widget.TextView;
@@ -56,7 +60,7 @@
   @Nullable private ImageView avatarImageView;
 
   // Row 2: Mobile +1 (650) 253-0000
-  // Row 2: [HD icon] 00:15
+  // Row 2: [HD attempting icon]/[HD icon] 00:15
   // Row 2: Call ended
   // Row 2: Hanging up
   // Row 2: [Alert sign] Suspected spam caller
@@ -77,7 +81,6 @@
   private PrimaryCallState primaryCallState = PrimaryCallState.createEmptyPrimaryCallState();
   private final LetterTileDrawable letterTile;
 
-
   public ContactGridManager(
       View view, @Nullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar) {
     context = view.getContext();
@@ -227,6 +230,24 @@
   }
 
   /**
+   * Returns the appropriate LetterTileDrawable.TYPE_ based on a given call state.
+   *
+   * <p>If no special state is detected, yields TYPE_DEFAULT.
+   */
+  private static @LetterTileDrawable.ContactType int getContactTypeForPrimaryCallState(
+      @NonNull PrimaryCallState callState, @NonNull PrimaryInfo primaryInfo) {
+    if (callState.isVoiceMailNumber) {
+      return LetterTileDrawable.TYPE_VOICEMAIL;
+    } else if (callState.isBusinessNumber) {
+      return LetterTileDrawable.TYPE_BUSINESS;
+    } else if (primaryInfo.numberPresentation == TelecomManager.PRESENTATION_RESTRICTED) {
+      return LetterTileDrawable.TYPE_GENERIC_AVATAR;
+    } else {
+      return LetterTileDrawable.TYPE_DEFAULT;
+    }
+  }
+
+  /**
    * Updates row 1. For example:
    *
    * <ul>
@@ -255,7 +276,7 @@
     if (avatarImageView != null) {
       if (hideAvatar) {
         avatarImageView.setVisibility(View.GONE);
-      } else if (avatarImageView != null && avatarSize > 0 && updateAvatarVisibility()) {
+      } else if (avatarSize > 0 && updateAvatarVisibility()) {
         boolean hasPhoto =
             primaryInfo.photo != null && primaryInfo.photoType == ContactPhotoType.CONTACT;
         // Contact has a photo, don't render a letter tile.
@@ -265,27 +286,29 @@
                   context, primaryInfo.photo, avatarSize, avatarSize));
           // Contact has a name, that isn't a number.
         } else {
-          int contactType =
-              primaryCallState.isVoiceMailNumber
-                  ? LetterTileDrawable.TYPE_VOICEMAIL
-                  : LetterTileDrawable.TYPE_DEFAULT;
           letterTile.setCanonicalDialerLetterTileDetails(
               primaryInfo.name,
               primaryInfo.contactInfoLookupKey,
               LetterTileDrawable.SHAPE_CIRCLE,
-              contactType);
+              getContactTypeForPrimaryCallState(primaryCallState, primaryInfo));
+
+          // By invalidating the avatarImageView we force a redraw of the letter tile.
+          // This is required to properly display the updated letter tile iconography based on the
+          // contact type, because the background drawable reference cached in the view, and the
+          // view is not aware of the mutations made to the background.
+          avatarImageView.invalidate();
           avatarImageView.setBackground(letterTile);
+        }
       }
     }
   }
-  }
 
   /**
    * Updates row 2. For example:
    *
    * <ul>
    *   <li>Mobile +1 (650) 253-0000
-   *   <li>[HD icon] 00:15
+   *   <li>[HD attempting icon]/[HD icon] 00:15
    *   <li>Call ended
    *   <li>Hanging up
    * </ul>
@@ -296,7 +319,15 @@
     bottomTextView.setText(info.label);
     bottomTextView.setAllCaps(info.isSpamIconVisible);
     workIconImageView.setVisibility(info.isWorkIconVisible ? View.VISIBLE : View.GONE);
-    hdIconImageView.setVisibility(info.isHdIconVisible ? View.VISIBLE : View.GONE);
+    boolean wasHdIconVisible = hdIconImageView.getVisibility() == View.VISIBLE;
+    if (!wasHdIconVisible && info.isHdAttemptinIconVisible) {
+      Animation animation = AnimationUtils.loadAnimation(context, R.anim.blinking);
+      hdIconImageView.startAnimation(animation);
+    } else if (wasHdIconVisible && !info.isHdAttemptinIconVisible) {
+      hdIconImageView.clearAnimation();
+    }
+    hdIconImageView.setVisibility(
+        info.isHdIconVisible || info.isHdAttemptinIconVisible ? View.VISIBLE : View.GONE);
     forwardIconImageView.setVisibility(info.isForwardIconVisible ? View.VISIBLE : View.GONE);
     spamIconImageView.setVisibility(info.isSpamIconVisible ? View.VISIBLE : View.GONE);
 
diff --git a/java/com/android/incallui/contactgrid/TopRow.java b/java/com/android/incallui/contactgrid/TopRow.java
index a340fd0..ecd5eea 100644
--- a/java/com/android/incallui/contactgrid/TopRow.java
+++ b/java/com/android/incallui/contactgrid/TopRow.java
@@ -21,10 +21,10 @@
 import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import com.android.dialer.common.Assert;
-import com.android.incallui.call.DialerCall;
 import com.android.incallui.call.DialerCall.State;
 import com.android.incallui.call.VideoUtils;
 import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.videotech.VideoTech;
 
 /**
  * Gets the content of the top row. For example:
@@ -95,7 +95,7 @@
   }
 
   private static CharSequence getLabelForIncoming(Context context, PrimaryCallState state) {
-    if (VideoUtils.isVideoCall(state.videoState)) {
+    if (state.isVideoCall) {
       return getLabelForIncomingVideo(context, state.isWifi);
     } else if (state.isWifi && !TextUtils.isEmpty(state.connectionLabel)) {
       return state.connectionLabel;
@@ -120,7 +120,7 @@
     if (!TextUtils.isEmpty(state.connectionLabel) && !state.isWifi) {
       return context.getString(R.string.incall_calling_via_template, state.connectionLabel);
     } else {
-      if (VideoUtils.isVideoCall(state.videoState)) {
+      if (state.isVideoCall) {
         if (state.isWifi) {
           return context.getString(R.string.incall_wifi_video_call_requesting);
         } else {
@@ -144,18 +144,18 @@
 
   private static CharSequence getLabelForVideoRequest(Context context, PrimaryCallState state) {
     switch (state.sessionModificationState) {
-      case DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE:
+      case VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE:
         return context.getString(R.string.incall_video_call_requesting);
-      case DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED:
-      case DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED:
+      case VideoTech.SESSION_MODIFICATION_STATE_REQUEST_FAILED:
+      case VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED:
         return context.getString(R.string.incall_video_call_request_failed);
-      case DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED:
+      case VideoTech.SESSION_MODIFICATION_STATE_REQUEST_REJECTED:
         return context.getString(R.string.incall_video_call_request_rejected);
-      case DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT:
+      case VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT:
         return context.getString(R.string.incall_video_call_request_timed_out);
-      case DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST:
+      case VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST:
         return getLabelForIncomingVideo(context, state.isWifi);
-      case DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST:
+      case VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST:
       default:
         Assert.fail();
         return null;
diff --git a/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
index 3900be5..b7a3fe7 100644
--- a/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
+++ b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
@@ -4,8 +4,8 @@
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
-  android:orientation="horizontal"
   android:gravity="center_horizontal"
+  android:orientation="horizontal"
   tools:showIn="@layout/incall_contact_grid">
   <ImageView
     android:id="@id/contactgrid_workIcon"
diff --git a/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml b/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml
index c213af5..6128ae5 100644
--- a/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml
+++ b/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml
@@ -39,7 +39,6 @@
       style="@style/Dialer.Incall.TextAppearance"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
-      android:textAllCaps="true"
       android:textColor="@android:color/white"
       android:text="@string/incall_on_hold"/>
   </LinearLayout>
diff --git a/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java b/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java
deleted file mode 100644
index addebc4..0000000
--- a/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.incallui.incall.impl;
-
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_MappedButtonConfig_MappingInfo extends MappedButtonConfig.MappingInfo {
-
-  private final int slot;
-  private final int slotOrder;
-  private final int conflictOrder;
-
-  private AutoValue_MappedButtonConfig_MappingInfo(
-      int slot,
-      int slotOrder,
-      int conflictOrder) {
-    this.slot = slot;
-    this.slotOrder = slotOrder;
-    this.conflictOrder = conflictOrder;
-  }
-
-  @Override
-  public int getSlot() {
-    return slot;
-  }
-
-  @Override
-  public int getSlotOrder() {
-    return slotOrder;
-  }
-
-  @Override
-  public int getConflictOrder() {
-    return conflictOrder;
-  }
-
-  @Override
-  public String toString() {
-    return "MappingInfo{"
-        + "slot=" + slot + ", "
-        + "slotOrder=" + slotOrder + ", "
-        + "conflictOrder=" + conflictOrder
-        + "}";
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o == this) {
-      return true;
-    }
-    if (o instanceof MappedButtonConfig.MappingInfo) {
-      MappedButtonConfig.MappingInfo that = (MappedButtonConfig.MappingInfo) o;
-      return (this.slot == that.getSlot())
-           && (this.slotOrder == that.getSlotOrder())
-           && (this.conflictOrder == that.getConflictOrder());
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    int h = 1;
-    h *= 1000003;
-    h ^= this.slot;
-    h *= 1000003;
-    h ^= this.slotOrder;
-    h *= 1000003;
-    h ^= this.conflictOrder;
-    return h;
-  }
-
-  static final class Builder extends MappedButtonConfig.MappingInfo.Builder {
-    private Integer slot;
-    private Integer slotOrder;
-    private Integer conflictOrder;
-    Builder() {
-    }
-    private Builder(MappedButtonConfig.MappingInfo source) {
-      this.slot = source.getSlot();
-      this.slotOrder = source.getSlotOrder();
-      this.conflictOrder = source.getConflictOrder();
-    }
-    @Override
-    public MappedButtonConfig.MappingInfo.Builder setSlot(int slot) {
-      this.slot = slot;
-      return this;
-    }
-    @Override
-    public MappedButtonConfig.MappingInfo.Builder setSlotOrder(int slotOrder) {
-      this.slotOrder = slotOrder;
-      return this;
-    }
-    @Override
-    public MappedButtonConfig.MappingInfo.Builder setConflictOrder(int conflictOrder) {
-      this.conflictOrder = conflictOrder;
-      return this;
-    }
-    @Override
-    public MappedButtonConfig.MappingInfo build() {
-      String missing = "";
-      if (this.slot == null) {
-        missing += " slot";
-      }
-      if (this.slotOrder == null) {
-        missing += " slotOrder";
-      }
-      if (this.conflictOrder == null) {
-        missing += " conflictOrder";
-      }
-      if (!missing.isEmpty()) {
-        throw new IllegalStateException("Missing required properties:" + missing);
-      }
-      return new AutoValue_MappedButtonConfig_MappingInfo(
-          this.slot,
-          this.slotOrder,
-          this.conflictOrder);
-    }
-  }
-
-}
diff --git a/java/com/android/incallui/incall/impl/FakeDragAnimation.java b/java/com/android/incallui/incall/impl/FakeDragAnimation.java
new file mode 100644
index 0000000..c84c3c4
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/FakeDragAnimation.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.incall.impl;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+
+/**
+ * An animation that controls the fake drag of a {@link ViewPager}. See {@link
+ * ViewPager#fakeDragBy(float)} for more details.
+ */
+public class FakeDragAnimation implements AnimatorUpdateListener {
+
+  /** The view to animate. */
+  private final ViewPager pager;
+
+  private final ValueAnimator animator;
+  private int oldDragPosition;
+
+  public FakeDragAnimation(ViewPager pager) {
+    this.pager = pager;
+    animator = ValueAnimator.ofInt(0, pager.getWidth());
+    animator.addUpdateListener(this);
+    animator.setInterpolator(new FastOutSlowInInterpolator());
+    animator.setDuration(600);
+  }
+
+  public void start() {
+    animator.start();
+  }
+
+  @Override
+  public void onAnimationUpdate(ValueAnimator animation) {
+    if (!pager.isFakeDragging()) {
+      pager.beginFakeDrag();
+    }
+    int dragPosition = (Integer) animation.getAnimatedValue();
+    int dragOffset = dragPosition - oldDragPosition;
+    oldDragPosition = dragPosition;
+    pager.fakeDragBy(-dragOffset);
+
+    if (animation.getAnimatedFraction() == 1) {
+      pager.endFakeDrag();
+    }
+  }
+}
diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java
index ef8a1ed..3f31651 100644
--- a/java/com/android/incallui/incall/impl/InCallFragment.java
+++ b/java/com/android/incallui/incall/impl/InCallFragment.java
@@ -213,9 +213,7 @@
   @Override
   public void setPrimary(@NonNull PrimaryInfo primaryInfo) {
     LogUtil.i("InCallFragment.setPrimary", primaryInfo.toString());
-    if (adapter == null) {
-      initAdapter(primaryInfo.multimediaData);
-    }
+    setAdapterMedia(primaryInfo.multimediaData);
     contactGridManager.setPrimary(primaryInfo);
 
     if (primaryInfo.shouldShowLocation) {
@@ -241,9 +239,13 @@
     }
   }
 
-  private void initAdapter(MultimediaData multimediaData) {
-    adapter = new InCallPagerAdapter(getChildFragmentManager(), multimediaData);
-    pager.setAdapter(adapter);
+  private void setAdapterMedia(MultimediaData multimediaData) {
+    if (adapter == null) {
+      adapter = new InCallPagerAdapter(getChildFragmentManager(), multimediaData);
+      pager.setAdapter(adapter);
+    } else {
+      adapter.setAttachments(multimediaData);
+    }
 
     if (adapter.getCount() > 1) {
       tabLayout.setVisibility(pager.getVisibility());
@@ -251,16 +253,13 @@
       if (!stateRestored) {
         new Handler()
             .postDelayed(
-                new Runnable() {
-                  @Override
-                  public void run() {
-                    // In order to prevent user confusion and educate the user on our UI, we animate
-                    // the view pager to the button grid after 2 seconds show them when the UI is
-                    // that they are more familiar with.
-                    pager.setCurrentItem(adapter.getButtonGridPosition());
-                  }
+                () -> {
+                  // In order to prevent user confusion and educate the user on our UI, we animate
+                  // the view pager to the button grid after a short period to show them where the
+                  // UI that they are more familiar with is located.
+                  new FakeDragAnimation(pager).start();
                 },
-                2000);
+                333);
       }
     } else {
       tabLayout.setVisibility(View.GONE);
@@ -479,23 +478,39 @@
 
   @Override
   public boolean isShowingLocationUi() {
-    Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
+    Fragment fragment = getLocationFragment();
     return fragment != null && fragment.isVisible();
   }
 
   @Override
   public void showLocationUi(@Nullable Fragment locationUi) {
-    boolean isShowing = isShowingLocationUi();
-    if (!isShowing && locationUi != null) {
+    boolean isVisible = isShowingLocationUi();
+    if (locationUi != null && !isVisible) {
       // Show the location fragment.
       getChildFragmentManager()
           .beginTransaction()
           .replace(R.id.incall_location_holder, locationUi)
           .commitAllowingStateLoss();
-    } else if (isShowing && locationUi == null) {
+    } else if (locationUi == null && isVisible) {
       // Hide the location fragment
-      Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
-      getChildFragmentManager().beginTransaction().remove(fragment).commitAllowingStateLoss();
+      getChildFragmentManager()
+          .beginTransaction()
+          .remove(getLocationFragment())
+          .commitAllowingStateLoss();
     }
   }
+
+  @Override
+  public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
+    super.onMultiWindowModeChanged(isInMultiWindowMode);
+    if (isInMultiWindowMode == isShowingLocationUi()) {
+      LogUtil.i("InCallFragment.onMultiWindowModeChanged", "hide = " + isInMultiWindowMode);
+      // Need to show or hide location
+      showLocationUi(isInMultiWindowMode ? null : getLocationFragment());
+    }
+  }
+
+  private Fragment getLocationFragment() {
+    return getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
+  }
 }
diff --git a/java/com/android/incallui/incall/impl/InCallPagerAdapter.java b/java/com/android/incallui/incall/impl/InCallPagerAdapter.java
index 50eb4c8..2e21835 100644
--- a/java/com/android/incallui/incall/impl/InCallPagerAdapter.java
+++ b/java/com/android/incallui/incall/impl/InCallPagerAdapter.java
@@ -19,17 +19,18 @@
 import android.support.annotation.Nullable;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.app.FragmentStatePagerAdapter;
+import android.support.v4.view.PagerAdapter;
 import android.text.TextUtils;
 import com.android.dialer.multimedia.MultimediaData;
 import com.android.incallui.sessiondata.MultimediaFragment;
 
 /** View pager adapter for in call ui. */
-public class InCallPagerAdapter extends FragmentPagerAdapter {
+public class InCallPagerAdapter extends FragmentStatePagerAdapter {
 
-  @Nullable private final MultimediaData attachments;
+  @Nullable private MultimediaData attachments;
 
-  public InCallPagerAdapter(FragmentManager fragmentManager, MultimediaData attachments) {
+  public InCallPagerAdapter(FragmentManager fragmentManager, @Nullable MultimediaData attachments) {
     super(fragmentManager);
     this.attachments = attachments;
   }
@@ -47,13 +48,27 @@
   @Override
   public int getCount() {
     if (attachments != null
-        && (!TextUtils.isEmpty(attachments.getSubject()) || attachments.hasImageData())) {
+        && (!TextUtils.isEmpty(attachments.getText()) || attachments.hasImageData())) {
       return 2;
     }
     return 1;
   }
 
+  public void setAttachments(@Nullable MultimediaData attachments) {
+    if (this.attachments != attachments) {
+      this.attachments = attachments;
+      notifyDataSetChanged();
+    }
+  }
+
   public int getButtonGridPosition() {
     return getCount() - 1;
   }
+
+  //this is called when notifyDataSetChanged() is called
+  @Override
+  public int getItemPosition(Object object) {
+    // refresh all fragments when data set changed
+    return PagerAdapter.POSITION_NONE;
+  }
 }
diff --git a/java/com/android/incallui/incall/impl/MappedButtonConfig.java b/java/com/android/incallui/incall/impl/MappedButtonConfig.java
index ecdb5df..7229837 100644
--- a/java/com/android/incallui/incall/impl/MappedButtonConfig.java
+++ b/java/com/android/incallui/incall/impl/MappedButtonConfig.java
@@ -22,7 +22,7 @@
 import com.android.dialer.common.Assert;
 import com.android.incallui.incall.protocol.InCallButtonIds;
 import com.android.incallui.incall.protocol.InCallButtonIdsExtension;
-
+import com.google.auto.value.AutoValue;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -151,7 +151,7 @@
   }
 
   /** Holds information about button mapping. */
-
+  @AutoValue
   abstract static class MappingInfo {
 
     /** The Ui slot into which a given button desires to be placed. */
@@ -179,7 +179,7 @@
     }
 
     /** Class used to build instances of {@link MappingInfo}. */
-
+    @AutoValue.Builder
     abstract static class Builder {
       public abstract Builder setSlot(int slot);
 
diff --git a/java/com/android/incallui/incall/protocol/PrimaryCallState.java b/java/com/android/incallui/incall/protocol/PrimaryCallState.java
index 7820908..6e1680b 100644
--- a/java/com/android/incallui/incall/protocol/PrimaryCallState.java
+++ b/java/com/android/incallui/incall/protocol/PrimaryCallState.java
@@ -18,15 +18,15 @@
 
 import android.graphics.drawable.Drawable;
 import android.telecom.DisconnectCause;
-import android.telecom.VideoProfile;
 import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.SessionModificationState;
 import java.util.Locale;
 
 /** State of the primary call. */
 public class PrimaryCallState {
   public final int state;
-  public final int videoState;
+  public final boolean isVideoCall;
   @SessionModificationState public final int sessionModificationState;
   public final DisconnectCause disconnectCause;
   public final String connectionLabel;
@@ -37,19 +37,21 @@
   public final boolean isWifi;
   public final boolean isConference;
   public final boolean isWorkCall;
+  public final boolean isHdAttempting;
   public final boolean isHdAudioCall;
   public final boolean isForwardedNumber;
   public final boolean shouldShowContactPhoto;
   public final long connectTimeMillis;
   public final boolean isVoiceMailNumber;
   public final boolean isRemotelyHeld;
+  public final boolean isBusinessNumber;
 
   // TODO: Convert to autovalue. b/34502119
   public static PrimaryCallState createEmptyPrimaryCallState() {
     return new PrimaryCallState(
         DialerCall.State.IDLE,
-        VideoProfile.STATE_AUDIO_ONLY,
-        DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST,
+        false, /* isVideoCall */
+        VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST,
         new DisconnectCause(DisconnectCause.UNKNOWN),
         null, /* connectionLabel */
         null, /* connectionIcon */
@@ -59,17 +61,19 @@
         false /* isWifi */,
         false /* isConference */,
         false /* isWorkCall */,
+        false /* isHdAttempting */,
         false /* isHdAudioCall */,
         false /* isForwardedNumber */,
         false /* shouldShowContactPhoto */,
         0,
         false /* isVoiceMailNumber */,
-        false /* isRemotelyHeld */);
+        false /* isRemotelyHeld */,
+        false /* isBusinessNumber */);
   }
 
   public PrimaryCallState(
       int state,
-      int videoState,
+      boolean isVideoCall,
       @SessionModificationState int sessionModificationState,
       DisconnectCause disconnectCause,
       String connectionLabel,
@@ -80,14 +84,16 @@
       boolean isWifi,
       boolean isConference,
       boolean isWorkCall,
+      boolean isHdAttempting,
       boolean isHdAudioCall,
       boolean isForwardedNumber,
       boolean shouldShowContactPhoto,
       long connectTimeMillis,
       boolean isVoiceMailNumber,
-      boolean isRemotelyHeld) {
+      boolean isRemotelyHeld,
+      boolean isBusinessNumber) {
     this.state = state;
-    this.videoState = videoState;
+    this.isVideoCall = isVideoCall;
     this.sessionModificationState = sessionModificationState;
     this.disconnectCause = disconnectCause;
     this.connectionLabel = connectionLabel;
@@ -98,12 +104,14 @@
     this.isWifi = isWifi;
     this.isConference = isConference;
     this.isWorkCall = isWorkCall;
+    this.isHdAttempting = isHdAttempting;
     this.isHdAudioCall = isHdAudioCall;
     this.isForwardedNumber = isForwardedNumber;
     this.shouldShowContactPhoto = shouldShowContactPhoto;
     this.connectTimeMillis = connectTimeMillis;
     this.isVoiceMailNumber = isVoiceMailNumber;
     this.isRemotelyHeld = isRemotelyHeld;
+    this.isBusinessNumber = isBusinessNumber;
   }
 
   @Override
diff --git a/java/com/android/incallui/incall/protocol/PrimaryInfo.java b/java/com/android/incallui/incall/protocol/PrimaryInfo.java
index 1833ed2..c170950 100644
--- a/java/com/android/incallui/incall/protocol/PrimaryInfo.java
+++ b/java/com/android/incallui/incall/protocol/PrimaryInfo.java
@@ -41,6 +41,7 @@
   // Used for consistent LetterTile coloring.
   @Nullable public final String contactInfoLookupKey;
   @Nullable public final MultimediaData multimediaData;
+  public final int numberPresentation;
 
   // TODO: Convert to autovalue. b/34502119
   public static PrimaryInfo createEmptyPrimaryInfo() {
@@ -59,7 +60,8 @@
         false,
         false,
         null,
-        null);
+        null,
+        -1);
   }
 
   public PrimaryInfo(
@@ -77,7 +79,8 @@
       boolean answeringDisconnectsOngoingCall,
       boolean shouldShowLocation,
       @Nullable String contactInfoLookupKey,
-      @Nullable MultimediaData multimediaData) {
+      @Nullable MultimediaData multimediaData,
+      int numberPresentation) {
     this.number = number;
     this.name = name;
     this.nameIsNumber = nameIsNumber;
@@ -93,6 +96,7 @@
     this.shouldShowLocation = shouldShowLocation;
     this.contactInfoLookupKey = contactInfoLookupKey;
     this.multimediaData = multimediaData;
+    this.numberPresentation = numberPresentation;
   }
 
   @Override
diff --git a/java/com/android/incallui/maps/StaticMapFactory.java b/java/com/android/incallui/maps/Maps.java
similarity index 64%
copy from java/com/android/incallui/maps/StaticMapFactory.java
copy to java/com/android/incallui/maps/Maps.java
index a350138..648cf9f 100644
--- a/java/com/android/incallui/maps/StaticMapFactory.java
+++ b/java/com/android/incallui/maps/Maps.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2017 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -20,9 +20,14 @@
 import android.support.annotation.NonNull;
 import android.support.v4.app.Fragment;
 
-/** A Factory that can create Fragments for showing a static map */
-public interface StaticMapFactory {
+/** Used to create a fragment that can display a static map at the given location. */
+public interface Maps {
+  /**
+   * Used to check if maps is available. This will return false if Dialer was compiled without
+   * support for Google Play Services.
+   */
+  boolean isAvailable();
 
   @NonNull
-  Fragment getStaticMap(@NonNull Location location);
+  Fragment createStaticMapFragment(@NonNull Location location);
 }
diff --git a/java/com/android/incallui/maps/MapsComponent.java b/java/com/android/incallui/maps/MapsComponent.java
new file mode 100644
index 0000000..1ca17b7
--- /dev/null
+++ b/java/com/android/incallui/maps/MapsComponent.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps;
+
+import android.content.Context;
+import com.android.dialer.inject.HasRootComponent;
+import dagger.Subcomponent;
+import com.android.incallui.maps.stub.StubMapsModule;
+
+/** Subcomponent that can be used to access the maps implementation. */
+public class MapsComponent {
+
+  private static MapsComponent instance;
+  private Maps maps;
+
+  public Maps getMaps() {
+    if (maps == null) {
+        maps = new StubMapsModule.StubMaps();
+    }
+    return maps;
+  }
+
+  public static MapsComponent get(Context context) {
+    if (instance == null) {
+        instance = new MapsComponent();
+    }
+    return instance;
+  }
+
+
+  /** Used to refer to the root application component. */
+  public interface HasComponent {
+    MapsComponent mapsComponent();
+  }
+}
diff --git a/java/com/android/incallui/maps/StaticMapBinding.java b/java/com/android/incallui/maps/StaticMapBinding.java
deleted file mode 100644
index 9d24ef2..0000000
--- a/java/com/android/incallui/maps/StaticMapBinding.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.incallui.maps;
-
-import android.app.Application;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-
-/** Utility for getting a {@link StaticMapFactory} */
-public class StaticMapBinding {
-
-  @Nullable
-  public static StaticMapFactory get(@NonNull Application application) {
-    if (useTestingInstance) {
-      return testingInstance;
-    }
-    if (application instanceof StaticMapFactory) {
-      return ((StaticMapFactory) application);
-    }
-    return null;
-  }
-
-  private static StaticMapFactory testingInstance;
-  private static boolean useTestingInstance;
-
-  @VisibleForTesting
-  public static void setForTesting(@Nullable StaticMapFactory staticMapFactory) {
-    testingInstance = staticMapFactory;
-    useTestingInstance = true;
-  }
-
-  @VisibleForTesting
-  public static void clearForTesting() {
-    useTestingInstance = false;
-  }
-}
diff --git a/java/com/android/incallui/maps/impl/AndroidManifest.xml b/java/com/android/incallui/maps/impl/AndroidManifest.xml
new file mode 100644
index 0000000..4ad0b3b
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2016 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<manifest
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.android.incallui.maps.impl">
+
+  <application>
+    <meta-data
+      android:name="com.google.android.gms.version"
+      android:value="@integer/google_play_services_version"/>
+  </application>
+</manifest>
diff --git a/java/com/android/incallui/maps/impl/MapsImpl.java b/java/com/android/incallui/maps/impl/MapsImpl.java
new file mode 100644
index 0000000..2cecee9
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/MapsImpl.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps.impl;
+
+import android.location.Location;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.incallui.maps.Maps;
+import javax.inject.Inject;
+
+/** Uses Google Play Services APIs to create a static map fragment. */
+final class MapsImpl implements Maps {
+  @Inject
+  public MapsImpl() {}
+
+  @Override
+  public boolean isAvailable() {
+    return true;
+  }
+
+  @Override
+  @NonNull
+  public Fragment createStaticMapFragment(@NonNull Location location) {
+    return StaticMapFragment.newInstance(location);
+  }
+}
diff --git a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java b/java/com/android/incallui/maps/impl/MapsModule.java
similarity index 70%
copy from java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
copy to java/com/android/incallui/maps/impl/MapsModule.java
index 39c55d0..22f2f32 100644
--- a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
+++ b/java/com/android/incallui/maps/impl/MapsModule.java
@@ -14,19 +14,18 @@
  * limitations under the License
  */
 
-package com.android.dialer.enrichedcall;
+package com.android.incallui.maps.impl;
 
+import com.android.incallui.maps.Maps;
+import dagger.Binds;
 import dagger.Module;
-import dagger.Provides;
 import javax.inject.Singleton;
 
-/** Module which binds {@link EnrichedCallManagerStub}. */
+/** This module provides an instance of maps. */
 @Module
-public class StubEnrichedCallModule {
+public abstract class MapsModule {
 
-  @Provides
+  @Binds
   @Singleton
-  static EnrichedCallManager provideEnrichedCallManager() {
-    return new EnrichedCallManagerStub();
-  }
+  public abstract Maps bindMaps(MapsImpl maps);
 }
diff --git a/java/com/android/incallui/maps/impl/StaticMapFragment.java b/java/com/android/incallui/maps/impl/StaticMapFragment.java
new file mode 100644
index 0000000..38a4c15
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/StaticMapFragment.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps.impl;
+
+import android.location.Location;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.google.android.gms.maps.CameraUpdateFactory;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.OnMapReadyCallback;
+import com.google.android.gms.maps.SupportMapFragment;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.MarkerOptions;
+
+/** Shows a static map centered on a specified location */
+public class StaticMapFragment extends Fragment implements OnMapReadyCallback {
+
+  private static final String ARG_LOCATION = "location";
+
+  public static StaticMapFragment newInstance(@NonNull Location location) {
+    Bundle args = new Bundle();
+    args.putParcelable(ARG_LOCATION, Assert.isNotNull(location));
+    StaticMapFragment fragment = new StaticMapFragment();
+    fragment.setArguments(args);
+    return fragment;
+  }
+
+  @Nullable
+  @Override
+  public View onCreateView(
+      LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+    return layoutInflater.inflate(R.layout.static_map_fragment, viewGroup, false);
+  }
+
+  @Override
+  public void onViewCreated(View view, @Nullable Bundle bundle) {
+    super.onViewCreated(view, bundle);
+    SupportMapFragment mapFragment =
+        (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.static_map);
+    if (mapFragment != null) {
+      mapFragment.getMapAsync(this);
+    } else {
+      LogUtil.w("StaticMapFragment.onViewCreated", "No map fragment found!");
+    }
+  }
+
+  @Override
+  public void onMapReady(GoogleMap googleMap) {
+    Location location = getArguments().getParcelable(ARG_LOCATION);
+    LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
+    googleMap.addMarker(new MarkerOptions().position(latLng).flat(true).draggable(false));
+    googleMap.getUiSettings().setMapToolbarEnabled(false);
+    googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15f));
+  }
+}
diff --git a/java/com/android/incallui/maps/impl/res/layout/static_map_fragment.xml b/java/com/android/incallui/maps/impl/res/layout/static_map_fragment.xml
new file mode 100644
index 0000000..54f41cb
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/res/layout/static_map_fragment.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2016 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:map="http://schemas.android.com/apk/res-auto"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent">
+  <fragment
+    android:id="@+id/static_map"
+    class="com.google.android.gms.maps.SupportMapFragment"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    map:liteMode="true"
+    map:mapType="normal"/>
+</FrameLayout>
diff --git a/java/com/android/incallui/maps/stub/StubMapsModule.java b/java/com/android/incallui/maps/stub/StubMapsModule.java
new file mode 100644
index 0000000..7267814
--- /dev/null
+++ b/java/com/android/incallui/maps/stub/StubMapsModule.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps.stub;
+
+import android.location.Location;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.dialer.common.Assert;
+import com.android.incallui.maps.Maps;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/** Stub for the maps module for build variants that don't support Google Play Services. */
+@Module
+public abstract class StubMapsModule {
+
+  @Binds
+  @Singleton
+  public abstract Maps bindMaps(StubMaps maps);
+
+  static public final class StubMaps implements Maps {
+    @Inject
+    public StubMaps() {}
+
+    @Override
+    public boolean isAvailable() {
+      return false;
+    }
+
+    @NonNull
+    @Override
+    public Fragment createStaticMapFragment(@NonNull Location location) {
+      throw Assert.createUnsupportedOperationFailException();
+    }
+  }
+}
diff --git a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java b/java/com/android/incallui/maps/testing/TestMapsModule.java
similarity index 61%
copy from java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
copy to java/com/android/incallui/maps/testing/TestMapsModule.java
index 39c55d0..bb09681 100644
--- a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
+++ b/java/com/android/incallui/maps/testing/TestMapsModule.java
@@ -14,19 +14,27 @@
  * limitations under the License
  */
 
-package com.android.dialer.enrichedcall;
+package com.android.incallui.maps.testing;
 
+import android.support.annotation.Nullable;
+import com.android.incallui.maps.Maps;
 import dagger.Module;
 import dagger.Provides;
-import javax.inject.Singleton;
 
-/** Module which binds {@link EnrichedCallManagerStub}. */
+/** This module provides a instance of maps for testing. */
 @Module
-public class StubEnrichedCallModule {
+public final class TestMapsModule {
+
+  @Nullable private static Maps maps;
+
+  public static void setMaps(@Nullable Maps maps) {
+    TestMapsModule.maps = maps;
+  }
 
   @Provides
-  @Singleton
-  static EnrichedCallManager provideEnrichedCallManager() {
-    return new EnrichedCallManagerStub();
+  static Maps getMaps() {
+    return maps;
   }
+
+  private TestMapsModule() {}
 }
diff --git a/java/com/android/incallui/res/values/strings.xml b/java/com/android/incallui/res/values/strings.xml
index 252d131..0b95a9c 100644
--- a/java/com/android/incallui/res/values/strings.xml
+++ b/java/com/android/incallui/res/values/strings.xml
@@ -223,17 +223,6 @@
     <item>ABSENTNUMBER</item>
   </string-array>
 
-  <!-- Preference for Voicemail service provider under "Voicemail" settings.
-       [CHAR LIMIT=40] -->
-  <string name="voicemail_provider">Service</string>
-
-  <!-- Preference for Voicemail setting of each provider.
-       [CHAR LIMIT=40] -->
-  <string name="voicemail_settings">Setup</string>
-
-  <!-- String to display in voicemail number summary when no voicemail num is set -->
-  <string name="voicemail_number_not_set">&lt;Not set&gt;</string>
-
   <!-- Title displayed above settings coming after voicemail in the call features screen -->
   <string name="other_settings">Other call settings</string>
 
@@ -242,26 +231,6 @@
   <!--  Use this to describe the select contact button in EditPhoneNumberPreference; currently for screen readers through accessibility. -->
   <string name="selectContact">select contact</string>
 
-  <!-- Dialog title for the vibration settings for voicemail notifications [CHAR LIMIT=40] -->
-  <string msgid="8731372580674292759" name="voicemail_notification_vibrate_when_title">Vibrate</string>
-  <!-- Dialog title for the vibration settings for voice mail notifications [CHAR LIMIT=40]-->
-  <string msgid="8995274609647451109" name="voicemail_notification_vibarte_when_dialog_title">Vibrate</string>
-
-  <!-- Voicemail ringtone title. The user clicks on this preference to select
-       which sound to play when a voicemail notification is received.
-       [CHAR LIMIT=30] -->
-  <string name="voicemail_notification_ringtone_title">Sound</string>
-
-  <!-- The default value value for voicemail notification. -->
-  <string name="voicemail_notification_vibrate_when_default" translatable="false">never</string>
-
-  <!-- Actual values used in our code for voicemail notifications. DO NOT TRANSLATE -->
-  <string-array name="voicemail_notification_vibrate_when_values" translatable="false">
-    <item>always</item>
-    <item>silent</item>
-    <item>never</item>
-  </string-array>
-
   <!-- Title for the category "ringtone", which is shown above ringtone and vibration
        related settings.
        [CHAR LIMIT=30] -->
diff --git a/java/com/android/incallui/sessiondata/MultimediaFragment.java b/java/com/android/incallui/sessiondata/MultimediaFragment.java
index d6f671d..14aa0a3 100644
--- a/java/com/android/incallui/sessiondata/MultimediaFragment.java
+++ b/java/com/android/incallui/sessiondata/MultimediaFragment.java
@@ -31,12 +31,10 @@
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.TextView;
-import com.android.dialer.common.Assert;
 import com.android.dialer.common.FragmentUtils;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.multimedia.MultimediaData;
-import com.android.incallui.maps.StaticMapBinding;
-import com.android.incallui.maps.StaticMapFactory;
+import com.android.incallui.maps.MapsComponent;
 import com.bumptech.glide.Glide;
 import com.bumptech.glide.load.DataSource;
 import com.bumptech.glide.load.engine.GlideException;
@@ -58,17 +56,13 @@
   private static final String ARG_INTERACTIVE = "interactive";
   private static final String ARG_SHOW_AVATAR = "show_avatar";
   private ImageView avatarImageView;
-  // TODO: add click listeners
-  @SuppressWarnings("unused")
-  private boolean isInteractive;
 
   private boolean showAvatar;
-  private StaticMapFactory mapFactory;
 
   public static MultimediaFragment newInstance(
       @NonNull MultimediaData multimediaData, boolean isInteractive, boolean showAvatar) {
     return newInstance(
-        multimediaData.getSubject(),
+        multimediaData.getText(),
         multimediaData.getImageUri(),
         multimediaData.getLocation(),
         isInteractive,
@@ -96,7 +90,6 @@
   @Override
   public void onCreate(@Nullable Bundle bundle) {
     super.onCreate(bundle);
-    isInteractive = getArguments().getBoolean(ARG_INTERACTIVE);
     showAvatar = getArguments().getBoolean(ARG_SHOW_AVATAR);
   }
 
@@ -107,10 +100,7 @@
     boolean hasImage = getImageUri() != null;
     boolean hasSubject = !TextUtils.isEmpty(getSubject());
     boolean hasMap = getLocation() != null;
-    if (hasMap) {
-      mapFactory = StaticMapBinding.get(getActivity().getApplication());
-    }
-    if (mapFactory != null) {
+    if (hasMap && MapsComponent.get(getContext()).getMaps().isAvailable()) {
       if (hasImage) {
         if (hasSubject) {
           return layoutInflater.inflate(
@@ -178,7 +168,7 @@
     if (fragmentHolder != null) {
       fragmentHolder.setClipToOutline(true);
       Fragment mapFragment =
-          Assert.isNotNull(mapFactory).getStaticMap(Assert.isNotNull(getLocation()));
+          MapsComponent.get(getContext()).getMaps().createStaticMapFragment(getLocation());
       getChildFragmentManager()
           .beginTransaction()
           .replace(R.id.answer_message_frag, mapFragment)
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml
index 7000f83..0882781 100644
--- a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml
@@ -46,5 +46,6 @@
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:id="@+id/loading_spinner"
-    android:layout_centerInParent="true"/>
+    android:layout_centerInParent="true"
+    android:elevation="2dp"/>
 </RelativeLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml
index 9959f4d..c816418 100644
--- a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml
@@ -42,6 +42,14 @@
     android:outlineProvider="background"
     android:scaleType="centerCrop"/>
 
+  <ProgressBar
+    android:id="@+id/loading_spinner"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_column="1"
+    android:layout_gravity="center"
+    android:elevation="2dp"/>
+
   <FrameLayout
     android:id="@id/answer_message_frag"
     android:layout_width="0dp"
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml
index 9955654..4e6fcba 100644
--- a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml
@@ -59,4 +59,12 @@
     android:elevation="@dimen/answer_data_elevation"
     android:outlineProvider="background"
     android:scaleType="centerCrop"/>
+
+  <ProgressBar
+    android:id="@+id/loading_spinner"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_column="1"
+    android:layout_gravity="center"
+    android:elevation="2dp"/>
 </GridLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml
index 387c5cf..ffbe41b 100644
--- a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml
@@ -61,6 +61,14 @@
     android:outlineProvider="background"
     android:scaleType="centerCrop"/>
 
+  <ProgressBar
+    android:id="@+id/loading_spinner"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_column="1"
+    android:layout_gravity="center"
+    android:elevation="2dp"/>
+
   <FrameLayout
     android:id="@id/answer_message_frag"
     android:layout_width="0dp"
diff --git a/java/com/android/incallui/spam/SpamCallListListener.java b/java/com/android/incallui/spam/SpamCallListListener.java
index 0897842..ed0a99e 100644
--- a/java/com/android/incallui/spam/SpamCallListListener.java
+++ b/java/com/android/incallui/spam/SpamCallListListener.java
@@ -17,6 +17,7 @@
 package com.android.incallui.spam;
 
 import android.app.Notification;
+import android.app.Notification.Builder;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.Context;
@@ -33,12 +34,13 @@
 import com.android.dialer.logging.Logger;
 import com.android.dialer.logging.nano.ContactLookupResult;
 import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
 import com.android.dialer.spam.Spam;
 import com.android.incallui.R;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
 import com.android.incallui.call.DialerCall.CallHistoryStatus;
-import com.android.incallui.call.DialerCall.SessionModificationState;
 import java.util.Random;
 
 /**
@@ -47,7 +49,7 @@
  */
 public class SpamCallListListener implements CallList.Listener {
 
-  static final int NOTIFICATION_ID = 1;
+  static final int NOTIFICATION_ID = R.id.notification_spam_call;
   private static final String TAG = "SpamCallListListener";
   private final Context context;
   private final Random random;
@@ -87,7 +89,7 @@
   public void onUpgradeToVideo(DialerCall call) {}
 
   @Override
-  public void onSessionModificationStateChange(@SessionModificationState int newState) {}
+  public void onSessionModificationStateChange(DialerCall call) {}
 
   @Override
   public void onCallListChange(CallList callList) {}
@@ -173,13 +175,16 @@
    * Creates a notification builder with properties common among the two after call notifications.
    */
   private Notification.Builder createAfterCallNotificationBuilder(DialerCall call) {
-    return new Notification.Builder(context)
-        .setContentIntent(
-            createActivityPendingIntent(call, SpamNotificationActivity.ACTION_SHOW_DIALOG))
-        .setCategory(Notification.CATEGORY_STATUS)
-        .setPriority(Notification.PRIORITY_DEFAULT)
-        .setColor(context.getColor(R.color.dialer_theme_color))
-        .setSmallIcon(R.drawable.ic_call_end_white_24dp);
+    Builder builder =
+        new Builder(context)
+            .setContentIntent(
+                createActivityPendingIntent(call, SpamNotificationActivity.ACTION_SHOW_DIALOG))
+            .setCategory(Notification.CATEGORY_STATUS)
+            .setPriority(Notification.PRIORITY_DEFAULT)
+            .setColor(context.getColor(R.color.dialer_theme_color))
+            .setSmallIcon(R.drawable.ic_call_end_white_24dp);
+    NotificationChannelManager.applyChannel(builder, context, Channel.MISC, null);
+    return builder;
   }
 
   private CharSequence getDisplayNumber(DialerCall call) {
diff --git a/java/com/android/incallui/video/bindings/VideoBindings.java b/java/com/android/incallui/video/bindings/VideoBindings.java
index 934ff07..a80a6c7 100644
--- a/java/com/android/incallui/video/bindings/VideoBindings.java
+++ b/java/com/android/incallui/video/bindings/VideoBindings.java
@@ -22,7 +22,7 @@
 /** Bindings for video module. */
 public class VideoBindings {
 
-  public static VideoCallScreen createVideoCallScreen() {
-    return new VideoCallFragment();
+  public static VideoCallScreen createVideoCallScreen(String callId) {
+    return VideoCallFragment.newInstance(callId);
   }
 }
diff --git a/java/com/android/incallui/video/impl/VideoCallFragment.java b/java/com/android/incallui/video/impl/VideoCallFragment.java
index 77a67d0..92c8b37 100644
--- a/java/com/android/incallui/video/impl/VideoCallFragment.java
+++ b/java/com/android/incallui/video/impl/VideoCallFragment.java
@@ -32,6 +32,7 @@
 import android.renderscript.ScriptIntrinsicBlur;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentTransaction;
 import android.support.v4.view.animation.FastOutLinearInInterpolator;
@@ -92,6 +93,9 @@
         AudioRouteSelectorPresenter,
         OnSystemUiVisibilityChangeListener {
 
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  static final String ARG_CALL_ID = "call_id";
+
   private static final float BLUR_PREVIEW_RADIUS = 16.0f;
   private static final float BLUR_PREVIEW_SCALE_FACTOR = 1.0f;
   private static final float BLUR_REMOTE_RADIUS = 25.0f;
@@ -156,6 +160,15 @@
         }
       };
 
+  public static VideoCallFragment newInstance(String callId) {
+    Bundle bundle = new Bundle();
+    bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId));
+
+    VideoCallFragment instance = new VideoCallFragment();
+    instance.setArguments(bundle);
+    return instance;
+  }
+
   @Override
   public void onCreate(@Nullable Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
@@ -308,6 +321,20 @@
   }
 
   @Override
+  public void onStart() {
+    super.onStart();
+    LogUtil.i("VideoCallFragment.onStart", null);
+    onVideoScreenStart();
+  }
+
+  @Override
+  public void onVideoScreenStart() {
+    inCallButtonUiDelegate.refreshMuteState();
+    videoCallScreenDelegate.onVideoCallScreenUiReady();
+    getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS);
+  }
+
+  @Override
   public void onResume() {
     super.onResume();
     LogUtil.i("VideoCallFragment.onResume", null);
@@ -315,15 +342,6 @@
   }
 
   @Override
-  public void onStart() {
-    super.onStart();
-    LogUtil.i("VideoCallFragment.onStart", null);
-    inCallButtonUiDelegate.refreshMuteState();
-    videoCallScreenDelegate.onVideoCallScreenUiReady();
-    getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS);
-  }
-
-  @Override
   public void onPause() {
     super.onPause();
     LogUtil.i("VideoCallFragment.onPause", null);
@@ -333,6 +351,11 @@
   public void onStop() {
     super.onStop();
     LogUtil.i("VideoCallFragment.onStop", null);
+    onVideoScreenStop();
+  }
+
+  @Override
+  public void onVideoScreenStop() {
     getView().removeCallbacks(cameraPermissionDialogRunnable);
     videoCallScreenDelegate.onVideoCallScreenUiUnready();
   }
@@ -721,6 +744,12 @@
   }
 
   @Override
+  @NonNull
+  public String getCallId() {
+    return Assert.isNotNull(getArguments().getString(ARG_CALL_ID));
+  }
+
+  @Override
   public void showButton(@InCallButtonIds int buttonId, boolean show) {
     LogUtil.v(
         "VideoCallFragment.showButton",
diff --git a/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml b/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
index dc663dd..f8c6fc3 100644
--- a/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
+++ b/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
@@ -31,6 +31,7 @@
     android:accessibilityTraversalBefore="@+id/videocall_speaker_button"
     android:drawablePadding="8dp"
     android:drawableTop="@drawable/quantum_ic_videocam_off_white_36"
+    android:drawableTint="@color/videocall_camera_off_tint"
     android:padding="64dp"
     android:text="@string/videocall_remote_video_off"
     android:textAppearance="@style/Dialer.Incall.TextAppearance"
@@ -43,7 +44,8 @@
     android:layout_height="match_parent"
     android:layout_alignParentBottom="true"
     android:layout_alignParentStart="true"
-    android:background="@color/videocall_overlay_background_color"/>
+    android:background="@color/videocall_overlay_background_color"
+    tools:visibility="gone"/>
 
   <TextureView
     android:id="@+id/videocall_video_preview"
@@ -71,7 +73,8 @@
     android:layout_height="match_parent"
     android:layout_alignParentBottom="true"
     android:layout_alignParentStart="true"
-    android:background="@color/videocall_overlay_background_color"/>
+    android:background="@color/videocall_overlay_background_color"
+    tools:visibility="gone"/>
 
   <ImageView
     android:id="@+id/videocall_video_preview_off_overlay"
@@ -82,7 +85,9 @@
     android:layout_alignRight="@+id/videocall_video_preview"
     android:layout_alignTop="@+id/videocall_video_preview"
     android:scaleType="center"
-    android:src="@drawable/quantum_ic_videocam_off_white_36"
+    android:src="@drawable/quantum_ic_videocam_off_white_24"
+    android:tint="@color/videocall_camera_off_tint"
+    android:tintMode="src_in"
     android:visibility="gone"
     android:importantForAccessibility="no"
     tools:visibility="visible"/>
diff --git a/java/com/android/incallui/video/impl/res/values/colors.xml b/java/com/android/incallui/video/impl/res/values/colors.xml
new file mode 100644
index 0000000..874bf94
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<resources>
+  <color name="videocall_camera_off_tint">#89ffffff</color>
+</resources>
diff --git a/java/com/android/incallui/video/protocol/VideoCallScreen.java b/java/com/android/incallui/video/protocol/VideoCallScreen.java
index 0eaf692..bad050c 100644
--- a/java/com/android/incallui/video/protocol/VideoCallScreen.java
+++ b/java/com/android/incallui/video/protocol/VideoCallScreen.java
@@ -21,6 +21,10 @@
 /** Interface for call video call module. */
 public interface VideoCallScreen {
 
+  void onVideoScreenStart();
+
+  void onVideoScreenStop();
+
   void showVideoViews(boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld);
 
   void onLocalVideoDimensionsChanged();
@@ -33,4 +37,6 @@
       boolean shouldShowFullscreen, boolean shouldShowGreenScreen);
 
   Fragment getVideoCallScreenFragment();
+
+  String getCallId();
 }
diff --git a/java/com/android/incallui/videotech/VideoTech.java b/java/com/android/incallui/videotech/VideoTech.java
new file mode 100644
index 0000000..fb26417
--- /dev/null
+++ b/java/com/android/incallui/videotech/VideoTech.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videotech;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Video calling interface. */
+public interface VideoTech {
+
+  boolean isAvailable();
+
+  boolean isTransmittingOrReceiving();
+
+  void onCallStateChanged(int newState);
+
+  @SessionModificationState
+  int getSessionModificationState();
+
+  void upgradeToVideo();
+
+  void acceptVideoRequest();
+
+  void acceptVideoRequestAsAudio();
+
+  void declineVideoRequest();
+
+  boolean isTransmitting();
+
+  void stopTransmission();
+
+  void resumeTransmission();
+
+  void pause();
+
+  void unpause();
+
+  void setCamera(String cameraId);
+
+  void setDeviceOrientation(int rotation);
+
+  /** Listener for video call events. */
+  interface VideoTechListener {
+
+    void onVideoTechStateChanged();
+
+    void onSessionModificationStateChanged();
+
+    void onCameraDimensionsChanged(int width, int height);
+
+    void onPeerDimensionsChanged(int width, int height);
+
+    void onVideoUpgradeRequestReceived();
+  }
+
+  /**
+   * Defines different states of session modify requests, which are used to upgrade to video, or
+   * downgrade to audio.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({
+    SESSION_MODIFICATION_STATE_NO_REQUEST,
+    SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE,
+    SESSION_MODIFICATION_STATE_REQUEST_FAILED,
+    SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
+    SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT,
+    SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED,
+    SESSION_MODIFICATION_STATE_REQUEST_REJECTED,
+    SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE
+  })
+  @interface SessionModificationState {}
+
+  int SESSION_MODIFICATION_STATE_NO_REQUEST = 0;
+  int SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE = 1;
+  int SESSION_MODIFICATION_STATE_REQUEST_FAILED = 2;
+  int SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST = 3;
+  int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT = 4;
+  int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED = 5;
+  int SESSION_MODIFICATION_STATE_REQUEST_REJECTED = 6;
+  int SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE = 7;
+}
diff --git a/java/com/android/incallui/videotech/empty/EmptyVideoTech.java b/java/com/android/incallui/videotech/empty/EmptyVideoTech.java
new file mode 100644
index 0000000..bc8db4c
--- /dev/null
+++ b/java/com/android/incallui/videotech/empty/EmptyVideoTech.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videotech.empty;
+
+import com.android.incallui.videotech.VideoTech;
+
+/** Default video tech that is always available but doesn't do anything. */
+public class EmptyVideoTech implements VideoTech {
+
+  @Override
+  public boolean isAvailable() {
+    return false;
+  }
+
+  @Override
+  public boolean isTransmittingOrReceiving() {
+    return false;
+  }
+
+  @Override
+  public void onCallStateChanged(int newState) {}
+
+  @Override
+  public int getSessionModificationState() {
+    return VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST;
+  }
+
+  @Override
+  public void upgradeToVideo() {}
+
+  @Override
+  public void acceptVideoRequest() {}
+
+  @Override
+  public void acceptVideoRequestAsAudio() {}
+
+  @Override
+  public void declineVideoRequest() {}
+
+  @Override
+  public boolean isTransmitting() {
+    return false;
+  }
+
+  @Override
+  public void stopTransmission() {}
+
+  @Override
+  public void resumeTransmission() {}
+
+  @Override
+  public void pause() {}
+
+  @Override
+  public void unpause() {}
+
+  @Override
+  public void setCamera(String cameraId) {}
+
+  @Override
+  public void setDeviceOrientation(int rotation) {}
+}
diff --git a/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java b/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
new file mode 100644
index 0000000..0a15f7e
--- /dev/null
+++ b/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videotech.ims;
+
+import android.os.Handler;
+import android.telecom.Call;
+import android.telecom.Connection;
+import android.telecom.Connection.VideoProvider;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.CameraCapabilities;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.SessionModificationState;
+import com.android.incallui.videotech.VideoTech.VideoTechListener;
+
+/** Receives IMS video call state updates. */
+public class ImsVideoCallCallback extends VideoCall.Callback {
+  private static final int CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS = 4000;
+  private final Handler handler = new Handler();
+  private final Call call;
+  private final ImsVideoTech videoTech;
+  private final VideoTechListener listener;
+  private int requestedVideoState = VideoProfile.STATE_AUDIO_ONLY;
+
+  ImsVideoCallCallback(final Call call, ImsVideoTech videoTech, VideoTechListener listener) {
+    this.call = call;
+    this.videoTech = videoTech;
+    this.listener = listener;
+  }
+
+  @Override
+  public void onSessionModifyRequestReceived(VideoProfile videoProfile) {
+    LogUtil.i(
+        "ImsVideoCallCallback.onSessionModifyRequestReceived", "videoProfile: " + videoProfile);
+
+    int previousVideoState = ImsVideoTech.getUnpausedVideoState(call.getDetails().getVideoState());
+    int newVideoState = ImsVideoTech.getUnpausedVideoState(videoProfile.getVideoState());
+
+    boolean wasVideoCall = VideoProfile.isVideo(previousVideoState);
+    boolean isVideoCall = VideoProfile.isVideo(newVideoState);
+
+    if (wasVideoCall && !isVideoCall) {
+      LogUtil.i(
+          "ImsVideoTech.onSessionModifyRequestReceived", "call downgraded to %d", newVideoState);
+    } else if (previousVideoState != newVideoState) {
+      requestedVideoState = newVideoState;
+      videoTech.setSessionModificationState(
+          VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
+      listener.onVideoUpgradeRequestReceived();
+    }
+  }
+
+  /**
+   * @param status Status of the session modify request. Valid values are {@link
+   *     Connection.VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, {@link
+   *     Connection.VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, {@link
+   *     Connection.VideoProvider#SESSION_MODIFY_REQUEST_INVALID}
+   * @param responseProfile The actual profile changes made by the peer device.
+   */
+  @Override
+  public void onSessionModifyResponseReceived(
+      int status, VideoProfile requestedProfile, VideoProfile responseProfile) {
+    LogUtil.i(
+        "ImsVideoCallCallback.onSessionModifyResponseReceived",
+        "status: %d, requestedProfile: %s, responseProfile: %s, session modification state: %d",
+        status,
+        requestedProfile,
+        responseProfile,
+        videoTech.getSessionModificationState());
+
+    if (videoTech.getSessionModificationState()
+        == VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE) {
+      handler.removeCallbacksAndMessages(null); // Clear everything
+
+      final int newSessionModificationState = getSessionModificationStateFromTelecomStatus(status);
+      if (status != VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
+        // This will update the video UI to display the error message.
+        videoTech.setSessionModificationState(newSessionModificationState);
+      }
+
+      // Wait for 4 seconds and then clean the session modification state. This allows the video UI
+      // to stay up so that the user can read the error message.
+      //
+      // If the other person accepted the upgrade request then this will keep the video UI up until
+      // the call's video state change. Without this we would switch to the voice call and then
+      // switch back to video UI.
+      handler.postDelayed(
+          () -> {
+            if (videoTech.getSessionModificationState() == newSessionModificationState) {
+              LogUtil.i("ImsVideoCallCallback.onSessionModifyResponseReceived", "clearing state");
+              videoTech.setSessionModificationState(
+                  VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+            } else {
+              LogUtil.i(
+                  "ImsVideoCallCallback.onSessionModifyResponseReceived",
+                  "session modification state has changed, not clearing state");
+            }
+          },
+          CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS);
+    } else if (videoTech.getSessionModificationState()
+        == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+      videoTech.setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+    } else if (videoTech.getSessionModificationState()
+        == VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE) {
+      videoTech.setSessionModificationState(getSessionModificationStateFromTelecomStatus(status));
+    } else {
+      LogUtil.i(
+          "ImsVideoCallCallback.onSessionModifyResponseReceived",
+          "call is not waiting for response, doing nothing");
+    }
+  }
+
+  @SessionModificationState
+  private int getSessionModificationStateFromTelecomStatus(int telecomStatus) {
+    switch (telecomStatus) {
+      case VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS:
+        return VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST;
+      case VideoProvider.SESSION_MODIFY_REQUEST_FAIL:
+      case VideoProvider.SESSION_MODIFY_REQUEST_INVALID:
+        // Check if it's already video call, which means the request is not video upgrade request.
+        if (VideoProfile.isVideo(call.getDetails().getVideoState())) {
+          return VideoTech.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
+        } else {
+          return VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED;
+        }
+      case VideoProvider.SESSION_MODIFY_REQUEST_TIMED_OUT:
+        return VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
+      case VideoProvider.SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE:
+        return VideoTech.SESSION_MODIFICATION_STATE_REQUEST_REJECTED;
+      default:
+        LogUtil.e(
+            "ImsVideoCallCallback.getSessionModificationStateFromTelecomStatus",
+            "unknown status: %d",
+            telecomStatus);
+        return VideoTech.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
+    }
+  }
+
+  @Override
+  public void onCallSessionEvent(int event) {
+    switch (event) {
+      case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE:
+        LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "rx_pause");
+        break;
+      case Connection.VideoProvider.SESSION_EVENT_RX_RESUME:
+        LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "rx_resume");
+        break;
+      case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE:
+        LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "camera_failure");
+        break;
+      case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY:
+        LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "camera_ready");
+        break;
+      default:
+        LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "unknown event = : " + event);
+        break;
+    }
+  }
+
+  @Override
+  public void onPeerDimensionsChanged(int width, int height) {
+    listener.onPeerDimensionsChanged(width, height);
+  }
+
+  @Override
+  public void onVideoQualityChanged(int videoQuality) {
+    LogUtil.i("ImsVideoCallCallback.onVideoQualityChanged", "videoQuality: %d", videoQuality);
+  }
+
+  @Override
+  public void onCallDataUsageChanged(long dataUsage) {
+    LogUtil.i("ImsVideoCallCallback.onCallDataUsageChanged", "dataUsage: %d", dataUsage);
+  }
+
+  @Override
+  public void onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities) {
+    if (cameraCapabilities != null) {
+      listener.onCameraDimensionsChanged(
+          cameraCapabilities.getWidth(), cameraCapabilities.getHeight());
+    }
+  }
+
+  int getRequestedVideoState() {
+    return requestedVideoState;
+  }
+}
diff --git a/java/com/android/incallui/videotech/ims/ImsVideoTech.java b/java/com/android/incallui/videotech/ims/ImsVideoTech.java
new file mode 100644
index 0000000..890e5c8
--- /dev/null
+++ b/java/com/android/incallui/videotech/ims/ImsVideoTech.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videotech.ims;
+
+import android.os.Build;
+import android.telecom.Call;
+import android.telecom.Call.Details;
+import android.telecom.VideoProfile;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.videotech.VideoTech;
+
+/** ViLTE implementation */
+public class ImsVideoTech implements VideoTech {
+  private final Call call;
+  private final VideoTechListener listener;
+  private ImsVideoCallCallback callback;
+  private @SessionModificationState int sessionModificationState =
+      VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST;
+  private int previousVideoState = VideoProfile.STATE_AUDIO_ONLY;
+
+  public ImsVideoTech(VideoTechListener listener, Call call) {
+    this.listener = listener;
+    this.call = call;
+  }
+
+  @Override
+  public boolean isAvailable() {
+    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+      return false;
+    }
+
+    boolean hasCapabilities =
+        call.getDetails().can(Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX)
+            && call.getDetails().can(Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX);
+
+    return call.getVideoCall() != null
+        && (hasCapabilities || VideoProfile.isVideo(call.getDetails().getVideoState()));
+  }
+
+  @Override
+  public boolean isTransmittingOrReceiving() {
+    return VideoProfile.isVideo(call.getDetails().getVideoState());
+  }
+
+  @Override
+  public void onCallStateChanged(int newState) {
+    if (!isAvailable()) {
+      return;
+    }
+
+    if (callback == null) {
+      callback = new ImsVideoCallCallback(call, this, listener);
+      call.getVideoCall().registerCallback(callback);
+    }
+
+    if (getSessionModificationState()
+            == VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
+        && isTransmittingOrReceiving()) {
+      // We don't clear the session modification state right away when we find out the video upgrade
+      // request was accepted to avoid having the UI switch from video to voice to video.
+      // Once the underlying telecom call updates to video mode it's safe to clear the state.
+      LogUtil.i(
+          "ImsVideoTech.onCallStateChanged",
+          "upgraded to video, clearing session modification state");
+      setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+    }
+
+    // Determines if a received upgrade to video request should be cancelled. This can happen if
+    // another InCall UI responds to the upgrade to video request.
+    int newVideoState = call.getDetails().getVideoState();
+    if (newVideoState != previousVideoState
+        && sessionModificationState
+            == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+      LogUtil.i("ImsVideoTech.onCallStateChanged", "cancelling upgrade notification");
+      setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+    }
+    previousVideoState = newVideoState;
+  }
+
+  @Override
+  public int getSessionModificationState() {
+    return sessionModificationState;
+  }
+
+  void setSessionModificationState(@SessionModificationState int state) {
+    if (state != sessionModificationState) {
+      LogUtil.i(
+          "ImsVideoTech.setSessionModificationState", "%d -> %d", sessionModificationState, state);
+      sessionModificationState = state;
+      listener.onSessionModificationStateChanged();
+    }
+  }
+
+  @Override
+  public void upgradeToVideo() {
+    LogUtil.enterBlock("ImsVideoTech.upgradeToVideo");
+
+    int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
+    call.getVideoCall()
+        .sendSessionModifyRequest(
+            new VideoProfile(unpausedVideoState | VideoProfile.STATE_BIDIRECTIONAL));
+    setSessionModificationState(
+        VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE);
+  }
+
+  @Override
+  public void acceptVideoRequest() {
+    int requestedVideoState = callback.getRequestedVideoState();
+    Assert.checkArgument(requestedVideoState != VideoProfile.STATE_AUDIO_ONLY);
+    LogUtil.i("ImsVideoTech.acceptUpgradeRequest", "videoState: " + requestedVideoState);
+    call.getVideoCall().sendSessionModifyResponse(new VideoProfile(requestedVideoState));
+    setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+  }
+
+  @Override
+  public void acceptVideoRequestAsAudio() {
+    LogUtil.enterBlock("ImsVideoTech.acceptVideoRequestAsAudio");
+    call.getVideoCall().sendSessionModifyResponse(new VideoProfile(VideoProfile.STATE_AUDIO_ONLY));
+    setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+  }
+
+  @Override
+  public void declineVideoRequest() {
+    LogUtil.enterBlock("ImsVideoTech.declineUpgradeRequest");
+    call.getVideoCall()
+        .sendSessionModifyResponse(new VideoProfile(call.getDetails().getVideoState()));
+    setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+  }
+
+  @Override
+  public boolean isTransmitting() {
+    return VideoProfile.isTransmissionEnabled(call.getDetails().getVideoState());
+  }
+
+  @Override
+  public void stopTransmission() {
+    LogUtil.enterBlock("ImsVideoTech.stopTransmission");
+
+    int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
+    call.getVideoCall()
+        .sendSessionModifyRequest(
+            new VideoProfile(unpausedVideoState & ~VideoProfile.STATE_TX_ENABLED));
+  }
+
+  @Override
+  public void resumeTransmission() {
+    LogUtil.enterBlock("ImsVideoTech.resumeTransmission");
+
+    int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
+    call.getVideoCall()
+        .sendSessionModifyRequest(
+            new VideoProfile(unpausedVideoState | VideoProfile.STATE_TX_ENABLED));
+    setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE);
+  }
+
+  @Override
+  public void pause() {
+    if (canPause()) {
+      LogUtil.i("ImsVideoTech.pause", "sending pause request");
+      int pausedVideoState = call.getDetails().getVideoState() | VideoProfile.STATE_PAUSED;
+      call.getVideoCall().sendSessionModifyRequest(new VideoProfile(pausedVideoState));
+    } else {
+      LogUtil.i("ImsVideoTech.pause", "not sending request: canPause: %b", canPause());
+    }
+  }
+
+  @Override
+  public void unpause() {
+    if (canPause()) {
+      LogUtil.i("ImsVideoTech.unpause", "sending unpause request");
+      int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
+      call.getVideoCall().sendSessionModifyRequest(new VideoProfile(unpausedVideoState));
+    } else {
+      LogUtil.i("ImsVideoTech.unpause", "not sending request: canPause: %b", canPause());
+    }
+  }
+
+  @Override
+  public void setCamera(String cameraId) {
+    call.getVideoCall().setCamera(cameraId);
+    call.getVideoCall().requestCameraCapabilities();
+  }
+
+  @Override
+  public void setDeviceOrientation(int rotation) {
+    call.getVideoCall().setDeviceOrientation(rotation);
+  }
+
+  private boolean canPause() {
+    return call.getDetails().can(Details.CAPABILITY_CAN_PAUSE_VIDEO)
+        && call.getState() == Call.STATE_ACTIVE;
+  }
+
+  static int getUnpausedVideoState(int videoState) {
+    return videoState & (~VideoProfile.STATE_PAUSED);
+  }
+}
diff --git a/java/com/android/incallui/videotech/rcs/RcsVideoShare.java b/java/com/android/incallui/videotech/rcs/RcsVideoShare.java
new file mode 100644
index 0000000..2cb4303
--- /dev/null
+++ b/java/com/android/incallui/videotech/rcs/RcsVideoShare.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.videotech.rcs;
+
+import android.support.annotation.NonNull;
+import android.telecom.Call;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.enrichedcall.EnrichedCallManager.CapabilitiesListener;
+import com.android.dialer.enrichedcall.Session;
+import com.android.dialer.enrichedcall.videoshare.VideoShareListener;
+import com.android.incallui.videotech.VideoTech;
+
+/** Allows the in-call UI to make video calls over RCS. */
+public class RcsVideoShare implements VideoTech, CapabilitiesListener, VideoShareListener {
+  private final EnrichedCallManager enrichedCallManager;
+  private final VideoTechListener listener;
+  private final String callingNumber;
+  private int previousCallState = Call.STATE_NEW;
+  private long inviteSessionId = Session.NO_SESSION_ID;
+  private long transmittingSessionId = Session.NO_SESSION_ID;
+  private long receivingSessionId = Session.NO_SESSION_ID;
+
+  private @SessionModificationState int sessionModificationState =
+      VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST;
+
+  public RcsVideoShare(
+      @NonNull EnrichedCallManager enrichedCallManager,
+      @NonNull VideoTechListener listener,
+      @NonNull String callingNumber) {
+    this.enrichedCallManager = Assert.isNotNull(enrichedCallManager);
+    this.listener = Assert.isNotNull(listener);
+    this.callingNumber = Assert.isNotNull(callingNumber);
+
+    enrichedCallManager.registerCapabilitiesListener(this);
+    enrichedCallManager.registerVideoShareListener(this);
+  }
+
+  @Override
+  public boolean isAvailable() {
+    EnrichedCallCapabilities capabilities = enrichedCallManager.getCapabilities(callingNumber);
+    return capabilities != null && capabilities.supportsVideoShare();
+  }
+
+  @Override
+  public boolean isTransmittingOrReceiving() {
+    return transmittingSessionId != Session.NO_SESSION_ID
+        || receivingSessionId != Session.NO_SESSION_ID;
+  }
+
+  @Override
+  public void onCallStateChanged(int newState) {
+    if (newState == Call.STATE_DISCONNECTING) {
+      enrichedCallManager.unregisterVideoShareListener(this);
+      enrichedCallManager.unregisterCapabilitiesListener(this);
+    }
+
+    if (newState != previousCallState && newState == Call.STATE_ACTIVE) {
+      // Per spec, request capabilities when the call becomes active
+      enrichedCallManager.requestCapabilities(callingNumber);
+    }
+
+    previousCallState = newState;
+  }
+
+  @Override
+  public int getSessionModificationState() {
+    return sessionModificationState;
+  }
+
+  private void setSessionModificationState(@SessionModificationState int state) {
+    if (state != sessionModificationState) {
+      LogUtil.i(
+          "RcsVideoShare.setSessionModificationState", "%d -> %d", sessionModificationState, state);
+      sessionModificationState = state;
+      listener.onSessionModificationStateChanged();
+    }
+  }
+
+  @Override
+  public void upgradeToVideo() {
+    LogUtil.enterBlock("RcsVideoShare.upgradeToVideo");
+    transmittingSessionId = enrichedCallManager.startVideoShareSession(callingNumber);
+    if (transmittingSessionId != Session.NO_SESSION_ID) {
+      setSessionModificationState(
+          VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE);
+    }
+  }
+
+  @Override
+  public void acceptVideoRequest() {
+    LogUtil.enterBlock("RcsVideoShare.acceptVideoRequest");
+    if (enrichedCallManager.acceptVideoShareSession(inviteSessionId)) {
+      receivingSessionId = inviteSessionId;
+    }
+    inviteSessionId = Session.NO_SESSION_ID;
+    setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+  }
+
+  @Override
+  public void acceptVideoRequestAsAudio() {
+    throw Assert.createUnsupportedOperationFailException();
+  }
+
+  @Override
+  public void declineVideoRequest() {
+    LogUtil.enterBlock("RcsVideoTech.declineUpgradeRequest");
+    enrichedCallManager.endVideoShareSession(
+        enrichedCallManager.getVideoShareInviteSessionId(callingNumber));
+    inviteSessionId = Session.NO_SESSION_ID;
+    setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+  }
+
+  @Override
+  public boolean isTransmitting() {
+    return transmittingSessionId != Session.NO_SESSION_ID;
+  }
+
+  @Override
+  public void stopTransmission() {
+    LogUtil.enterBlock("RcsVideoTech.stopTransmission");
+  }
+
+  @Override
+  public void resumeTransmission() {
+    LogUtil.enterBlock("RcsVideoTech.resumeTransmission");
+  }
+
+  @Override
+  public void pause() {}
+
+  @Override
+  public void unpause() {}
+
+  @Override
+  public void setCamera(String cameraId) {}
+
+  @Override
+  public void setDeviceOrientation(int rotation) {}
+
+  @Override
+  public void onCapabilitiesUpdated() {
+    listener.onVideoTechStateChanged();
+  }
+
+  @Override
+  public void onVideoShareChanged() {
+    long existingInviteSessionId = inviteSessionId;
+
+    inviteSessionId = enrichedCallManager.getVideoShareInviteSessionId(callingNumber);
+    if (inviteSessionId != Session.NO_SESSION_ID) {
+      if (existingInviteSessionId == Session.NO_SESSION_ID) {
+        // This is a new invite
+        setSessionModificationState(
+            VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
+        listener.onVideoUpgradeRequestReceived();
+      }
+    } else {
+      setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+    }
+
+    if (sessionIsClosed(transmittingSessionId)) {
+      LogUtil.i("RcsVideoShare.onSessionClosed", "transmitting session closed");
+      transmittingSessionId = Session.NO_SESSION_ID;
+    }
+
+    if (sessionIsClosed(receivingSessionId)) {
+      LogUtil.i("RcsVideoShare.onSessionClosed", "receiving session closed");
+      receivingSessionId = Session.NO_SESSION_ID;
+    }
+
+    listener.onVideoTechStateChanged();
+  }
+
+  private boolean sessionIsClosed(long sessionId) {
+    return sessionId != Session.NO_SESSION_ID
+        && enrichedCallManager.getVideoShareSession(sessionId) == null;
+  }
+}
diff --git a/java/com/android/voicemail/VoicemailClient.java b/java/com/android/voicemail/VoicemailClient.java
new file mode 100644
index 0000000..b237f65
--- /dev/null
+++ b/java/com/android/voicemail/VoicemailClient.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail;
+
+import android.content.Context;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import java.util.List;
+
+/** Public interface for the voicemail module */
+public interface VoicemailClient {
+
+  /**
+   * Broadcast to tell the client to upload local database changes to the server. Since the dialer
+   * UI and the client are in the same package, the {@link
+   * android.content.Intent#ACTION_PROVIDER_CHANGED} will always be a self-change even if the UI is
+   * external to the client.
+   */
+  String ACTION_UPLOAD = "com.android.voicemailomtp.VoicemailClient.ACTION_UPLOAD";
+
+  /**
+   * Appends the selection to ignore voicemails from non-active OMTP voicemail package. In OC there
+   * can be multiple packages handling OMTP voicemails which represents the same source of truth.
+   * These packages should mark their voicemails as {@link Voicemails#IS_OMTP_VOICEMAIL} and only
+   * the voicemails from {@link TelephonyManager#getVisualVoicemailPackageName()} should be shown.
+   * For example, the user synced voicemails with DialerA, and then switched to DialerB, voicemails
+   * from DialerA should be ignored as they are no longer current. Voicemails from {@link
+   * #OMTP_VOICEMAIL_BLACKLIST} will also be ignored as they are voicemail source only valid pre-OC.
+   */
+  void appendOmtpVoicemailSelectionClause(
+      Context context, StringBuilder where, List<String> selectionArgs);
+  /**
+   * @return the class name of the {@link android.preference.PreferenceFragment} for voicemail
+   *     settings, or {@code null} if dialer cannot control voicemail settings. Always return {@code
+   *     null} before OC.
+   */
+  @Nullable
+  String getSettingsFragment();
+
+  boolean isVoicemailArchiveEnabled(Context context, PhoneAccountHandle phoneAccountHandle);
+
+  void setVoicemailArchiveEnabled(
+      Context context, PhoneAccountHandle phoneAccountHandle, boolean value);
+}
diff --git a/java/com/android/voicemail/VoicemailComponent.java b/java/com/android/voicemail/VoicemailComponent.java
new file mode 100644
index 0000000..6dd6f9d
--- /dev/null
+++ b/java/com/android/voicemail/VoicemailComponent.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail;
+
+import android.content.Context;
+import dagger.Subcomponent;
+import com.android.voicemail.impl.VoicemailClientImpl;
+
+/** Subcomponent that can be used to access the voicemail implementation. */
+public class VoicemailComponent {
+    private static VoicemailComponent instance;
+    private VoicemailClientImpl voicemailClient;
+
+  public VoicemailClient getVoicemailClient() {
+    if (voicemailClient == null) {
+        voicemailClient = new VoicemailClientImpl();
+    }
+    return voicemailClient;
+  }
+
+  public static VoicemailComponent get(Context context) {
+    if (instance == null) {
+        instance = new VoicemailComponent();
+    }
+    return instance;
+  }
+
+  /** Used to refer to the root application component. */
+  public interface HasComponent {
+    VoicemailComponent voicemailComponent();
+  }
+}
diff --git a/java/com/android/voicemail/impl/ActivationTask.java b/java/com/android/voicemail/impl/ActivationTask.java
new file mode 100644
index 0000000..c471611
--- /dev/null
+++ b/java/com/android/voicemail/impl/ActivationTask.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import com.android.voicemail.impl.protocol.VisualVoicemailProtocol;
+import com.android.voicemail.impl.scheduling.BaseTask;
+import com.android.voicemail.impl.scheduling.RetryPolicy;
+import com.android.voicemail.impl.sms.StatusMessage;
+import com.android.voicemail.impl.sms.StatusSmsFetcher;
+import com.android.voicemail.impl.sync.OmtpVvmSyncService;
+import com.android.voicemail.impl.sync.SyncTask;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Task to activate the visual voicemail service. A request to activate VVM will be sent to the
+ * carrier, which will respond with a STATUS SMS. The credentials will be updated from the SMS. If
+ * the user is not provisioned provisioning will be attempted. Activation happens when the phone
+ * boots, the SIM is inserted, signal returned when VVM is not activated yet, and when the carrier
+ * spontaneously sent a STATUS SMS.
+ */
+@TargetApi(VERSION_CODES.O)
+public class ActivationTask extends BaseTask {
+
+  private static final String TAG = "VvmActivationTask";
+
+  private static final int RETRY_TIMES = 4;
+  private static final int RETRY_INTERVAL_MILLIS = 5_000;
+
+  private static final String EXTRA_MESSAGE_DATA_BUNDLE = "extra_message_data_bundle";
+
+  @Nullable private static DeviceProvisionedObserver sDeviceProvisionedObserver;
+
+  private final RetryPolicy mRetryPolicy;
+
+  private Bundle mMessageData;
+
+  public ActivationTask() {
+    super(TASK_ACTIVATION);
+    mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS);
+    addPolicy(mRetryPolicy);
+  }
+
+  /** Has the user gone through the setup wizard yet. */
+  private static boolean isDeviceProvisioned(Context context) {
+    return Settings.Global.getInt(
+            context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0)
+        == 1;
+  }
+
+  /**
+   * @param messageData The optional bundle from {@link android.provider.VoicemailContract#
+   *     EXTRA_VOICEMAIL_SMS_FIELDS}, if the task is initiated by a status SMS. If null the task
+   *     will request a status SMS itself.
+   */
+  public static void start(
+      Context context, PhoneAccountHandle phoneAccountHandle, @Nullable Bundle messageData) {
+    if (!isDeviceProvisioned(context)) {
+      VvmLog.i(TAG, "Activation requested while device is not provisioned, postponing");
+      // Activation might need information such as system language to be set, so wait until
+      // the setup wizard is finished. The data bundle from the SMS will be re-requested upon
+      // activation.
+      queueActivationAfterProvisioned(context, phoneAccountHandle);
+      return;
+    }
+
+    Intent intent = BaseTask.createIntent(context, ActivationTask.class, phoneAccountHandle);
+    if (messageData != null) {
+      intent.putExtra(EXTRA_MESSAGE_DATA_BUNDLE, messageData);
+    }
+    context.startService(intent);
+  }
+
+  @Override
+  public void onCreate(Context context, Intent intent, int flags, int startId) {
+    super.onCreate(context, intent, flags, startId);
+    mMessageData = intent.getParcelableExtra(EXTRA_MESSAGE_DATA_BUNDLE);
+  }
+
+  @Override
+  public Intent createRestartIntent() {
+    Intent intent = super.createRestartIntent();
+    // mMessageData is discarded, request a fresh STATUS SMS for retries.
+    return intent;
+  }
+
+  @Override
+  @WorkerThread
+  public void onExecuteInBackgroundThread() {
+    Assert.isNotMainThread();
+
+    PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
+    if (phoneAccountHandle == null) {
+      // This should never happen
+      VvmLog.e(TAG, "null PhoneAccountHandle");
+      return;
+    }
+
+    OmtpVvmCarrierConfigHelper helper =
+        new OmtpVvmCarrierConfigHelper(getContext(), phoneAccountHandle);
+    if (!helper.isValid()) {
+      VvmLog.i(TAG, "VVM not supported on phoneAccountHandle " + phoneAccountHandle);
+      VvmAccountManager.removeAccount(getContext(), phoneAccountHandle);
+      return;
+    }
+
+    // OmtpVvmCarrierConfigHelper can start the activation process; it will pass in a vvm
+    // content provider URI which we will use.  On some occasions, setting that URI will
+    // fail, so we will perform a few attempts to ensure that the vvm content provider has
+    // a good chance of being started up.
+    if (!VoicemailStatus.edit(getContext(), phoneAccountHandle)
+        .setType(helper.getVvmType())
+        .apply()) {
+      VvmLog.e(TAG, "Failed to configure content provider - " + helper.getVvmType());
+      fail();
+    }
+    VvmLog.i(TAG, "VVM content provider configured - " + helper.getVvmType());
+
+    if (VvmAccountManager.isAccountActivated(getContext(), phoneAccountHandle)) {
+      VvmLog.i(TAG, "Account is already activated");
+      return;
+    }
+    helper.handleEvent(
+        VoicemailStatus.edit(getContext(), phoneAccountHandle), OmtpEvents.CONFIG_ACTIVATING);
+
+    if (!hasSignal(getContext(), phoneAccountHandle)) {
+      VvmLog.i(TAG, "Service lost during activation, aborting");
+      // Restore the "NO SIGNAL" state since it will be overwritten by the CONFIG_ACTIVATING
+      // event.
+      helper.handleEvent(
+          VoicemailStatus.edit(getContext(), phoneAccountHandle),
+          OmtpEvents.NOTIFICATION_SERVICE_LOST);
+      // Don't retry, a new activation will be started after the signal returned.
+      return;
+    }
+
+    helper.activateSmsFilter();
+    VoicemailStatus.Editor status = mRetryPolicy.getVoicemailStatusEditor();
+
+    VisualVoicemailProtocol protocol = helper.getProtocol();
+
+    Bundle data;
+    if (mMessageData != null) {
+      // The content of STATUS SMS is provided to launch this task, no need to request it
+      // again.
+      data = mMessageData;
+    } else {
+      try (StatusSmsFetcher fetcher = new StatusSmsFetcher(getContext(), phoneAccountHandle)) {
+        protocol.startActivation(helper, fetcher.getSentIntent());
+        // Both the fetcher and OmtpMessageReceiver will be triggered, but
+        // OmtpMessageReceiver will just route the SMS back to ActivationTask, which will be
+        // rejected because the task is still running.
+        data = fetcher.get();
+      } catch (TimeoutException e) {
+        // The carrier is expected to return an STATUS SMS within STATUS_SMS_TIMEOUT_MILLIS
+        // handleEvent() will do the logging.
+        helper.handleEvent(status, OmtpEvents.CONFIG_STATUS_SMS_TIME_OUT);
+        fail();
+        return;
+      } catch (CancellationException e) {
+        VvmLog.e(TAG, "Unable to send status request SMS");
+        fail();
+        return;
+      } catch (InterruptedException | ExecutionException | IOException e) {
+        VvmLog.e(TAG, "can't get future STATUS SMS", e);
+        fail();
+        return;
+      }
+    }
+
+    StatusMessage message = new StatusMessage(data);
+    VvmLog.d(
+        TAG,
+        "STATUS SMS received: st="
+            + message.getProvisioningStatus()
+            + ", rc="
+            + message.getReturnCode());
+
+    if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_READY)) {
+      VvmLog.d(TAG, "subscriber ready, no activation required");
+      updateSource(getContext(), phoneAccountHandle, status, message);
+    } else {
+      if (helper.supportsProvisioning()) {
+        VvmLog.i(TAG, "Subscriber not ready, start provisioning");
+        helper.startProvisioning(this, phoneAccountHandle, status, message, data);
+
+      } else if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_NEW)) {
+        VvmLog.i(TAG, "Subscriber new but provisioning is not supported");
+        // Ignore the non-ready state and attempt to use the provided info as is.
+        // This is probably caused by not completing the new user tutorial.
+        updateSource(getContext(), phoneAccountHandle, status, message);
+      } else {
+        VvmLog.i(TAG, "Subscriber not ready but provisioning is not supported");
+        helper.handleEvent(status, OmtpEvents.CONFIG_SERVICE_NOT_AVAILABLE);
+      }
+    }
+  }
+
+  public static void updateSource(
+      Context context,
+      PhoneAccountHandle phone,
+      VoicemailStatus.Editor status,
+      StatusMessage message) {
+
+    if (OmtpConstants.SUCCESS.equals(message.getReturnCode())) {
+      OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(context, phone);
+      helper.handleEvent(status, OmtpEvents.CONFIG_REQUEST_STATUS_SUCCESS);
+
+      // Save the IMAP credentials in preferences so they are persistent and can be retrieved.
+      VvmAccountManager.addAccount(context, phone, message);
+
+      SyncTask.start(context, phone, OmtpVvmSyncService.SYNC_FULL_SYNC);
+    } else {
+      VvmLog.e(TAG, "Visual voicemail not available for subscriber.");
+    }
+  }
+
+  private static boolean hasSignal(Context context, PhoneAccountHandle phoneAccountHandle) {
+    TelephonyManager telephonyManager =
+        context
+            .getSystemService(TelephonyManager.class)
+            .createForPhoneAccountHandle(phoneAccountHandle);
+    return telephonyManager.getServiceState().getState() == ServiceState.STATE_IN_SERVICE;
+  }
+
+  private static void queueActivationAfterProvisioned(
+      Context context, PhoneAccountHandle phoneAccountHandle) {
+    if (sDeviceProvisionedObserver == null) {
+      sDeviceProvisionedObserver = new DeviceProvisionedObserver(context);
+      context
+          .getContentResolver()
+          .registerContentObserver(
+              Settings.Global.getUriFor(Global.DEVICE_PROVISIONED),
+              false,
+              sDeviceProvisionedObserver);
+    }
+    sDeviceProvisionedObserver.addPhoneAcountHandle(phoneAccountHandle);
+  }
+
+  private static class DeviceProvisionedObserver extends ContentObserver {
+
+    private final Context mContext;
+    private final Set<PhoneAccountHandle> mPhoneAccountHandles = new HashSet<>();
+
+    private DeviceProvisionedObserver(Context context) {
+      super(null);
+      mContext = context;
+    }
+
+    public void addPhoneAcountHandle(PhoneAccountHandle phoneAccountHandle) {
+      mPhoneAccountHandles.add(phoneAccountHandle);
+    }
+
+    @Override
+    public void onChange(boolean selfChange) {
+      if (isDeviceProvisioned(mContext)) {
+        VvmLog.i(TAG, "device provisioned, resuming activation");
+        for (PhoneAccountHandle phoneAccountHandle : mPhoneAccountHandles) {
+          start(mContext, phoneAccountHandle, null);
+        }
+        mContext.getContentResolver().unregisterContentObserver(sDeviceProvisionedObserver);
+        sDeviceProvisionedObserver = null;
+      }
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/AndroidManifest.xml b/java/com/android/voicemail/impl/AndroidManifest.xml
new file mode 100644
index 0000000..0d90d59
--- /dev/null
+++ b/java/com/android/voicemail/impl/AndroidManifest.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+  package="com.android.voicemailomtp"
+  >
+
+  <application
+    android:allowBackup="false"
+    android:supportsRtl="true"
+    android:usesCleartextTraffic="true"
+    android:defaultToDeviceProtectedStorage="true"
+    android:directBootAware="true">
+
+    <!-- Causes the "Voicemail" item under "Calls" setting to be hidden. The voicemail module will
+      be handling the settings. Has no effect before OC where dialer cannot provide voicemail
+      settings-->
+    <meta-data android:name="android.telephony.HIDE_VOICEMAIL_SETTINGS_MENU" android:value="true"/>
+
+    <receiver
+      android:name="com.android.voicemail.impl.sms.OmtpMessageReceiver"
+      android:exported="false"
+      androidprv:systemUserOnly="true">
+      <intent-filter>
+        <action android:name="com.android.vociemailomtp.sms.sms_received"/>
+      </intent-filter>
+    </receiver>
+
+    <receiver android:name="com.android.voicemail.impl.VoicemailClientReceiver"
+      android:exported="false">
+      <intent-filter>
+        <action android:name="com.android.voicemailomtp.VoicemailClient.ACTION_UPLOAD"/>
+      </intent-filter>
+    </receiver>
+
+    <receiver
+      android:name="com.android.voicemail.impl.fetch.FetchVoicemailReceiver"
+      android:exported="true"
+      android:permission="com.android.voicemail.permission.READ_VOICEMAIL"
+      androidprv:systemUserOnly="true">
+      <intent-filter>
+        <action android:name="android.intent.action.FETCH_VOICEMAIL"/>
+        <data
+          android:scheme="content"
+          android:host="com.android.voicemail"
+          android:mimeType="vnd.android.cursor.item/voicemail"/>
+      </intent-filter>
+    </receiver>
+    <receiver
+      android:name="com.android.voicemail.impl.sync.OmtpVvmSyncReceiver"
+      android:exported="true"
+      android:permission="com.android.voicemail.permission.READ_VOICEMAIL"
+      androidprv:systemUserOnly="true">
+      <intent-filter>
+        <action android:name="android.provider.action.SYNC_VOICEMAIL"/>
+      </intent-filter>
+    </receiver>
+    <receiver
+      android:name="com.android.voicemail.impl.sync.VoicemailProviderChangeReceiver"
+      android:exported="true">
+      <intent-filter>
+        <action android:name="android.intent.action.PROVIDER_CHANGED"/>
+        <data
+          android:scheme="content"
+          android:host="com.android.voicemail"
+          android:mimeType="vnd.android.cursor.dir/voicemails"/>
+      </intent-filter>
+    </receiver>
+
+    <service
+      android:name="com.android.voicemail.impl.scheduling.TaskSchedulerService"
+      android:exported="false"/>
+
+    <service
+      android:name="com.android.voicemail.impl.OmtpService"
+      android:permission="android.permission.BIND_VISUAL_VOICEMAIL_SERVICE"
+      android:exported="true">
+      <intent-filter>
+        <action android:name="android.telephony.VisualVoicemailService"/>
+      </intent-filter>
+    </service>
+
+    <activity
+      android:name="com.android.voicemail.impl.settings.VoicemailChangePinActivity"
+      android:exported="false"
+      android:windowSoftInputMode="stateVisible|adjustResize">
+    </activity>
+  </application>
+</manifest>
diff --git a/java/com/android/voicemail/impl/Assert.java b/java/com/android/voicemail/impl/Assert.java
new file mode 100644
index 0000000..fe06372
--- /dev/null
+++ b/java/com/android/voicemail/impl/Assert.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.os.Looper;
+
+/** Assertions which will result in program termination. */
+public class Assert {
+
+  private static Boolean sIsMainThreadForTest;
+
+  public static void isTrue(boolean condition) {
+    if (!condition) {
+      throw new AssertionError("Expected condition to be true");
+    }
+  }
+
+  public static void isMainThread() {
+    if (sIsMainThreadForTest != null) {
+      isTrue(sIsMainThreadForTest);
+      return;
+    }
+    isTrue(Looper.getMainLooper().equals(Looper.myLooper()));
+  }
+
+  public static void isNotMainThread() {
+    if (sIsMainThreadForTest != null) {
+      isTrue(!sIsMainThreadForTest);
+      return;
+    }
+    isTrue(!Looper.getMainLooper().equals(Looper.myLooper()));
+  }
+
+  public static void fail() {
+    throw new AssertionError("Fail");
+  }
+
+  /** Override the main thread status for tests. Set to null to revert to normal behavior */
+  @NeededForTesting
+  public static void setIsMainThreadForTesting(Boolean isMainThread) {
+    sIsMainThreadForTest = isMainThread;
+  }
+}
diff --git a/java/com/android/voicemail/impl/DefaultOmtpEventHandler.java b/java/com/android/voicemail/impl/DefaultOmtpEventHandler.java
new file mode 100644
index 0000000..13aaf05
--- /dev/null
+++ b/java/com/android/voicemail/impl/DefaultOmtpEventHandler.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.content.Context;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Status;
+import com.android.voicemail.impl.OmtpEvents.Type;
+
+public class DefaultOmtpEventHandler {
+
+  private static final String TAG = "DefErrorCodeHandler";
+
+  public static void handleEvent(
+      Context context,
+      OmtpVvmCarrierConfigHelper config,
+      VoicemailStatus.Editor status,
+      OmtpEvents event) {
+    switch (event.getType()) {
+      case Type.CONFIGURATION:
+        handleConfigurationEvent(context, status, event);
+        break;
+      case Type.DATA_CHANNEL:
+        handleDataChannelEvent(context, status, event);
+        break;
+      case Type.NOTIFICATION_CHANNEL:
+        handleNotificationChannelEvent(context, config, status, event);
+        break;
+      case Type.OTHER:
+        handleOtherEvent(context, status, event);
+        break;
+      default:
+        VvmLog.wtf(TAG, "invalid event type " + event.getType() + " for " + event);
+    }
+  }
+
+  private static void handleConfigurationEvent(
+      Context context, VoicemailStatus.Editor status, OmtpEvents event) {
+    switch (event) {
+      case CONFIG_DEFAULT_PIN_REPLACED:
+      case CONFIG_REQUEST_STATUS_SUCCESS:
+      case CONFIG_PIN_SET:
+        status
+            .setConfigurationState(VoicemailContract.Status.CONFIGURATION_STATE_OK)
+            .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+            .apply();
+        break;
+      case CONFIG_ACTIVATING:
+        // Wipe all errors from the last activation. All errors shown should be new errors
+        // for this activation.
+        status
+            .setConfigurationState(Status.CONFIGURATION_STATE_CONFIGURING)
+            .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+            .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
+            .apply();
+        break;
+      case CONFIG_ACTIVATING_SUBSEQUENT:
+        status
+            .setConfigurationState(Status.CONFIGURATION_STATE_OK)
+            .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+            .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
+            .apply();
+        break;
+      case CONFIG_SERVICE_NOT_AVAILABLE:
+        status.setConfigurationState(Status.CONFIGURATION_STATE_FAILED).apply();
+        break;
+      case CONFIG_STATUS_SMS_TIME_OUT:
+        status.setConfigurationState(Status.CONFIGURATION_STATE_FAILED).apply();
+        break;
+      default:
+        VvmLog.wtf(TAG, "invalid configuration event " + event);
+    }
+  }
+
+  private static void handleDataChannelEvent(
+      Context context, VoicemailStatus.Editor status, OmtpEvents event) {
+    switch (event) {
+      case DATA_IMAP_OPERATION_STARTED:
+      case DATA_IMAP_OPERATION_COMPLETED:
+        status.setDataChannelState(Status.DATA_CHANNEL_STATE_OK).apply();
+        break;
+
+      case DATA_NO_CONNECTION:
+        status.setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION).apply();
+        break;
+
+      case DATA_NO_CONNECTION_CELLULAR_REQUIRED:
+        status
+            .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED)
+            .apply();
+        break;
+      case DATA_INVALID_PORT:
+        status
+            .setDataChannelState(VoicemailContract.Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION)
+            .apply();
+        break;
+      case DATA_CANNOT_RESOLVE_HOST_ON_NETWORK:
+        status
+            .setDataChannelState(
+                VoicemailContract.Status.DATA_CHANNEL_STATE_SERVER_CONNECTION_ERROR)
+            .apply();
+        break;
+      case DATA_SSL_INVALID_HOST_NAME:
+      case DATA_CANNOT_ESTABLISH_SSL_SESSION:
+      case DATA_IOE_ON_OPEN:
+      case DATA_GENERIC_IMAP_IOE:
+        status
+            .setDataChannelState(VoicemailContract.Status.DATA_CHANNEL_STATE_COMMUNICATION_ERROR)
+            .apply();
+        break;
+      case DATA_BAD_IMAP_CREDENTIAL:
+      case DATA_AUTH_UNKNOWN_USER:
+      case DATA_AUTH_UNKNOWN_DEVICE:
+      case DATA_AUTH_INVALID_PASSWORD:
+      case DATA_AUTH_MAILBOX_NOT_INITIALIZED:
+      case DATA_AUTH_SERVICE_NOT_PROVISIONED:
+      case DATA_AUTH_SERVICE_NOT_ACTIVATED:
+      case DATA_AUTH_USER_IS_BLOCKED:
+        status
+            .setDataChannelState(VoicemailContract.Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION)
+            .apply();
+        break;
+
+      case DATA_REJECTED_SERVER_RESPONSE:
+      case DATA_INVALID_INITIAL_SERVER_RESPONSE:
+      case DATA_MAILBOX_OPEN_FAILED:
+      case DATA_SSL_EXCEPTION:
+      case DATA_ALL_SOCKET_CONNECTION_FAILED:
+        status
+            .setDataChannelState(VoicemailContract.Status.DATA_CHANNEL_STATE_SERVER_ERROR)
+            .apply();
+        break;
+
+      default:
+        VvmLog.wtf(TAG, "invalid data channel event " + event);
+    }
+  }
+
+  private static void handleNotificationChannelEvent(
+      Context context,
+      OmtpVvmCarrierConfigHelper config,
+      VoicemailStatus.Editor status,
+      OmtpEvents event) {
+    switch (event) {
+      case NOTIFICATION_IN_SERVICE:
+        status
+            .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+            // Clear the error state. A sync should follow signal return so any error
+            // will be reposted.
+            .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
+            .apply();
+        break;
+      case NOTIFICATION_SERVICE_LOST:
+        status.setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION);
+        if (config.isCellularDataRequired()) {
+          status.setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED);
+        }
+        status.apply();
+        break;
+      default:
+        VvmLog.wtf(TAG, "invalid notification channel event " + event);
+    }
+  }
+
+  private static void handleOtherEvent(
+      Context context, VoicemailStatus.Editor status, OmtpEvents event) {
+    switch (event) {
+      case OTHER_SOURCE_REMOVED:
+        status
+            .setConfigurationState(Status.CONFIGURATION_STATE_NOT_CONFIGURED)
+            .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION)
+            .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION)
+            .apply();
+        break;
+      default:
+        VvmLog.wtf(TAG, "invalid other event " + event);
+    }
+  }
+}
diff --git a/java/com/android/voicemailomtp/NeededForTesting.java b/java/com/android/voicemail/impl/NeededForTesting.java
similarity index 90%
rename from java/com/android/voicemailomtp/NeededForTesting.java
rename to java/com/android/voicemail/impl/NeededForTesting.java
index 20517fe..70e7383 100644
--- a/java/com/android/voicemailomtp/NeededForTesting.java
+++ b/java/com/android/voicemail/impl/NeededForTesting.java
@@ -14,12 +14,10 @@
  * limitations under the License
  */
 
-package com.android.voicemailomtp;
+package com.android.voicemail.impl;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
 @Retention(RetentionPolicy.SOURCE)
-public @interface NeededForTesting {
-
-}
+public @interface NeededForTesting {}
diff --git a/java/com/android/voicemail/impl/OmtpConstants.java b/java/com/android/voicemail/impl/OmtpConstants.java
new file mode 100644
index 0000000..599d0d5
--- /dev/null
+++ b/java/com/android/voicemail/impl/OmtpConstants.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Wrapper class to hold relevant OMTP constants as defined in the OMTP spec.
+ *
+ * <p>In essence this is a programmatic representation of the relevant portions of OMTP spec.
+ */
+public class OmtpConstants {
+  public static final String SMS_FIELD_SEPARATOR = ";";
+  public static final String SMS_KEY_VALUE_SEPARATOR = "=";
+  public static final String SMS_PREFIX_SEPARATOR = ":";
+
+  public static final String SYNC_SMS_PREFIX = "SYNC";
+  public static final String STATUS_SMS_PREFIX = "STATUS";
+
+  // This is the format designated by the OMTP spec.
+  public static final String DATE_TIME_FORMAT = "dd/MM/yyyy HH:mm Z";
+
+  /** OMTP protocol versions. */
+  public static final String PROTOCOL_VERSION1_1 = "11";
+
+  public static final String PROTOCOL_VERSION1_2 = "12";
+  public static final String PROTOCOL_VERSION1_3 = "13";
+
+  ///////////////////////// Client/Mobile originated SMS //////////////////////
+
+  /** Mobile Originated requests */
+  public static final String ACTIVATE_REQUEST = "Activate";
+
+  public static final String DEACTIVATE_REQUEST = "Deactivate";
+  public static final String STATUS_REQUEST = "Status";
+
+  /** fields that can be present in a Mobile Originated OMTP SMS */
+  public static final String CLIENT_TYPE = "ct";
+
+  public static final String APPLICATION_PORT = "pt";
+  public static final String PROTOCOL_VERSION = "pv";
+
+  //////////////////////////////// Sync SMS fields ////////////////////////////
+
+  /**
+   * Sync SMS fields.
+   *
+   * <p>Each string constant is the field's key in the SMS body which is used by the parser to
+   * identify the field's value, if present, in the SMS body.
+   */
+
+  /** The event that triggered this SYNC SMS. See {@link OmtpConstants#SYNC_TRIGGER_EVENT_VALUES} */
+  public static final String SYNC_TRIGGER_EVENT = "ev";
+
+  public static final String MESSAGE_UID = "id";
+  public static final String MESSAGE_LENGTH = "l";
+  public static final String NUM_MESSAGE_COUNT = "c";
+  /** See {@link OmtpConstants#CONTENT_TYPE_VALUES} */
+  public static final String CONTENT_TYPE = "t";
+
+  public static final String SENDER = "s";
+  public static final String TIME = "dt";
+
+  /**
+   * SYNC message trigger events.
+   *
+   * <p>These are the possible values of {@link OmtpConstants#SYNC_TRIGGER_EVENT}.
+   */
+  public static final String NEW_MESSAGE = "NM";
+
+  public static final String MAILBOX_UPDATE = "MBU";
+  public static final String GREETINGS_UPDATE = "GU";
+
+  public static final String[] SYNC_TRIGGER_EVENT_VALUES = {
+    NEW_MESSAGE, MAILBOX_UPDATE, GREETINGS_UPDATE
+  };
+
+  /**
+   * Content types supported by OMTP VVM.
+   *
+   * <p>These are the possible values of {@link OmtpConstants#CONTENT_TYPE}.
+   */
+  public static final String VOICE = "v";
+
+  public static final String VIDEO = "o";
+  public static final String FAX = "f";
+  /** Voice message deposited by an external application */
+  public static final String INFOTAINMENT = "i";
+  /** Empty Call Capture - i.e. voicemail with no voice message. */
+  public static final String ECC = "e";
+
+  public static final String[] CONTENT_TYPE_VALUES = {VOICE, VIDEO, FAX, INFOTAINMENT, ECC};
+
+  ////////////////////////////// Status SMS fields ////////////////////////////
+
+  /**
+   * Status SMS fields.
+   *
+   * <p>Each string constant is the field's key in the SMS body which is used by the parser to
+   * identify the field's value, if present, in the SMS body.
+   */
+  /** See {@link OmtpConstants#PROVISIONING_STATUS_VALUES} */
+  public static final String PROVISIONING_STATUS = "st";
+  /** See {@link OmtpConstants#RETURN_CODE_VALUES} */
+  public static final String RETURN_CODE = "rc";
+  /** URL to send users to for activation VVM */
+  public static final String SUBSCRIPTION_URL = "rs";
+  /** IMAP4/SMTP server IP address or fully qualified domain name */
+  public static final String SERVER_ADDRESS = "srv";
+  /** Phone number to access voicemails through Telephony User Interface */
+  public static final String TUI_ACCESS_NUMBER = "tui";
+
+  public static final String TUI_PASSWORD_LENGTH = "pw_len";
+  /** Number to send client origination SMS */
+  public static final String CLIENT_SMS_DESTINATION_NUMBER = "dn";
+
+  public static final String IMAP_PORT = "ipt";
+  public static final String IMAP_USER_NAME = "u";
+  public static final String IMAP_PASSWORD = "pw";
+  public static final String SMTP_PORT = "spt";
+  public static final String SMTP_USER_NAME = "smtp_u";
+  public static final String SMTP_PASSWORD = "smtp_pw";
+
+  /**
+   * User provisioning status values.
+   *
+   * <p>Referred by {@link OmtpConstants#PROVISIONING_STATUS}.
+   */
+  public static final String SUBSCRIBER_NEW = "N";
+
+  public static final String SUBSCRIBER_READY = "R";
+  public static final String SUBSCRIBER_PROVISIONED = "P";
+  public static final String SUBSCRIBER_UNKNOWN = "U";
+  public static final String SUBSCRIBER_BLOCKED = "B";
+
+  public static final String[] PROVISIONING_STATUS_VALUES = {
+    SUBSCRIBER_NEW, SUBSCRIBER_READY, SUBSCRIBER_PROVISIONED, SUBSCRIBER_UNKNOWN, SUBSCRIBER_BLOCKED
+  };
+
+  /**
+   * The return code included in a status message.
+   *
+   * <p>These are the possible values of {@link OmtpConstants#RETURN_CODE}.
+   */
+  public static final String SUCCESS = "0";
+
+  public static final String SYSTEM_ERROR = "1";
+  public static final String SUBSCRIBER_ERROR = "2";
+  public static final String MAILBOX_UNKNOWN = "3";
+  public static final String VVM_NOT_ACTIVATED = "4";
+  public static final String VVM_NOT_PROVISIONED = "5";
+  public static final String VVM_CLIENT_UKNOWN = "6";
+  public static final String VVM_MAILBOX_NOT_INITIALIZED = "7";
+
+  public static final String[] RETURN_CODE_VALUES = {
+    SUCCESS,
+    SYSTEM_ERROR,
+    SUBSCRIBER_ERROR,
+    MAILBOX_UNKNOWN,
+    VVM_NOT_ACTIVATED,
+    VVM_NOT_PROVISIONED,
+    VVM_CLIENT_UKNOWN,
+    VVM_MAILBOX_NOT_INITIALIZED,
+  };
+
+  /** IMAP command extensions */
+
+  /**
+   * OMTP spec v1.3 2.3.1 Change password request syntax
+   *
+   * <p>This changes the PIN to access the Telephone User Interface, the traditional voicemail
+   * system.
+   */
+  public static final String IMAP_CHANGE_TUI_PWD_FORMAT = "XCHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
+
+  /**
+   * OMTP spec v1.3 2.4.1 Change languate request syntax
+   *
+   * <p>This changes the language in the Telephone User Interface.
+   */
+  public static final String IMAP_CHANGE_VM_LANG_FORMAT = "XCHANGE_VM_LANG LANG=%1$s";
+
+  /**
+   * OMTP spec v1.3 2.5.1 Close NUT Request syntax
+   *
+   * <p>This disables the new user tutorial, the message played to new users calling in the
+   * Telephone User Interface.
+   */
+  public static final String IMAP_CLOSE_NUT = "XCLOSE_NUT";
+
+  /** Possible NO responses for CHANGE_TUI_PWD */
+  public static final String RESPONSE_CHANGE_PIN_TOO_SHORT = "password too short";
+
+  public static final String RESPONSE_CHANGE_PIN_TOO_LONG = "password too long";
+  public static final String RESPONSE_CHANGE_PIN_TOO_WEAK = "password too weak";
+  public static final String RESPONSE_CHANGE_PIN_MISMATCH = "old password mismatch";
+  public static final String RESPONSE_CHANGE_PIN_INVALID_CHARACTER =
+      "password contains invalid characters";
+
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef(
+    value = {
+      CHANGE_PIN_SUCCESS,
+      CHANGE_PIN_TOO_SHORT,
+      CHANGE_PIN_TOO_LONG,
+      CHANGE_PIN_TOO_WEAK,
+      CHANGE_PIN_MISMATCH,
+      CHANGE_PIN_INVALID_CHARACTER,
+      CHANGE_PIN_SYSTEM_ERROR
+    }
+  )
+  public @interface ChangePinResult {}
+
+  public static final int CHANGE_PIN_SUCCESS = 0;
+  public static final int CHANGE_PIN_TOO_SHORT = 1;
+  public static final int CHANGE_PIN_TOO_LONG = 2;
+  public static final int CHANGE_PIN_TOO_WEAK = 3;
+  public static final int CHANGE_PIN_MISMATCH = 4;
+  public static final int CHANGE_PIN_INVALID_CHARACTER = 5;
+  public static final int CHANGE_PIN_SYSTEM_ERROR = 6;
+
+  /** Indicates the client is Google visual voicemail version 1.0. */
+  public static final String CLIENT_TYPE_GOOGLE_10 = "google.vvm.10";
+}
diff --git a/java/com/android/voicemail/impl/OmtpEvents.java b/java/com/android/voicemail/impl/OmtpEvents.java
new file mode 100644
index 0000000..6807edc
--- /dev/null
+++ b/java/com/android/voicemail/impl/OmtpEvents.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Events internal to the OMTP client. These should be translated into {@link
+ * android.provider.VoicemailContract.Status} error codes before writing into the voicemail status
+ * table.
+ */
+public enum OmtpEvents {
+
+  // Configuration State
+  CONFIG_REQUEST_STATUS_SUCCESS(Type.CONFIGURATION, true),
+
+  CONFIG_PIN_SET(Type.CONFIGURATION, true),
+  // The voicemail PIN is replaced with a generated PIN, user should change it.
+  CONFIG_DEFAULT_PIN_REPLACED(Type.CONFIGURATION, true),
+  CONFIG_ACTIVATING(Type.CONFIGURATION, true),
+  // There are already activation records, this is only a book-keeping activation.
+  CONFIG_ACTIVATING_SUBSEQUENT(Type.CONFIGURATION, true),
+  CONFIG_STATUS_SMS_TIME_OUT(Type.CONFIGURATION),
+  CONFIG_SERVICE_NOT_AVAILABLE(Type.CONFIGURATION),
+
+  // Data channel State
+
+  // A new sync has started, old errors in data channel should be cleared.
+  DATA_IMAP_OPERATION_STARTED(Type.DATA_CHANNEL, true),
+  // Successfully downloaded/uploaded data from the server, which means the data channel is clear.
+  DATA_IMAP_OPERATION_COMPLETED(Type.DATA_CHANNEL, true),
+  // The port provided in the STATUS SMS is invalid.
+  DATA_INVALID_PORT(Type.DATA_CHANNEL),
+  // No connection to the internet, and the carrier requires cellular data
+  DATA_NO_CONNECTION_CELLULAR_REQUIRED(Type.DATA_CHANNEL),
+  // No connection to the internet.
+  DATA_NO_CONNECTION(Type.DATA_CHANNEL),
+  // Address lookup for the server hostname failed. DNS error?
+  DATA_CANNOT_RESOLVE_HOST_ON_NETWORK(Type.DATA_CHANNEL),
+  // All destination address that resolves to the server hostname are rejected or timed out
+  DATA_ALL_SOCKET_CONNECTION_FAILED(Type.DATA_CHANNEL),
+  // Failed to establish SSL with the server, either with a direct SSL connection or by
+  // STARTTLS command
+  DATA_CANNOT_ESTABLISH_SSL_SESSION(Type.DATA_CHANNEL),
+  // Identity of the server cannot be verified.
+  DATA_SSL_INVALID_HOST_NAME(Type.DATA_CHANNEL),
+  // The server rejected our username/password
+  DATA_BAD_IMAP_CREDENTIAL(Type.DATA_CHANNEL),
+
+  DATA_AUTH_UNKNOWN_USER(Type.DATA_CHANNEL),
+  DATA_AUTH_UNKNOWN_DEVICE(Type.DATA_CHANNEL),
+  DATA_AUTH_INVALID_PASSWORD(Type.DATA_CHANNEL),
+  DATA_AUTH_MAILBOX_NOT_INITIALIZED(Type.DATA_CHANNEL),
+  DATA_AUTH_SERVICE_NOT_PROVISIONED(Type.DATA_CHANNEL),
+  DATA_AUTH_SERVICE_NOT_ACTIVATED(Type.DATA_CHANNEL),
+  DATA_AUTH_USER_IS_BLOCKED(Type.DATA_CHANNEL),
+
+  // A command to the server didn't result with an "OK" or continuation request
+  DATA_REJECTED_SERVER_RESPONSE(Type.DATA_CHANNEL),
+  // The server did not greet us with a "OK", possibly not a IMAP server.
+  DATA_INVALID_INITIAL_SERVER_RESPONSE(Type.DATA_CHANNEL),
+  // An IOException occurred while trying to open an ImapConnection
+  // TODO: reduce scope
+  DATA_IOE_ON_OPEN(Type.DATA_CHANNEL),
+  // The SELECT command on a mailbox is rejected
+  DATA_MAILBOX_OPEN_FAILED(Type.DATA_CHANNEL),
+  // An IOException has occurred
+  // TODO: reduce scope
+  DATA_GENERIC_IMAP_IOE(Type.DATA_CHANNEL),
+  // An SslException has occurred while opening an ImapConnection
+  // TODO: reduce scope
+  DATA_SSL_EXCEPTION(Type.DATA_CHANNEL),
+
+  // Notification Channel
+
+  // Cell signal restored, can received VVM SMSs
+  NOTIFICATION_IN_SERVICE(Type.NOTIFICATION_CHANNEL, true),
+  // Cell signal lost, cannot received VVM SMSs
+  NOTIFICATION_SERVICE_LOST(Type.NOTIFICATION_CHANNEL, false),
+
+  // Other
+  OTHER_SOURCE_REMOVED(Type.OTHER, false),
+
+  // VVM3
+  VVM3_NEW_USER_SETUP_FAILED,
+  // Table 4. client internal error handling
+  VVM3_VMG_DNS_FAILURE,
+  VVM3_SPG_DNS_FAILURE,
+  VVM3_VMG_CONNECTION_FAILED,
+  VVM3_SPG_CONNECTION_FAILED,
+  VVM3_VMG_TIMEOUT,
+  VVM3_STATUS_SMS_TIMEOUT,
+
+  VVM3_SUBSCRIBER_PROVISIONED,
+  VVM3_SUBSCRIBER_BLOCKED,
+  VVM3_SUBSCRIBER_UNKNOWN;
+
+  public static class Type {
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({CONFIGURATION, DATA_CHANNEL, NOTIFICATION_CHANNEL, OTHER})
+    public @interface Values {}
+
+    public static final int CONFIGURATION = 1;
+    public static final int DATA_CHANNEL = 2;
+    public static final int NOTIFICATION_CHANNEL = 3;
+    public static final int OTHER = 4;
+  }
+
+  private final int mType;
+  private final boolean mIsSuccess;
+
+  OmtpEvents(int type, boolean isSuccess) {
+    mType = type;
+    mIsSuccess = isSuccess;
+  }
+
+  OmtpEvents(int type) {
+    mType = type;
+    mIsSuccess = false;
+  }
+
+  OmtpEvents() {
+    mType = Type.OTHER;
+    mIsSuccess = false;
+  }
+
+  @Type.Values
+  public int getType() {
+    return mType;
+  }
+
+  public boolean isSuccess() {
+    return mIsSuccess;
+  }
+}
diff --git a/java/com/android/voicemail/impl/OmtpService.java b/java/com/android/voicemail/impl/OmtpService.java
new file mode 100644
index 0000000..dfbd4cf
--- /dev/null
+++ b/java/com/android/voicemail/impl/OmtpService.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.VisualVoicemailService;
+import android.telephony.VisualVoicemailSms;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+
+public class OmtpService extends VisualVoicemailService {
+
+  private static final String TAG = "VvmOmtpService";
+
+  public static final String ACTION_SMS_RECEIVED = "com.android.vociemailomtp.sms.sms_received";
+
+  public static final String EXTRA_VOICEMAIL_SMS = "extra_voicemail_sms";
+
+  @Override
+  public void onCellServiceConnected(
+      VisualVoicemailTask task, final PhoneAccountHandle phoneAccountHandle) {
+    VvmLog.i(TAG, "onCellServiceConnected");
+    ActivationTask.start(OmtpService.this, phoneAccountHandle, null);
+    task.finish();
+  }
+
+  @Override
+  public void onSmsReceived(VisualVoicemailTask task, final VisualVoicemailSms sms) {
+    VvmLog.i(TAG, "onSmsReceived");
+    Intent intent = new Intent(ACTION_SMS_RECEIVED);
+    intent.setPackage(getPackageName());
+    intent.putExtra(EXTRA_VOICEMAIL_SMS, sms);
+    sendBroadcast(intent);
+    task.finish();
+  }
+
+  @Override
+  public void onSimRemoved(
+      final VisualVoicemailTask task, final PhoneAccountHandle phoneAccountHandle) {
+    VvmLog.i(TAG, "onSimRemoved");
+    VvmAccountManager.removeAccount(this, phoneAccountHandle);
+    task.finish();
+  }
+
+  @Override
+  public void onStopped(VisualVoicemailTask task) {
+    VvmLog.i(TAG, "onStopped");
+  }
+}
diff --git a/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java b/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java
new file mode 100644
index 0000000..0296d20
--- /dev/null
+++ b/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.TelephonyManager;
+import android.telephony.VisualVoicemailService;
+import android.telephony.VisualVoicemailSmsFilterSettings;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.impl.protocol.VisualVoicemailProtocol;
+import com.android.voicemail.impl.protocol.VisualVoicemailProtocolFactory;
+import com.android.voicemail.impl.sms.StatusMessage;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Manages carrier dependent visual voicemail configuration values. The primary source is the value
+ * retrieved from CarrierConfigManager. If CarrierConfigManager does not provide the config
+ * (KEY_VVM_TYPE_STRING is empty, or "hidden" configs), then the value hardcoded in telephony will
+ * be used (in res/xml/vvm_config.xml)
+ *
+ * <p>Hidden configs are new configs that are planned for future APIs, or miscellaneous settings
+ * that may clutter CarrierConfigManager too much.
+ *
+ * <p>The current hidden configs are: {@link #getSslPort()} {@link #getDisabledCapabilities()}
+ */
+public class OmtpVvmCarrierConfigHelper {
+
+  private static final String TAG = "OmtpVvmCarrierCfgHlpr";
+
+  static final String KEY_VVM_TYPE_STRING = CarrierConfigManager.KEY_VVM_TYPE_STRING;
+  static final String KEY_VVM_DESTINATION_NUMBER_STRING =
+      CarrierConfigManager.KEY_VVM_DESTINATION_NUMBER_STRING;
+  static final String KEY_VVM_PORT_NUMBER_INT = CarrierConfigManager.KEY_VVM_PORT_NUMBER_INT;
+  static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING =
+      CarrierConfigManager.KEY_CARRIER_VVM_PACKAGE_NAME_STRING;
+  static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY =
+      "carrier_vvm_package_name_string_array";
+  static final String KEY_VVM_PREFETCH_BOOL = CarrierConfigManager.KEY_VVM_PREFETCH_BOOL;
+  static final String KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL =
+      CarrierConfigManager.KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL;
+
+  /** @see #getSslPort() */
+  static final String KEY_VVM_SSL_PORT_NUMBER_INT = "vvm_ssl_port_number_int";
+
+  /** @see #isLegacyModeEnabled() */
+  static final String KEY_VVM_LEGACY_MODE_ENABLED_BOOL = "vvm_legacy_mode_enabled_bool";
+
+  /**
+   * Ban a capability reported by the server from being used. The array of string should be a subset
+   * of the capabilities returned IMAP CAPABILITY command.
+   *
+   * @see #getDisabledCapabilities()
+   */
+  static final String KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY =
+      "vvm_disabled_capabilities_string_array";
+
+  static final String KEY_VVM_CLIENT_PREFIX_STRING = "vvm_client_prefix_string";
+
+  private final Context mContext;
+  private final PersistableBundle mCarrierConfig;
+  private final String mVvmType;
+  private final VisualVoicemailProtocol mProtocol;
+  private final PersistableBundle mTelephonyConfig;
+
+  private PhoneAccountHandle mPhoneAccountHandle;
+
+  public OmtpVvmCarrierConfigHelper(Context context, PhoneAccountHandle handle) {
+    mContext = context;
+    mPhoneAccountHandle = handle;
+    TelephonyManager telephonyManager =
+        context
+            .getSystemService(TelephonyManager.class)
+            .createForPhoneAccountHandle(mPhoneAccountHandle);
+    if (telephonyManager == null) {
+      VvmLog.e(TAG, "PhoneAccountHandle is invalid");
+      mCarrierConfig = null;
+      mTelephonyConfig = null;
+      mVvmType = null;
+      mProtocol = null;
+      return;
+    }
+
+    mCarrierConfig = getCarrierConfig(telephonyManager);
+    mTelephonyConfig =
+        new TelephonyVvmConfigManager(context.getResources())
+            .getConfig(telephonyManager.getSimOperator());
+
+    mVvmType = getVvmType();
+    mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
+  }
+
+  @VisibleForTesting
+  OmtpVvmCarrierConfigHelper(
+      Context context, PersistableBundle carrierConfig, PersistableBundle telephonyConfig) {
+    mContext = context;
+    mCarrierConfig = carrierConfig;
+    mTelephonyConfig = telephonyConfig;
+    mVvmType = getVvmType();
+    mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
+  }
+
+  public Context getContext() {
+    return mContext;
+  }
+
+  @Nullable
+  public PhoneAccountHandle getPhoneAccountHandle() {
+    return mPhoneAccountHandle;
+  }
+
+  /**
+   * return whether the carrier's visual voicemail is supported, with KEY_VVM_TYPE_STRING set as a
+   * known protocol.
+   */
+  public boolean isValid() {
+    return mProtocol != null;
+  }
+
+  @Nullable
+  public String getVvmType() {
+    return (String) getValue(KEY_VVM_TYPE_STRING);
+  }
+
+  @Nullable
+  public VisualVoicemailProtocol getProtocol() {
+    return mProtocol;
+  }
+
+  /** @returns arbitrary String stored in the config file. Used for protocol specific values. */
+  @Nullable
+  public String getString(String key) {
+    Assert.checkArgument(isValid());
+    return (String) getValue(key);
+  }
+
+  @Nullable
+  public Set<String> getCarrierVvmPackageNames() {
+    Assert.checkArgument(isValid());
+    Set<String> names = getCarrierVvmPackageNames(mCarrierConfig);
+    if (names != null) {
+      return names;
+    }
+    return getCarrierVvmPackageNames(mTelephonyConfig);
+  }
+
+  private static Set<String> getCarrierVvmPackageNames(@Nullable PersistableBundle bundle) {
+    if (bundle == null) {
+      return null;
+    }
+    Set<String> names = new ArraySet<>();
+    if (bundle.containsKey(KEY_CARRIER_VVM_PACKAGE_NAME_STRING)) {
+      names.add(bundle.getString(KEY_CARRIER_VVM_PACKAGE_NAME_STRING));
+    }
+    if (bundle.containsKey(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY)) {
+      String[] vvmPackages = bundle.getStringArray(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY);
+      if (vvmPackages != null && vvmPackages.length > 0) {
+        Collections.addAll(names, vvmPackages);
+      }
+    }
+    if (names.isEmpty()) {
+      return null;
+    }
+    return names;
+  }
+
+  /**
+   * For checking upon sim insertion whether visual voicemail should be enabled. This method does so
+   * by checking if the carrier's voicemail app is installed.
+   */
+  public boolean isEnabledByDefault() {
+    if (!isValid()) {
+      return false;
+    }
+
+    Set<String> carrierPackages = getCarrierVvmPackageNames();
+    if (carrierPackages == null) {
+      return true;
+    }
+    for (String packageName : carrierPackages) {
+      try {
+        mContext.getPackageManager().getPackageInfo(packageName, 0);
+        return false;
+      } catch (NameNotFoundException e) {
+        // Do nothing.
+      }
+    }
+    return true;
+  }
+
+  public boolean isCellularDataRequired() {
+    Assert.checkArgument(isValid());
+    return (boolean) getValue(KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL, false);
+  }
+
+  public boolean isPrefetchEnabled() {
+    Assert.checkArgument(isValid());
+    return (boolean) getValue(KEY_VVM_PREFETCH_BOOL, true);
+  }
+
+  public int getApplicationPort() {
+    Assert.checkArgument(isValid());
+    return (int) getValue(KEY_VVM_PORT_NUMBER_INT, 0);
+  }
+
+  @Nullable
+  public String getDestinationNumber() {
+    Assert.checkArgument(isValid());
+    return (String) getValue(KEY_VVM_DESTINATION_NUMBER_STRING);
+  }
+
+  /**
+   * @return Port to start a SSL IMAP connection directly.
+   */
+  public int getSslPort() {
+    Assert.checkArgument(isValid());
+    return (int) getValue(KEY_VVM_SSL_PORT_NUMBER_INT, 0);
+  }
+
+  /**
+   * Hidden Config.
+   *
+   * <p>Sometimes the server states it supports a certain feature but we found they have bug on the
+   * server side. For example, in b/28717550 the server reported AUTH=DIGEST-MD5 capability but
+   * using it to login will cause subsequent response to be erroneous.
+   *
+   * @return A set of capabilities that is reported by the IMAP CAPABILITY command, but determined
+   *     to have issues and should not be used.
+   */
+  @Nullable
+  public Set<String> getDisabledCapabilities() {
+    Assert.checkArgument(isValid());
+    Set<String> disabledCapabilities = getDisabledCapabilities(mCarrierConfig);
+    if (disabledCapabilities != null) {
+      return disabledCapabilities;
+    }
+    return getDisabledCapabilities(mTelephonyConfig);
+  }
+
+  @Nullable
+  private static Set<String> getDisabledCapabilities(@Nullable PersistableBundle bundle) {
+    if (bundle == null) {
+      return null;
+    }
+    if (!bundle.containsKey(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY)) {
+      return null;
+    }
+    String[] disabledCapabilities =
+        bundle.getStringArray(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY);
+    if (disabledCapabilities != null && disabledCapabilities.length > 0) {
+      ArraySet<String> result = new ArraySet<>();
+      Collections.addAll(result, disabledCapabilities);
+      return result;
+    }
+    return null;
+  }
+
+  public String getClientPrefix() {
+    Assert.checkArgument(isValid());
+    String prefix = (String) getValue(KEY_VVM_CLIENT_PREFIX_STRING);
+    if (prefix != null) {
+      return prefix;
+    }
+    return "//VVM";
+  }
+
+  /**
+   * Should legacy mode be used when the OMTP VVM client is disabled?
+   *
+   * <p>Legacy mode is a mode that on the carrier side visual voicemail is still activated, but on
+   * the client side all network operations are disabled. SMSs are still monitored so a new message
+   * SYNC SMS will be translated to show a message waiting indicator, like traditional voicemails.
+   *
+   * <p>This is for carriers that does not support VVM deactivation so voicemail can continue to
+   * function without the data cost.
+   */
+  public boolean isLegacyModeEnabled() {
+    Assert.checkArgument(isValid());
+    return (boolean) getValue(KEY_VVM_LEGACY_MODE_ENABLED_BOOL, false);
+  }
+
+  public void startActivation() {
+    Assert.checkArgument(isValid());
+    PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
+    if (phoneAccountHandle == null) {
+      // This should never happen
+      // Error logged in getPhoneAccountHandle().
+      return;
+    }
+
+    if (mVvmType == null || mVvmType.isEmpty()) {
+      // The VVM type is invalid; we should never have gotten here in the first place since
+      // this is loaded initially in the constructor, and callers should check isValid()
+      // before trying to start activation anyways.
+      VvmLog.e(TAG, "startActivation : vvmType is null or empty for account " + phoneAccountHandle);
+      return;
+    }
+
+    if (mProtocol != null) {
+      ActivationTask.start(mContext, mPhoneAccountHandle, null);
+    }
+  }
+
+  public void activateSmsFilter() {
+    Assert.checkArgument(isValid());
+    VisualVoicemailService.setSmsFilterSettings(
+        mContext,
+        getPhoneAccountHandle(),
+        new VisualVoicemailSmsFilterSettings.Builder().setClientPrefix(getClientPrefix()).build());
+  }
+
+  public void startDeactivation() {
+    Assert.checkArgument(isValid());
+    if (!isLegacyModeEnabled()) {
+      // SMS should still be filtered in legacy mode
+      VisualVoicemailService.setSmsFilterSettings(mContext, getPhoneAccountHandle(), null);
+    }
+    if (mProtocol != null) {
+      mProtocol.startDeactivation(this);
+    }
+    VvmAccountManager.removeAccount(mContext, getPhoneAccountHandle());
+  }
+
+  public boolean supportsProvisioning() {
+    Assert.checkArgument(isValid());
+    return mProtocol.supportsProvisioning();
+  }
+
+  public void startProvisioning(
+      ActivationTask task,
+      PhoneAccountHandle phone,
+      VoicemailStatus.Editor status,
+      StatusMessage message,
+      Bundle data) {
+    Assert.checkArgument(isValid());
+    mProtocol.startProvisioning(task, phone, this, status, message, data);
+  }
+
+  public void requestStatus(@Nullable PendingIntent sentIntent) {
+    Assert.checkArgument(isValid());
+    mProtocol.requestStatus(this, sentIntent);
+  }
+
+  public void handleEvent(VoicemailStatus.Editor status, OmtpEvents event) {
+    Assert.checkArgument(isValid());
+    VvmLog.i(TAG, "OmtpEvent:" + event);
+    mProtocol.handleEvent(mContext, this, status, event);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder builder = new StringBuilder("OmtpVvmCarrierConfigHelper [");
+    builder
+        .append("phoneAccountHandle: ")
+        .append(mPhoneAccountHandle)
+        .append(", carrierConfig: ")
+        .append(mCarrierConfig != null)
+        .append(", telephonyConfig: ")
+        .append(mTelephonyConfig != null)
+        .append(", type: ")
+        .append(getVvmType())
+        .append(", destinationNumber: ")
+        .append(getDestinationNumber())
+        .append(", applicationPort: ")
+        .append(getApplicationPort())
+        .append(", sslPort: ")
+        .append(getSslPort())
+        .append(", isEnabledByDefault: ")
+        .append(isEnabledByDefault())
+        .append(", isCellularDataRequired: ")
+        .append(isCellularDataRequired())
+        .append(", isPrefetchEnabled: ")
+        .append(isPrefetchEnabled())
+        .append(", isLegacyModeEnabled: ")
+        .append(isLegacyModeEnabled())
+        .append("]");
+    return builder.toString();
+  }
+
+  @Nullable
+  private PersistableBundle getCarrierConfig(@NonNull TelephonyManager telephonyManager) {
+    CarrierConfigManager carrierConfigManager =
+        (CarrierConfigManager) mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+    if (carrierConfigManager == null) {
+      VvmLog.w(TAG, "No carrier config service found.");
+      return null;
+    }
+
+    PersistableBundle config = telephonyManager.getCarrierConfig();
+
+    if (TextUtils.isEmpty(config.getString(CarrierConfigManager.KEY_VVM_TYPE_STRING))) {
+      return null;
+    }
+    return config;
+  }
+
+  @Nullable
+  private Object getValue(String key) {
+    return getValue(key, null);
+  }
+
+  @Nullable
+  private Object getValue(String key, Object defaultValue) {
+    Object result;
+    if (mCarrierConfig != null) {
+      result = mCarrierConfig.get(key);
+      if (result != null) {
+        return result;
+      }
+    }
+    if (mTelephonyConfig != null) {
+      result = mTelephonyConfig.get(key);
+      if (result != null) {
+        return result;
+      }
+    }
+    return defaultValue;
+  }
+}
diff --git a/java/com/android/voicemail/impl/SubscriptionInfoHelper.java b/java/com/android/voicemail/impl/SubscriptionInfoHelper.java
new file mode 100644
index 0000000..d8a8423
--- /dev/null
+++ b/java/com/android/voicemail/impl/SubscriptionInfoHelper.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl;
+
+import android.app.ActionBar;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.text.TextUtils;
+
+/**
+ * Helper for manipulating intents or components with subscription-related information.
+ *
+ * <p>In settings, subscription ids and labels are passed along to indicate that settings are being
+ * changed for particular subscriptions. This helper provides functions for helping extract this
+ * info and perform common operations using this info.
+ */
+public class SubscriptionInfoHelper {
+  public static final int NO_SUB_ID = -1;
+
+  // Extra on intent containing the id of a subscription.
+  public static final String SUB_ID_EXTRA =
+      "com.android.voicemailomtp.settings.SubscriptionInfoHelper.SubscriptionId";
+  // Extra on intent containing the label of a subscription.
+  private static final String SUB_LABEL_EXTRA =
+      "com.android.voicemailomtp.settings.SubscriptionInfoHelper.SubscriptionLabel";
+
+  private static Context mContext;
+
+  private static int mSubId = NO_SUB_ID;
+  private static String mSubLabel;
+
+  /** Instantiates the helper, by extracting the subscription id and label from the intent. */
+  public SubscriptionInfoHelper(Context context, Intent intent) {
+    mContext = context;
+    mSubId = intent.getIntExtra(SUB_ID_EXTRA, NO_SUB_ID);
+    mSubLabel = intent.getStringExtra(SUB_LABEL_EXTRA);
+  }
+
+  /**
+   * Sets the action bar title to the string specified by the given resource id, formatting it with
+   * the subscription label. This assumes the resource string is formattable with a string-type
+   * specifier.
+   *
+   * <p>If the subscription label does not exists, leave the existing title.
+   */
+  public void setActionBarTitle(ActionBar actionBar, Resources res, int resId) {
+    if (actionBar == null || TextUtils.isEmpty(mSubLabel)) {
+      return;
+    }
+
+    String title = String.format(res.getString(resId), mSubLabel);
+    actionBar.setTitle(title);
+  }
+
+  public int getSubId() {
+    return mSubId;
+  }
+}
diff --git a/java/com/android/voicemail/impl/TelephonyManagerStub.java b/java/com/android/voicemail/impl/TelephonyManagerStub.java
new file mode 100644
index 0000000..4762e90
--- /dev/null
+++ b/java/com/android/voicemail/impl/TelephonyManagerStub.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.annotation.TargetApi;
+import android.os.Build.VERSION_CODES;
+
+/**
+ * Temporary stub for public APIs that should be added into telephony manager.
+ *
+ * <p>TODO(b/32637799) remove this.
+ */
+@TargetApi(VERSION_CODES.O)
+public class TelephonyManagerStub {
+
+  public static void showVoicemailNotification(int voicemailCount) {}
+
+  /**
+   * Dismisses the message waiting (voicemail) indicator.
+   *
+   * @param subId the subscription id we should dismiss the notification for.
+   */
+  public static void clearMwiIndicator(int subId) {}
+
+  public static void setShouldCheckVisualVoicemailConfigurationForMwi(int subId, boolean enabled) {}
+}
diff --git a/java/com/android/voicemail/impl/TelephonyMangerCompat.java b/java/com/android/voicemail/impl/TelephonyMangerCompat.java
new file mode 100644
index 0000000..353cd69
--- /dev/null
+++ b/java/com/android/voicemail/impl/TelephonyMangerCompat.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import java.lang.reflect.Method;
+
+/** Handles {@link TelephonyManager} API changes in experimental SDK */
+public class TelephonyMangerCompat {
+
+  private static final String GET_VISUAL_VOICEMAIL_PACKGE_NAME = "getVisualVoicemailPackageName";
+
+  /**
+   * Changed from getVisualVoicemailPackageName(PhoneAccountHandle) to
+   * getVisualVoicemailPackageName()
+   */
+  public static String getVisualVoicemailPackageName(TelephonyManager telephonyManager) {
+    try {
+      Method method = TelephonyManager.class.getMethod(GET_VISUAL_VOICEMAIL_PACKGE_NAME);
+      try {
+        return (String) method.invoke(telephonyManager);
+      } catch (ReflectiveOperationException e) {
+        throw new RuntimeException(e);
+      }
+    } catch (NoSuchMethodException e) {
+      // Do nothing, try the next version.
+    }
+
+    try {
+      Method method =
+          TelephonyManager.class.getMethod(
+              GET_VISUAL_VOICEMAIL_PACKGE_NAME, PhoneAccountHandle.class);
+      try {
+        return (String) method.invoke(telephonyManager, (Object) null);
+      } catch (ReflectiveOperationException e) {
+        throw new RuntimeException(e);
+      }
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/TelephonyVvmConfigManager.java b/java/com/android/voicemail/impl/TelephonyVvmConfigManager.java
new file mode 100644
index 0000000..04012c9
--- /dev/null
+++ b/java/com/android/voicemail/impl/TelephonyVvmConfigManager.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.content.res.Resources;
+import android.os.PersistableBundle;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArrayMap;
+import com.android.voicemail.impl.utils.XmlUtils;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Map.Entry;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/** Load and caches telephony vvm config from res/xml/vvm_config.xml */
+public class TelephonyVvmConfigManager {
+
+  private static final String TAG = "TelephonyVvmCfgMgr";
+
+  private static final boolean USE_DEBUG_CONFIG = false;
+
+  private static final String TAG_PERSISTABLEMAP = "pbundle_as_map";
+
+  static final String KEY_MCCMNC = "mccmnc";
+
+  private static Map<String, PersistableBundle> sCachedConfigs;
+
+  private final Map<String, PersistableBundle> mConfigs;
+
+  public TelephonyVvmConfigManager(Resources resources) {
+    if (sCachedConfigs == null) {
+      sCachedConfigs = loadConfigs(resources.getXml(R.xml.vvm_config));
+    }
+    mConfigs = sCachedConfigs;
+  }
+
+  @VisibleForTesting
+  TelephonyVvmConfigManager(XmlPullParser parser) {
+    mConfigs = loadConfigs(parser);
+  }
+
+  @Nullable
+  public PersistableBundle getConfig(String mccMnc) {
+    if (USE_DEBUG_CONFIG) {
+      return mConfigs.get("TEST");
+    }
+    return mConfigs.get(mccMnc);
+  }
+
+  private static Map<String, PersistableBundle> loadConfigs(XmlPullParser parser) {
+    Map<String, PersistableBundle> configs = new ArrayMap<>();
+    try {
+      ArrayList list = readBundleList(parser);
+      for (Object object : list) {
+        if (!(object instanceof PersistableBundle)) {
+          throw new IllegalArgumentException("PersistableBundle expected, got " + object);
+        }
+        PersistableBundle bundle = (PersistableBundle) object;
+        String[] mccMncs = bundle.getStringArray(KEY_MCCMNC);
+        if (mccMncs == null) {
+          throw new IllegalArgumentException("MCCMNC is null");
+        }
+        for (String mccMnc : mccMncs) {
+          configs.put(mccMnc, bundle);
+        }
+      }
+    } catch (IOException | XmlPullParserException e) {
+      throw new RuntimeException(e);
+    }
+    return configs;
+  }
+
+  @Nullable
+  public static ArrayList readBundleList(XmlPullParser in)
+      throws IOException, XmlPullParserException {
+    final int outerDepth = in.getDepth();
+    int event;
+    while (((event = in.next()) != XmlPullParser.END_DOCUMENT)
+        && (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
+      if (event == XmlPullParser.START_TAG) {
+        final String startTag = in.getName();
+        final String[] tagName = new String[1];
+        in.next();
+        return XmlUtils.readThisListXml(in, startTag, tagName, new MyReadMapCallback(), false);
+      }
+    }
+    return null;
+  }
+
+  public static PersistableBundle restoreFromXml(XmlPullParser in)
+      throws IOException, XmlPullParserException {
+    final int outerDepth = in.getDepth();
+    final String startTag = in.getName();
+    final String[] tagName = new String[1];
+    int event;
+    while (((event = in.next()) != XmlPullParser.END_DOCUMENT)
+        && (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
+      if (event == XmlPullParser.START_TAG) {
+        ArrayMap<String, ?> map =
+            XmlUtils.readThisArrayMapXml(in, startTag, tagName, new MyReadMapCallback());
+        PersistableBundle result = new PersistableBundle();
+        for (Entry<String, ?> entry : map.entrySet()) {
+          Object value = entry.getValue();
+          if (value instanceof Integer) {
+            result.putInt(entry.getKey(), (int) value);
+          } else if (value instanceof Boolean) {
+            result.putBoolean(entry.getKey(), (boolean) value);
+          } else if (value instanceof String) {
+            result.putString(entry.getKey(), (String) value);
+          } else if (value instanceof String[]) {
+            result.putStringArray(entry.getKey(), (String[]) value);
+          } else if (value instanceof PersistableBundle) {
+            result.putPersistableBundle(entry.getKey(), (PersistableBundle) value);
+          }
+        }
+        return result;
+      }
+    }
+    return PersistableBundle.EMPTY;
+  }
+
+  static class MyReadMapCallback implements XmlUtils.ReadMapCallback {
+
+    @Override
+    public Object readThisUnknownObjectXml(XmlPullParser in, String tag)
+        throws XmlPullParserException, IOException {
+      if (TAG_PERSISTABLEMAP.equals(tag)) {
+        return restoreFromXml(in);
+      }
+      throw new XmlPullParserException("Unknown tag=" + tag);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/VisualVoicemailPreferences.java b/java/com/android/voicemail/impl/VisualVoicemailPreferences.java
new file mode 100644
index 0000000..72506eb
--- /dev/null
+++ b/java/com/android/voicemail/impl/VisualVoicemailPreferences.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl;
+
+import android.content.Context;
+import android.preference.PreferenceManager;
+import android.telecom.PhoneAccountHandle;
+import com.android.dialer.common.PerAccountSharedPreferences;
+
+/**
+ * Save visual voicemail values in shared preferences to be retrieved later. Because a voicemail
+ * source is tied 1:1 to a phone account, the phone account handle is used in the key for each
+ * voicemail source and the associated data.
+ */
+public class VisualVoicemailPreferences extends PerAccountSharedPreferences {
+
+  public VisualVoicemailPreferences(Context context, PhoneAccountHandle phoneAccountHandle) {
+    super(
+        context,
+        phoneAccountHandle,
+        PreferenceManager.getDefaultSharedPreferences(context),
+        "visual_voicemail_");
+  }
+}
diff --git a/java/com/android/voicemail/impl/Voicemail.java b/java/com/android/voicemail/impl/Voicemail.java
new file mode 100644
index 0000000..f98d56f
--- /dev/null
+++ b/java/com/android/voicemail/impl/Voicemail.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl;
+
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+
+/** Represents a single voicemail stored in the voicemail content provider. */
+public class Voicemail implements Parcelable {
+
+  private final Long mTimestamp;
+  private final String mNumber;
+  private final PhoneAccountHandle mPhoneAccount;
+  private final Long mId;
+  private final Long mDuration;
+  private final String mSource;
+  private final String mProviderData;
+  private final Uri mUri;
+  private final Boolean mIsRead;
+  private final Boolean mHasContent;
+  private final String mTranscription;
+
+  private Voicemail(
+      Long timestamp,
+      String number,
+      PhoneAccountHandle phoneAccountHandle,
+      Long id,
+      Long duration,
+      String source,
+      String providerData,
+      Uri uri,
+      Boolean isRead,
+      Boolean hasContent,
+      String transcription) {
+    mTimestamp = timestamp;
+    mNumber = number;
+    mPhoneAccount = phoneAccountHandle;
+    mId = id;
+    mDuration = duration;
+    mSource = source;
+    mProviderData = providerData;
+    mUri = uri;
+    mIsRead = isRead;
+    mHasContent = hasContent;
+    mTranscription = transcription;
+  }
+
+  /**
+   * Create a {@link Builder} for a new {@link Voicemail} to be inserted.
+   *
+   * <p>The number and the timestamp are mandatory for insertion.
+   */
+  public static Builder createForInsertion(long timestamp, String number) {
+    return new Builder().setNumber(number).setTimestamp(timestamp);
+  }
+
+  /**
+   * Create a {@link Builder} for a {@link Voicemail} to be updated (or deleted).
+   *
+   * <p>The id and source data fields are mandatory for update - id is necessary for updating the
+   * database and source data is necessary for updating the server.
+   */
+  public static Builder createForUpdate(long id, String sourceData) {
+    return new Builder().setId(id).setSourceData(sourceData);
+  }
+
+  /**
+   * Builder pattern for creating a {@link Voicemail}. The builder must be created with the {@link
+   * #createForInsertion(long, String)} method.
+   *
+   * <p>This class is <b>not thread safe</b>
+   */
+  public static class Builder {
+
+    private Long mBuilderTimestamp;
+    private String mBuilderNumber;
+    private PhoneAccountHandle mBuilderPhoneAccount;
+    private Long mBuilderId;
+    private Long mBuilderDuration;
+    private String mBuilderSourcePackage;
+    private String mBuilderSourceData;
+    private Uri mBuilderUri;
+    private Boolean mBuilderIsRead;
+    private boolean mBuilderHasContent;
+    private String mBuilderTranscription;
+
+    /** You should use the correct factory method to construct a builder. */
+    private Builder() {}
+
+    public Builder setNumber(String number) {
+      mBuilderNumber = number;
+      return this;
+    }
+
+    public Builder setTimestamp(long timestamp) {
+      mBuilderTimestamp = timestamp;
+      return this;
+    }
+
+    public Builder setPhoneAccount(PhoneAccountHandle phoneAccount) {
+      mBuilderPhoneAccount = phoneAccount;
+      return this;
+    }
+
+    public Builder setId(long id) {
+      mBuilderId = id;
+      return this;
+    }
+
+    public Builder setDuration(long duration) {
+      mBuilderDuration = duration;
+      return this;
+    }
+
+    public Builder setSourcePackage(String sourcePackage) {
+      mBuilderSourcePackage = sourcePackage;
+      return this;
+    }
+
+    public Builder setSourceData(String sourceData) {
+      mBuilderSourceData = sourceData;
+      return this;
+    }
+
+    public Builder setUri(Uri uri) {
+      mBuilderUri = uri;
+      return this;
+    }
+
+    public Builder setIsRead(boolean isRead) {
+      mBuilderIsRead = isRead;
+      return this;
+    }
+
+    public Builder setHasContent(boolean hasContent) {
+      mBuilderHasContent = hasContent;
+      return this;
+    }
+
+    public Builder setTranscription(String transcription) {
+      mBuilderTranscription = transcription;
+      return this;
+    }
+
+    public Voicemail build() {
+      mBuilderId = mBuilderId == null ? -1 : mBuilderId;
+      mBuilderTimestamp = mBuilderTimestamp == null ? 0 : mBuilderTimestamp;
+      mBuilderDuration = mBuilderDuration == null ? 0 : mBuilderDuration;
+      mBuilderIsRead = mBuilderIsRead == null ? false : mBuilderIsRead;
+      return new Voicemail(
+          mBuilderTimestamp,
+          mBuilderNumber,
+          mBuilderPhoneAccount,
+          mBuilderId,
+          mBuilderDuration,
+          mBuilderSourcePackage,
+          mBuilderSourceData,
+          mBuilderUri,
+          mBuilderIsRead,
+          mBuilderHasContent,
+          mBuilderTranscription);
+    }
+  }
+
+  /**
+   * The identifier of the voicemail in the content provider.
+   *
+   * <p>This may be missing in the case of a new {@link Voicemail} that we plan to insert into the
+   * content provider, since until it has been inserted we don't know what id it should have. If
+   * none is specified, we return -1.
+   */
+  public long getId() {
+    return mId;
+  }
+
+  /** The number of the person leaving the voicemail, empty string if unknown, null if not set. */
+  public String getNumber() {
+    return mNumber;
+  }
+
+  /** The phone account associated with the voicemail, null if not set. */
+  public PhoneAccountHandle getPhoneAccount() {
+    return mPhoneAccount;
+  }
+
+  /** The timestamp the voicemail was received, in millis since the epoch, zero if not set. */
+  public long getTimestampMillis() {
+    return mTimestamp;
+  }
+
+  /** Gets the duration of the voicemail in millis, or zero if the field is not set. */
+  public long getDuration() {
+    return mDuration;
+  }
+
+  /**
+   * Returns the package name of the source that added this voicemail, or null if this field is not
+   * set.
+   */
+  public String getSourcePackage() {
+    return mSource;
+  }
+
+  /**
+   * Returns the application-specific data type stored with the voicemail, or null if this field is
+   * not set.
+   *
+   * <p>Source data is typically used as an identifier to uniquely identify the voicemail against
+   * the voicemail server. This is likely to be something like the IMAP UID, or some other
+   * server-generated identifying string.
+   */
+  public String getSourceData() {
+    return mProviderData;
+  }
+
+  /**
+   * Gets the Uri that can be used to refer to this voicemail, and to make it play.
+   *
+   * <p>Returns null if we don't know the Uri.
+   */
+  public Uri getUri() {
+    return mUri;
+  }
+
+  /**
+   * Tells us if the voicemail message has been marked as read.
+   *
+   * <p>Always returns false if this field has not been set, i.e. if hasRead() returns false.
+   */
+  public boolean isRead() {
+    return mIsRead;
+  }
+
+  /** Tells us if there is content stored at the Uri. */
+  public boolean hasContent() {
+    return mHasContent;
+  }
+
+  /** Returns the text transcription of this voicemail, or null if this field is not set. */
+  public String getTranscription() {
+    return mTranscription;
+  }
+
+  @Override
+  public int describeContents() {
+    return 0;
+  }
+
+  @Override
+  public void writeToParcel(Parcel dest, int flags) {
+    dest.writeLong(mTimestamp);
+    writeCharSequence(dest, mNumber);
+    if (mPhoneAccount == null) {
+      dest.writeInt(0);
+    } else {
+      dest.writeInt(1);
+      mPhoneAccount.writeToParcel(dest, flags);
+    }
+    dest.writeLong(mId);
+    dest.writeLong(mDuration);
+    writeCharSequence(dest, mSource);
+    writeCharSequence(dest, mProviderData);
+    if (mUri == null) {
+      dest.writeInt(0);
+    } else {
+      dest.writeInt(1);
+      mUri.writeToParcel(dest, flags);
+    }
+    if (mIsRead) {
+      dest.writeInt(1);
+    } else {
+      dest.writeInt(0);
+    }
+    if (mHasContent) {
+      dest.writeInt(1);
+    } else {
+      dest.writeInt(0);
+    }
+    writeCharSequence(dest, mTranscription);
+  }
+
+  public static final Creator<Voicemail> CREATOR =
+      new Creator<Voicemail>() {
+        @Override
+        public Voicemail createFromParcel(Parcel in) {
+          return new Voicemail(in);
+        }
+
+        @Override
+        public Voicemail[] newArray(int size) {
+          return new Voicemail[size];
+        }
+      };
+
+  private Voicemail(Parcel in) {
+    mTimestamp = in.readLong();
+    mNumber = (String) readCharSequence(in);
+    if (in.readInt() > 0) {
+      mPhoneAccount = PhoneAccountHandle.CREATOR.createFromParcel(in);
+    } else {
+      mPhoneAccount = null;
+    }
+    mId = in.readLong();
+    mDuration = in.readLong();
+    mSource = (String) readCharSequence(in);
+    mProviderData = (String) readCharSequence(in);
+    if (in.readInt() > 0) {
+      mUri = Uri.CREATOR.createFromParcel(in);
+    } else {
+      mUri = null;
+    }
+    mIsRead = in.readInt() > 0 ? true : false;
+    mHasContent = in.readInt() > 0 ? true : false;
+    mTranscription = (String) readCharSequence(in);
+  }
+
+  private static CharSequence readCharSequence(Parcel in) {
+    return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+  }
+
+  public static void writeCharSequence(Parcel dest, CharSequence val) {
+    TextUtils.writeToParcel(val, dest, 0);
+  }
+}
diff --git a/java/com/android/voicemail/impl/VoicemailClientImpl.java b/java/com/android/voicemail/impl/VoicemailClientImpl.java
new file mode 100644
index 0000000..1ad12ae
--- /dev/null
+++ b/java/com/android/voicemail/impl/VoicemailClientImpl.java
@@ -0,0 +1,90 @@
+/**
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.Nullable;
+import android.support.v4.os.BuildCompat;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.VoicemailClient;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+import com.android.voicemail.impl.settings.VoicemailSettingsFragment;
+import java.util.List;
+import javax.inject.Inject;
+
+/**
+ * {@link VoicemailClient} to be used when the voicemail module is activated. May only be used above
+ * O.
+ */
+public class VoicemailClientImpl implements VoicemailClient {
+
+  /**
+   * List of legacy OMTP voicemail packages that should be ignored. It could never be the active VVM
+   * package anymore. For example, voicemails in OC will no longer be handled by telephony, but
+   * legacy voicemails might still exist in the database due to upgrading from NYC. Dialer will
+   * fetch these voicemails again so it should be ignored.
+   */
+  private static final String[] OMTP_VOICEMAIL_BLACKLIST = {"com.android.phone"};
+
+  @Inject
+  public VoicemailClientImpl() {
+    Assert.checkArgument(BuildCompat.isAtLeastO());
+  }
+
+  @Nullable
+  @Override
+  public String getSettingsFragment() {
+    return VoicemailSettingsFragment.class.getName();
+  }
+
+  @Override
+  public boolean isVoicemailArchiveEnabled(Context context, PhoneAccountHandle phoneAccountHandle) {
+    return VisualVoicemailSettingsUtil.isArchiveEnabled(context, phoneAccountHandle);
+  }
+
+  @Override
+  public void setVoicemailArchiveEnabled(
+      Context context, PhoneAccountHandle phoneAccountHandle, boolean value) {
+    VisualVoicemailSettingsUtil.setArchiveEnabled(context, phoneAccountHandle, value);
+  }
+
+  @TargetApi(VERSION_CODES.O)
+  @Override
+  public void appendOmtpVoicemailSelectionClause(
+      Context context, StringBuilder where, List<String> selectionArgs) {
+    TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+    String omtpSource = TelephonyMangerCompat.getVisualVoicemailPackageName(telephonyManager);
+    where.append(
+        "AND ("
+            + "("
+            + Voicemails.IS_OMTP_VOICEMAIL
+            + " != 1)"
+            + "OR "
+            + "("
+            + Voicemails.SOURCE_PACKAGE
+            + " = ? )"
+            + ")");
+    selectionArgs.add(omtpSource);
+
+    for (String blacklistedPackage : OMTP_VOICEMAIL_BLACKLIST) {
+      where.append("AND (" + Voicemails.SOURCE_PACKAGE + "!= ?)");
+      selectionArgs.add(blacklistedPackage);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/VoicemailClientReceiver.java b/java/com/android/voicemail/impl/VoicemailClientReceiver.java
new file mode 100644
index 0000000..49a55a4
--- /dev/null
+++ b/java/com/android/voicemail/impl/VoicemailClientReceiver.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.voicemail.VoicemailClient;
+import com.android.voicemail.impl.sync.UploadTask;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+
+/** Receiver for broadcasts in {@link VoicemailClient#ACTION_UPLOAD} */
+public class VoicemailClientReceiver extends BroadcastReceiver {
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    switch (intent.getAction()) {
+      case VoicemailClient.ACTION_UPLOAD:
+        doUpload(context);
+        break;
+      default:
+        Assert.fail("Unexpected action " + intent.getAction());
+        break;
+    }
+  }
+
+  /** Upload local database changes to the server. */
+  private static void doUpload(Context context) {
+    LogUtil.i("VoicemailClientReceiver.onReceive", "ACTION_UPLOAD received");
+    for (PhoneAccountHandle phoneAccountHandle : VvmAccountManager.getActiveAccounts(context)) {
+      UploadTask.start(context, phoneAccountHandle);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/VoicemailModule.java b/java/com/android/voicemail/impl/VoicemailModule.java
new file mode 100644
index 0000000..c3e5714
--- /dev/null
+++ b/java/com/android/voicemail/impl/VoicemailModule.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.support.v4.os.BuildCompat;
+import com.android.voicemail.VoicemailClient;
+import com.android.voicemail.stub.StubVoicemailClient;
+import dagger.Module;
+import dagger.Provides;
+import javax.inject.Singleton;
+
+/** This module provides an instance of the voicemail client. */
+@Module
+public final class VoicemailModule {
+
+  @Provides
+  @Singleton
+  static VoicemailClient provideVoicemailClient() {
+    if (BuildCompat.isAtLeastO()) {
+      return new VoicemailClientImpl();
+    } else {
+      return new StubVoicemailClient();
+    }
+  }
+
+  private VoicemailModule() {}
+}
diff --git a/java/com/android/voicemail/impl/VoicemailStatus.java b/java/com/android/voicemail/impl/VoicemailStatus.java
new file mode 100644
index 0000000..ec1ab4e
--- /dev/null
+++ b/java/com/android/voicemail/impl/VoicemailStatus.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+
+public class VoicemailStatus {
+
+  private static final String TAG = "VvmStatus";
+
+  public static class Editor {
+
+    private final Context mContext;
+    @Nullable private final PhoneAccountHandle mPhoneAccountHandle;
+
+    private ContentValues mValues = new ContentValues();
+
+    private Editor(Context context, PhoneAccountHandle phoneAccountHandle) {
+      mContext = context;
+      mPhoneAccountHandle = phoneAccountHandle;
+      if (mPhoneAccountHandle == null) {
+        VvmLog.w(
+            TAG,
+            "VoicemailStatus.Editor created with null phone account, status will"
+                + " not be written");
+      }
+    }
+
+    @Nullable
+    public PhoneAccountHandle getPhoneAccountHandle() {
+      return mPhoneAccountHandle;
+    }
+
+    public Editor setType(String type) {
+      mValues.put(Status.SOURCE_TYPE, type);
+      return this;
+    }
+
+    public Editor setConfigurationState(int configurationState) {
+      mValues.put(Status.CONFIGURATION_STATE, configurationState);
+      return this;
+    }
+
+    public Editor setDataChannelState(int dataChannelState) {
+      mValues.put(Status.DATA_CHANNEL_STATE, dataChannelState);
+      return this;
+    }
+
+    public Editor setNotificationChannelState(int notificationChannelState) {
+      mValues.put(Status.NOTIFICATION_CHANNEL_STATE, notificationChannelState);
+      return this;
+    }
+
+    public Editor setQuota(int occupied, int total) {
+      if (occupied == VoicemailContract.Status.QUOTA_UNAVAILABLE
+          && total == VoicemailContract.Status.QUOTA_UNAVAILABLE) {
+        return this;
+      }
+
+      mValues.put(Status.QUOTA_OCCUPIED, occupied);
+      mValues.put(Status.QUOTA_TOTAL, total);
+      return this;
+    }
+
+    /**
+     * Apply the changes to the {@link VoicemailStatus} {@link #Editor}.
+     *
+     * @return {@code true} if the changes were successfully applied, {@code false} otherwise.
+     */
+    public boolean apply() {
+      if (mPhoneAccountHandle == null) {
+        return false;
+      }
+      mValues.put(
+          Status.PHONE_ACCOUNT_COMPONENT_NAME,
+          mPhoneAccountHandle.getComponentName().flattenToString());
+      mValues.put(Status.PHONE_ACCOUNT_ID, mPhoneAccountHandle.getId());
+      ContentResolver contentResolver = mContext.getContentResolver();
+      Uri statusUri = VoicemailContract.Status.buildSourceUri(mContext.getPackageName());
+      try {
+        contentResolver.insert(statusUri, mValues);
+      } catch (IllegalArgumentException iae) {
+        VvmLog.e(TAG, "apply :: failed to insert content resolver ", iae);
+        mValues.clear();
+        return false;
+      }
+      mValues.clear();
+      return true;
+    }
+
+    public ContentValues getValues() {
+      return mValues;
+    }
+  }
+
+  /**
+   * A voicemail status editor that the decision of whether to actually write to the database can be
+   * deferred. This object will be passed around as a usual {@link Editor}, but {@link #apply()}
+   * doesn't do anything. If later the creator of this object decides any status changes written to
+   * it should be committed, {@link #deferredApply()} should be called.
+   */
+  public static class DeferredEditor extends Editor {
+
+    private DeferredEditor(Context context, PhoneAccountHandle phoneAccountHandle) {
+      super(context, phoneAccountHandle);
+    }
+
+    @Override
+    public boolean apply() {
+      // Do nothing
+      return true;
+    }
+
+    public void deferredApply() {
+      super.apply();
+    }
+  }
+
+  public static Editor edit(Context context, PhoneAccountHandle phoneAccountHandle) {
+    return new Editor(context, phoneAccountHandle);
+  }
+
+  /**
+   * Reset the status to the "disabled" state, which the UI should not show anything for this
+   * phoneAccountHandle.
+   */
+  public static void disable(Context context, PhoneAccountHandle phoneAccountHandle) {
+    edit(context, phoneAccountHandle)
+        .setConfigurationState(Status.CONFIGURATION_STATE_NOT_CONFIGURED)
+        .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION)
+        .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION)
+        .apply();
+  }
+
+  public static DeferredEditor deferredEdit(
+      Context context, PhoneAccountHandle phoneAccountHandle) {
+    return new DeferredEditor(context, phoneAccountHandle);
+  }
+}
diff --git a/java/com/android/voicemail/impl/VvmLog.java b/java/com/android/voicemail/impl/VvmLog.java
new file mode 100644
index 0000000..595207f
--- /dev/null
+++ b/java/com/android/voicemail/impl/VvmLog.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl;
+
+import com.android.dialer.common.LogUtil;
+import com.android.voicemail.impl.utils.IndentingPrintWriter;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayDeque;
+import java.util.Calendar;
+import java.util.Deque;
+import java.util.Iterator;
+
+/** Helper methods for adding to OMTP visual voicemail local logs. */
+public class VvmLog {
+
+  private static final int MAX_OMTP_VVM_LOGS = 100;
+
+  private static final LocalLog sLocalLog = new LocalLog(MAX_OMTP_VVM_LOGS);
+
+  public static void log(String tag, String log) {
+    sLocalLog.log(tag + ": " + log);
+  }
+
+  public static void dump(FileDescriptor fd, PrintWriter printwriter, String[] args) {
+    IndentingPrintWriter indentingPrintWriter = new IndentingPrintWriter(printwriter, "  ");
+    indentingPrintWriter.increaseIndent();
+    sLocalLog.dump(fd, indentingPrintWriter, args);
+    indentingPrintWriter.decreaseIndent();
+  }
+
+  public static void e(String tag, String log) {
+    log(tag, log);
+    LogUtil.e(tag, log);
+  }
+
+  public static void e(String tag, String log, Throwable e) {
+    log(tag, log + " " + e);
+    LogUtil.e(tag, log, e);
+  }
+
+  public static void w(String tag, String log) {
+    log(tag, log);
+    LogUtil.w(tag, log);
+  }
+
+  public static void w(String tag, String log, Throwable e) {
+    log(tag, log + " " + e);
+    LogUtil.w(tag, log, e);
+  }
+
+  public static void i(String tag, String log) {
+    log(tag, log);
+    LogUtil.i(tag, log);
+  }
+
+  public static void i(String tag, String log, Throwable e) {
+    log(tag, log + " " + e);
+    LogUtil.i(tag, log, e);
+  }
+
+  public static void d(String tag, String log) {
+    log(tag, log);
+    LogUtil.d(tag, log);
+  }
+
+  public static void d(String tag, String log, Throwable e) {
+    log(tag, log + " " + e);
+    LogUtil.d(tag, log, e);
+  }
+
+  public static void v(String tag, String log) {
+    log(tag, log);
+    LogUtil.v(tag, log);
+  }
+
+  public static void v(String tag, String log, Throwable e) {
+    log(tag, log + " " + e);
+    LogUtil.v(tag, log, e);
+  }
+
+  public static void wtf(String tag, String log) {
+    log(tag, log);
+    LogUtil.e(tag, log);
+  }
+
+  public static void wtf(String tag, String log, Throwable e) {
+    log(tag, log + " " + e);
+    LogUtil.e(tag, log, e);
+  }
+
+  /**
+   * Redact personally identifiable information for production users. If we are running in verbose
+   * mode, return the original string, otherwise return a SHA-1 hash of the input string.
+   */
+  public static String pii(Object pii) {
+    if (pii == null) {
+      return String.valueOf(pii);
+    }
+    return "[PII]";
+  }
+
+  public static class LocalLog {
+
+    private final Deque<String> mLog;
+    private final int mMaxLines;
+
+    public LocalLog(int maxLines) {
+      mMaxLines = Math.max(0, maxLines);
+      mLog = new ArrayDeque<>(mMaxLines);
+    }
+
+    public void log(String msg) {
+      if (mMaxLines <= 0) {
+        return;
+      }
+      Calendar c = Calendar.getInstance();
+      c.setTimeInMillis(System.currentTimeMillis());
+      append(String.format("%tm-%td %tH:%tM:%tS.%tL - %s", c, c, c, c, c, c, msg));
+    }
+
+    private synchronized void append(String logLine) {
+      while (mLog.size() >= mMaxLines) {
+        mLog.remove();
+      }
+      mLog.add(logLine);
+    }
+
+    public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+      Iterator<String> itr = mLog.iterator();
+      while (itr.hasNext()) {
+        pw.println(itr.next());
+      }
+    }
+
+    public synchronized void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
+      Iterator<String> itr = mLog.descendingIterator();
+      while (itr.hasNext()) {
+        pw.println(itr.next());
+      }
+    }
+
+    public static class ReadOnlyLocalLog {
+
+      private final LocalLog mLog;
+
+      ReadOnlyLocalLog(LocalLog log) {
+        mLog = log;
+      }
+
+      public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        mLog.dump(fd, pw, args);
+      }
+
+      public void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        mLog.reverseDump(fd, pw, args);
+      }
+    }
+
+    public ReadOnlyLocalLog readOnlyLocalLog() {
+      return new ReadOnlyLocalLog(this);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/VvmPackageInstallReceiver.java b/java/com/android/voicemail/impl/VvmPackageInstallReceiver.java
new file mode 100644
index 0000000..c5650b3
--- /dev/null
+++ b/java/com/android/voicemail/impl/VvmPackageInstallReceiver.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+
+/**
+ * When a new package is installed, check if it matches any of the vvm carrier apps of the currently
+ * enabled dialer vvm sources.
+ */
+public class VvmPackageInstallReceiver extends BroadcastReceiver {
+
+  private static final String TAG = "VvmPkgInstallReceiver";
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    if (intent.getData() == null) {
+      return;
+    }
+
+    String packageName = intent.getData().getSchemeSpecificPart();
+    if (packageName == null) {
+      return;
+    }
+
+    for (PhoneAccountHandle phoneAccount :
+        context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) {
+      if (VisualVoicemailSettingsUtil.isEnabledUserSet(context, phoneAccount)) {
+        // Skip the check if this voicemail source's setting is overridden by the user.
+        continue;
+      }
+
+      OmtpVvmCarrierConfigHelper carrierConfigHelper =
+          new OmtpVvmCarrierConfigHelper(context, phoneAccount);
+      if (carrierConfigHelper.getCarrierVvmPackageNames() == null) {
+        continue;
+      }
+      if (carrierConfigHelper.getCarrierVvmPackageNames().contains(packageName)) {
+        // Force deactivate the client. The user can re-enable it in the settings.
+        // There is no need to update the settings for deactivation. At this point, if the
+        // default value is used it should be false because a carrier package is present.
+        VvmLog.i(TAG, "Carrier VVM package installed, disabling system VVM client");
+        VisualVoicemailSettingsUtil.setEnabled(context, phoneAccount, false);
+      }
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/VvmPhoneStateListener.java b/java/com/android/voicemail/impl/VvmPhoneStateListener.java
new file mode 100644
index 0000000..48b7204
--- /dev/null
+++ b/java/com/android/voicemail/impl/VvmPhoneStateListener.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
+import com.android.voicemail.impl.sync.OmtpVvmSyncService;
+import com.android.voicemail.impl.sync.SyncTask;
+import com.android.voicemail.impl.sync.VoicemailStatusQueryHelper;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+
+/**
+ * Check if service is lost and indicate this in the voicemail status. TODO(b/35125657): Not used
+ * for now, restore it.
+ */
+public class VvmPhoneStateListener extends PhoneStateListener {
+
+  private static final String TAG = "VvmPhoneStateListener";
+
+  private PhoneAccountHandle mPhoneAccount;
+  private Context mContext;
+  private int mPreviousState = -1;
+
+  public VvmPhoneStateListener(Context context, PhoneAccountHandle accountHandle) {
+    // TODO: b/32637799 too much trouble to call super constructor through reflection,
+    // just use non-phoneAccountHandle version for now.
+    super();
+    mContext = context;
+    mPhoneAccount = accountHandle;
+  }
+
+  @Override
+  public void onServiceStateChanged(ServiceState serviceState) {
+    if (mPhoneAccount == null) {
+      VvmLog.e(
+          TAG,
+          "onServiceStateChanged on phoneAccount "
+              + mPhoneAccount
+              + " with invalid phoneAccountHandle, ignoring");
+      return;
+    }
+
+    int state = serviceState.getState();
+    if (state == mPreviousState
+        || (state != ServiceState.STATE_IN_SERVICE
+            && mPreviousState != ServiceState.STATE_IN_SERVICE)) {
+      // Only interested in state changes or transitioning into or out of "in service".
+      // Otherwise just quit.
+      mPreviousState = state;
+      return;
+    }
+
+    OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(mContext, mPhoneAccount);
+
+    if (state == ServiceState.STATE_IN_SERVICE) {
+      VoicemailStatusQueryHelper voicemailStatusQueryHelper =
+          new VoicemailStatusQueryHelper(mContext);
+      if (voicemailStatusQueryHelper.isVoicemailSourceConfigured(mPhoneAccount)) {
+        if (!voicemailStatusQueryHelper.isNotificationsChannelActive(mPhoneAccount)) {
+          VvmLog.v(TAG, "Notifications channel is active for " + mPhoneAccount);
+          helper.handleEvent(
+              VoicemailStatus.edit(mContext, mPhoneAccount), OmtpEvents.NOTIFICATION_IN_SERVICE);
+        }
+      }
+
+      if (VvmAccountManager.isAccountActivated(mContext, mPhoneAccount)) {
+        VvmLog.v(TAG, "Signal returned: requesting resync for " + mPhoneAccount);
+        // If the source is already registered, run a full sync in case something was missed
+        // while signal was down.
+        SyncTask.start(mContext, mPhoneAccount, OmtpVvmSyncService.SYNC_FULL_SYNC);
+      } else {
+        VvmLog.v(TAG, "Signal returned: reattempting activation for " + mPhoneAccount);
+        // Otherwise initiate an activation because this means that an OMTP source was
+        // recognized but either the activation text was not successfully sent or a response
+        // was not received.
+        helper.startActivation();
+      }
+    } else {
+      VvmLog.v(TAG, "Notifications channel is inactive for " + mPhoneAccount);
+
+      if (!VvmAccountManager.isAccountActivated(mContext, mPhoneAccount)) {
+        return;
+      }
+      helper.handleEvent(
+          VoicemailStatus.edit(mContext, mPhoneAccount), OmtpEvents.NOTIFICATION_SERVICE_LOST);
+    }
+    mPreviousState = state;
+  }
+}
diff --git a/java/com/android/voicemail/impl/fetch/FetchVoicemailReceiver.java b/java/com/android/voicemail/impl/fetch/FetchVoicemailReceiver.java
new file mode 100644
index 0000000..07e8008
--- /dev/null
+++ b/java/com/android/voicemail/impl/fetch/FetchVoicemailReceiver.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.fetch;
+
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Network;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.os.BuildCompat;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+import com.android.voicemail.impl.sync.VvmNetworkRequestCallback;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/** handles {@link VoicemailContract#ACTION_FETCH_VOICEMAIL} */
+@TargetApi(VERSION_CODES.O)
+public class FetchVoicemailReceiver extends BroadcastReceiver {
+
+  private static final String TAG = "FetchVoicemailReceiver";
+
+  static final String[] PROJECTION =
+      new String[] {
+        Voicemails.SOURCE_DATA, // 0
+        Voicemails.PHONE_ACCOUNT_ID, // 1
+        Voicemails.PHONE_ACCOUNT_COMPONENT_NAME, // 2
+      };
+
+  public static final int SOURCE_DATA = 0;
+  public static final int PHONE_ACCOUNT_ID = 1;
+  public static final int PHONE_ACCOUNT_COMPONENT_NAME = 2;
+
+  // Number of retries
+  private static final int NETWORK_RETRY_COUNT = 3;
+
+  private ContentResolver mContentResolver;
+  private Uri mUri;
+  private VvmNetworkRequestCallback mNetworkCallback;
+  private Context mContext;
+  private String mUid;
+  private PhoneAccountHandle mPhoneAccount;
+  private int mRetryCount = NETWORK_RETRY_COUNT;
+
+  @Override
+  public void onReceive(final Context context, Intent intent) {
+    if (VoicemailContract.ACTION_FETCH_VOICEMAIL.equals(intent.getAction())) {
+      VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL received");
+      mContext = context;
+      mContentResolver = context.getContentResolver();
+      mUri = intent.getData();
+
+      if (mUri == null) {
+        VvmLog.w(TAG, VoicemailContract.ACTION_FETCH_VOICEMAIL + " intent sent with no data");
+        return;
+      }
+
+      if (!context
+          .getPackageName()
+          .equals(mUri.getQueryParameter(VoicemailContract.PARAM_KEY_SOURCE_PACKAGE))) {
+        // Ignore if the fetch request is for a voicemail not from this package.
+        VvmLog.e(TAG, "ACTION_FETCH_VOICEMAIL from foreign pacakge " + context.getPackageName());
+        return;
+      }
+
+      Cursor cursor = mContentResolver.query(mUri, PROJECTION, null, null, null);
+      if (cursor == null) {
+        VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL query returned null");
+        return;
+      }
+      try {
+        if (cursor.moveToFirst()) {
+          mUid = cursor.getString(SOURCE_DATA);
+          String accountId = cursor.getString(PHONE_ACCOUNT_ID);
+          if (TextUtils.isEmpty(accountId)) {
+            TelephonyManager telephonyManager =
+                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+            accountId = telephonyManager.getSimSerialNumber();
+
+            if (TextUtils.isEmpty(accountId)) {
+              VvmLog.e(TAG, "Account null and no default sim found.");
+              return;
+            }
+          }
+
+          mPhoneAccount =
+              new PhoneAccountHandle(
+                  ComponentName.unflattenFromString(cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME)),
+                  cursor.getString(PHONE_ACCOUNT_ID));
+          if (!VvmAccountManager.isAccountActivated(context, mPhoneAccount)) {
+            mPhoneAccount = getAccountFromMarshmallowAccount(context, mPhoneAccount);
+            if (mPhoneAccount == null) {
+              VvmLog.w(TAG, "Account not registered - cannot retrieve message.");
+              return;
+            }
+            VvmLog.i(TAG, "Fetching voicemail with Marshmallow PhoneAccountHandle");
+          }
+          VvmLog.i(TAG, "Requesting network to fetch voicemail");
+          mNetworkCallback = new fetchVoicemailNetworkRequestCallback(context, mPhoneAccount);
+          mNetworkCallback.requestNetwork();
+        }
+      } finally {
+        cursor.close();
+      }
+    }
+  }
+
+  /**
+   * In ag/930496 the format of PhoneAccountHandle has changed between Marshmallow and Nougat. This
+   * method attempts to search the account from the old database in registered sources using the old
+   * format. There's a chance of M phone account collisions on multi-SIM devices, but visual
+   * voicemail is not supported on M multi-SIM.
+   */
+  @Nullable
+  private static PhoneAccountHandle getAccountFromMarshmallowAccount(
+      Context context, PhoneAccountHandle oldAccount) {
+    if (!BuildCompat.isAtLeastN()) {
+      return null;
+    }
+    for (PhoneAccountHandle handle :
+        context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) {
+      if (getIccSerialNumberFromFullIccSerialNumber(handle.getId()).equals(oldAccount.getId())) {
+        return handle;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * getIccSerialNumber() is used for ID before N, and getFullIccSerialNumber() after.
+   * getIccSerialNumber() stops at the first hex char.
+   */
+  @NonNull
+  private static String getIccSerialNumberFromFullIccSerialNumber(@NonNull String id) {
+    for (int i = 0; i < id.length(); i++) {
+      if (!Character.isDigit(id.charAt(i))) {
+        return id.substring(0, i);
+      }
+    }
+    return id;
+  }
+
+  private class fetchVoicemailNetworkRequestCallback extends VvmNetworkRequestCallback {
+
+    public fetchVoicemailNetworkRequestCallback(Context context, PhoneAccountHandle phoneAccount) {
+      super(context, phoneAccount, VoicemailStatus.edit(context, phoneAccount));
+    }
+
+    @Override
+    public void onAvailable(final Network network) {
+      super.onAvailable(network);
+      fetchVoicemail(network, getVoicemailStatusEditor());
+    }
+  }
+
+  private void fetchVoicemail(final Network network, final VoicemailStatus.Editor status) {
+    Executor executor = Executors.newCachedThreadPool();
+    executor.execute(
+        new Runnable() {
+          @Override
+          public void run() {
+            try {
+              while (mRetryCount > 0) {
+                VvmLog.i(TAG, "fetching voicemail, retry count=" + mRetryCount);
+                try (ImapHelper imapHelper =
+                    new ImapHelper(mContext, mPhoneAccount, network, status)) {
+                  boolean success =
+                      imapHelper.fetchVoicemailPayload(
+                          new VoicemailFetchedCallback(mContext, mUri, mPhoneAccount), mUid);
+                  if (!success && mRetryCount > 0) {
+                    VvmLog.i(TAG, "fetch voicemail failed, retrying");
+                    mRetryCount--;
+                  } else {
+                    return;
+                  }
+                } catch (InitializingException e) {
+                  VvmLog.w(TAG, "Can't retrieve Imap credentials ", e);
+                  return;
+                }
+              }
+            } finally {
+              if (mNetworkCallback != null) {
+                mNetworkCallback.releaseNetwork();
+              }
+            }
+          }
+        });
+  }
+}
diff --git a/java/com/android/voicemail/impl/fetch/VoicemailFetchedCallback.java b/java/com/android/voicemail/impl/fetch/VoicemailFetchedCallback.java
new file mode 100644
index 0000000..f386fce
--- /dev/null
+++ b/java/com/android/voicemail/impl/fetch/VoicemailFetchedCallback.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.fetch;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.voicemail.impl.R;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.imap.VoicemailPayload;
+import java.io.IOException;
+import java.io.OutputStream;
+import org.apache.commons.io.IOUtils;
+
+/**
+ * Callback for when a voicemail payload is fetched. It copies the returned stream to the data file
+ * corresponding to the voicemail.
+ */
+public class VoicemailFetchedCallback {
+  private static final String TAG = "VoicemailFetchedCallback";
+
+  private final Context mContext;
+  private final ContentResolver mContentResolver;
+  private final Uri mUri;
+  private final PhoneAccountHandle mPhoneAccountHandle;
+
+  public VoicemailFetchedCallback(Context context, Uri uri, PhoneAccountHandle phoneAccountHandle) {
+    mContext = context;
+    mContentResolver = context.getContentResolver();
+    mUri = uri;
+    mPhoneAccountHandle = phoneAccountHandle;
+  }
+
+  /**
+   * Saves the voicemail payload data into the voicemail provider then sets the "has_content" bit of
+   * the voicemail to "1".
+   *
+   * @param voicemailPayload The object containing the content data for the voicemail
+   */
+  public void setVoicemailContent(@Nullable VoicemailPayload voicemailPayload) {
+    if (voicemailPayload == null) {
+      VvmLog.i(TAG, "Payload not found, message has unsupported format");
+      ContentValues values = new ContentValues();
+      values.put(
+          Voicemails.TRANSCRIPTION,
+          mContext.getString(
+              R.string.vvm_unsupported_message_format,
+              mContext
+                  .getSystemService(TelecomManager.class)
+                  .getVoiceMailNumber(mPhoneAccountHandle)));
+      updateVoicemail(values);
+      return;
+    }
+
+    VvmLog.d(TAG, String.format("Writing new voicemail content: %s", mUri));
+    OutputStream outputStream = null;
+
+    try {
+      outputStream = mContentResolver.openOutputStream(mUri);
+      byte[] inputBytes = voicemailPayload.getBytes();
+      if (inputBytes != null) {
+        outputStream.write(inputBytes);
+      }
+    } catch (IOException e) {
+      VvmLog.w(TAG, String.format("File not found for %s", mUri));
+      return;
+    } finally {
+      IOUtils.closeQuietly(outputStream);
+    }
+
+    // Update mime_type & has_content after we are done with file update.
+    ContentValues values = new ContentValues();
+    values.put(Voicemails.MIME_TYPE, voicemailPayload.getMimeType());
+    values.put(Voicemails.HAS_CONTENT, true);
+    updateVoicemail(values);
+  }
+
+  private void updateVoicemail(ContentValues values) {
+    int updatedCount = mContentResolver.update(mUri, values, null, null);
+    if (updatedCount != 1) {
+      VvmLog.e(TAG, "Updating voicemail should have updated 1 row, was: " + updatedCount);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/imap/ImapHelper.java b/java/com/android/voicemail/impl/imap/ImapHelper.java
new file mode 100644
index 0000000..6aa4158
--- /dev/null
+++ b/java/com/android/voicemail/impl/imap/ImapHelper.java
@@ -0,0 +1,693 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.imap;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkInfo;
+import android.provider.VoicemailContract;
+import android.telecom.PhoneAccountHandle;
+import android.util.Base64;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpConstants.ChangePinResult;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.Voicemail;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VoicemailStatus.Editor;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.fetch.VoicemailFetchedCallback;
+import com.android.voicemail.impl.mail.Address;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.BodyPart;
+import com.android.voicemail.impl.mail.FetchProfile;
+import com.android.voicemail.impl.mail.Flag;
+import com.android.voicemail.impl.mail.Message;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Multipart;
+import com.android.voicemail.impl.mail.TempDirectory;
+import com.android.voicemail.impl.mail.internet.MimeMessage;
+import com.android.voicemail.impl.mail.store.ImapConnection;
+import com.android.voicemail.impl.mail.store.ImapFolder;
+import com.android.voicemail.impl.mail.store.ImapStore;
+import com.android.voicemail.impl.mail.store.imap.ImapConstants;
+import com.android.voicemail.impl.mail.store.imap.ImapResponse;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import com.android.voicemail.impl.sync.OmtpVvmSyncService.TranscriptionFetchedCallback;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import org.apache.commons.io.IOUtils;
+
+/** A helper interface to abstract commands sent across IMAP interface for a given account. */
+public class ImapHelper implements Closeable {
+
+  private static final String TAG = "ImapHelper";
+
+  private ImapFolder mFolder;
+  private ImapStore mImapStore;
+
+  private final Context mContext;
+  private final PhoneAccountHandle mPhoneAccount;
+  private final Network mNetwork;
+  private final Editor mStatus;
+
+  VisualVoicemailPreferences mPrefs;
+  private static final String PREF_KEY_QUOTA_OCCUPIED = "quota_occupied_";
+  private static final String PREF_KEY_QUOTA_TOTAL = "quota_total_";
+
+  private int mQuotaOccupied;
+  private int mQuotaTotal;
+
+  private final OmtpVvmCarrierConfigHelper mConfig;
+
+  /** InitializingException */
+  public static class InitializingException extends Exception {
+
+    public InitializingException(String message) {
+      super(message);
+    }
+  }
+
+  public ImapHelper(
+      Context context,
+      PhoneAccountHandle phoneAccount,
+      Network network,
+      Editor status)
+      throws InitializingException {
+    this(
+        context,
+        new OmtpVvmCarrierConfigHelper(context, phoneAccount),
+        phoneAccount,
+        network,
+        status);
+  }
+
+  public ImapHelper(
+      Context context,
+      OmtpVvmCarrierConfigHelper config,
+      PhoneAccountHandle phoneAccount,
+      Network network,
+      Editor status)
+      throws InitializingException {
+    mContext = context;
+    mPhoneAccount = phoneAccount;
+    mNetwork = network;
+    mStatus = status;
+    mConfig = config;
+    mPrefs = new VisualVoicemailPreferences(context, phoneAccount);
+
+    try {
+      TempDirectory.setTempDirectory(context);
+
+      String username = mPrefs.getString(OmtpConstants.IMAP_USER_NAME, null);
+      String password = mPrefs.getString(OmtpConstants.IMAP_PASSWORD, null);
+      String serverName = mPrefs.getString(OmtpConstants.SERVER_ADDRESS, null);
+      int port = Integer.parseInt(mPrefs.getString(OmtpConstants.IMAP_PORT, null));
+      int auth = ImapStore.FLAG_NONE;
+
+      int sslPort = mConfig.getSslPort();
+      if (sslPort != 0) {
+        port = sslPort;
+        auth = ImapStore.FLAG_SSL;
+      }
+
+      mImapStore =
+          new ImapStore(context, this, username, password, port, serverName, auth, network);
+    } catch (NumberFormatException e) {
+      handleEvent(OmtpEvents.DATA_INVALID_PORT);
+      LogUtils.w(TAG, "Could not parse port number");
+      throw new InitializingException("cannot initialize ImapHelper:" + e.toString());
+    }
+
+    mQuotaOccupied =
+        mPrefs.getInt(PREF_KEY_QUOTA_OCCUPIED, VoicemailContract.Status.QUOTA_UNAVAILABLE);
+    mQuotaTotal = mPrefs.getInt(PREF_KEY_QUOTA_TOTAL, VoicemailContract.Status.QUOTA_UNAVAILABLE);
+  }
+
+  @Override
+  public void close() {
+    mImapStore.closeConnection();
+  }
+
+  public boolean isRoaming() {
+    ConnectivityManager connectivityManager =
+        (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+    NetworkInfo info = connectivityManager.getNetworkInfo(mNetwork);
+    if (info == null) {
+      return false;
+    }
+    return info.isRoaming();
+  }
+
+  public OmtpVvmCarrierConfigHelper getConfig() {
+    return mConfig;
+  }
+
+  public ImapConnection connect() {
+    return mImapStore.getConnection();
+  }
+
+  /** The caller thread will block until the method returns. */
+  public boolean markMessagesAsRead(List<Voicemail> voicemails) {
+    return setFlags(voicemails, Flag.SEEN);
+  }
+
+  /** The caller thread will block until the method returns. */
+  public boolean markMessagesAsDeleted(List<Voicemail> voicemails) {
+    return setFlags(voicemails, Flag.DELETED);
+  }
+
+  public void handleEvent(OmtpEvents event) {
+    mConfig.handleEvent(mStatus, event);
+  }
+
+  /**
+   * Set flags on the server for a given set of voicemails.
+   *
+   * @param voicemails The voicemails to set flags for.
+   * @param flags The flags to set on the voicemails.
+   * @return {@code true} if the operation completes successfully, {@code false} otherwise.
+   */
+  private boolean setFlags(List<Voicemail> voicemails, String... flags) {
+    if (voicemails.size() == 0) {
+      return false;
+    }
+    try {
+      mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+      if (mFolder != null) {
+        mFolder.setFlags(convertToImapMessages(voicemails), flags, true);
+        return true;
+      }
+      return false;
+    } catch (MessagingException e) {
+      LogUtils.e(TAG, e, "Messaging exception");
+      return false;
+    } finally {
+      closeImapFolder();
+    }
+  }
+
+  /**
+   * Fetch a list of voicemails from the server.
+   *
+   * @return A list of voicemail objects containing data about voicemails stored on the server.
+   */
+  public List<Voicemail> fetchAllVoicemails() {
+    List<Voicemail> result = new ArrayList<Voicemail>();
+    Message[] messages;
+    try {
+      mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+      if (mFolder == null) {
+        // This means we were unable to successfully open the folder.
+        return null;
+      }
+
+      // This method retrieves lightweight messages containing only the uid of the message.
+      messages = mFolder.getMessages(null);
+
+      for (Message message : messages) {
+        // Get the voicemail details (message structure).
+        MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
+        if (messageStructureWrapper != null) {
+          result.add(getVoicemailFromMessageStructure(messageStructureWrapper));
+        }
+      }
+      return result;
+    } catch (MessagingException e) {
+      LogUtils.e(TAG, e, "Messaging Exception");
+      return null;
+    } finally {
+      closeImapFolder();
+    }
+  }
+
+  /**
+   * Extract voicemail details from the message structure. Also fetch transcription if a
+   * transcription exists.
+   */
+  private Voicemail getVoicemailFromMessageStructure(
+      MessageStructureWrapper messageStructureWrapper) throws MessagingException {
+    Message messageDetails = messageStructureWrapper.messageStructure;
+
+    TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
+    if (messageStructureWrapper.transcriptionBodyPart != null) {
+      FetchProfile fetchProfile = new FetchProfile();
+      fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
+
+      mFolder.fetch(new Message[] {messageDetails}, fetchProfile, listener);
+    }
+
+    // Found an audio attachment, this is a valid voicemail.
+    long time = messageDetails.getSentDate().getTime();
+    String number = getNumber(messageDetails.getFrom());
+    boolean isRead = Arrays.asList(messageDetails.getFlags()).contains(Flag.SEEN);
+    return Voicemail.createForInsertion(time, number)
+        .setPhoneAccount(mPhoneAccount)
+        .setSourcePackage(mContext.getPackageName())
+        .setSourceData(messageDetails.getUid())
+        .setIsRead(isRead)
+        .setTranscription(listener.getVoicemailTranscription())
+        .build();
+  }
+
+  /**
+   * The "from" field of a visual voicemail IMAP message is the number of the caller who left the
+   * message. Extract this number from the list of "from" addresses.
+   *
+   * @param fromAddresses A list of addresses that comprise the "from" line.
+   * @return The number of the voicemail sender.
+   */
+  private String getNumber(Address[] fromAddresses) {
+    if (fromAddresses != null && fromAddresses.length > 0) {
+      if (fromAddresses.length != 1) {
+        LogUtils.w(TAG, "More than one from addresses found. Using the first one.");
+      }
+      String sender = fromAddresses[0].getAddress();
+      int atPos = sender.indexOf('@');
+      if (atPos != -1) {
+        // Strip domain part of the address.
+        sender = sender.substring(0, atPos);
+      }
+      return sender;
+    }
+    return null;
+  }
+
+  /**
+   * Fetches the structure of the given message and returns a wrapper containing the message
+   * structure and the transcription structure (if applicable).
+   *
+   * @throws MessagingException if fetching the structure of the message fails
+   */
+  private MessageStructureWrapper fetchMessageStructure(Message message) throws MessagingException {
+    LogUtils.d(TAG, "Fetching message structure for " + message.getUid());
+
+    MessageStructureFetchedListener listener = new MessageStructureFetchedListener();
+
+    FetchProfile fetchProfile = new FetchProfile();
+    fetchProfile.addAll(
+        Arrays.asList(
+            FetchProfile.Item.FLAGS, FetchProfile.Item.ENVELOPE, FetchProfile.Item.STRUCTURE));
+
+    // The IMAP folder fetch method will call "messageRetrieved" on the listener when the
+    // message is successfully retrieved.
+    mFolder.fetch(new Message[] {message}, fetchProfile, listener);
+    return listener.getMessageStructure();
+  }
+
+  public boolean fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid) {
+    try {
+      mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+      if (mFolder == null) {
+        // This means we were unable to successfully open the folder.
+        return false;
+      }
+      Message message = mFolder.getMessage(uid);
+      if (message == null) {
+        return false;
+      }
+      VoicemailPayload voicemailPayload = fetchVoicemailPayload(message);
+      callback.setVoicemailContent(voicemailPayload);
+      return true;
+    } catch (MessagingException e) {
+    } finally {
+      closeImapFolder();
+    }
+    return false;
+  }
+
+  /**
+   * Fetches the body of the given message and returns the parsed voicemail payload.
+   *
+   * @throws MessagingException if fetching the body of the message fails
+   */
+  private VoicemailPayload fetchVoicemailPayload(Message message) throws MessagingException {
+    LogUtils.d(TAG, "Fetching message body for " + message.getUid());
+
+    MessageBodyFetchedListener listener = new MessageBodyFetchedListener();
+
+    FetchProfile fetchProfile = new FetchProfile();
+    fetchProfile.add(FetchProfile.Item.BODY);
+
+    mFolder.fetch(new Message[] {message}, fetchProfile, listener);
+    return listener.getVoicemailPayload();
+  }
+
+  public boolean fetchTranscription(TranscriptionFetchedCallback callback, String uid) {
+    try {
+      mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+      if (mFolder == null) {
+        // This means we were unable to successfully open the folder.
+        return false;
+      }
+
+      Message message = mFolder.getMessage(uid);
+      if (message == null) {
+        return false;
+      }
+
+      MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
+      if (messageStructureWrapper != null) {
+        TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
+        if (messageStructureWrapper.transcriptionBodyPart != null) {
+          FetchProfile fetchProfile = new FetchProfile();
+          fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
+
+          // This method is called synchronously so the transcription will be populated
+          // in the listener once the next method is called.
+          mFolder.fetch(new Message[] {message}, fetchProfile, listener);
+          callback.setVoicemailTranscription(listener.getVoicemailTranscription());
+        }
+      }
+      return true;
+    } catch (MessagingException e) {
+      LogUtils.e(TAG, e, "Messaging Exception");
+      return false;
+    } finally {
+      closeImapFolder();
+    }
+  }
+
+  @ChangePinResult
+  public int changePin(String oldPin, String newPin) throws MessagingException {
+    ImapConnection connection = mImapStore.getConnection();
+    try {
+      String command =
+          getConfig().getProtocol().getCommand(OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT);
+      connection.sendCommand(String.format(Locale.US, command, newPin, oldPin), true);
+      return getChangePinResultFromImapResponse(connection.readResponse());
+    } catch (IOException ioe) {
+      VvmLog.e(TAG, "changePin: ", ioe);
+      return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR;
+    } finally {
+      connection.destroyResponses();
+    }
+  }
+
+  public void changeVoicemailTuiLanguage(String languageCode) throws MessagingException {
+    ImapConnection connection = mImapStore.getConnection();
+    try {
+      String command =
+          getConfig().getProtocol().getCommand(OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT);
+      connection.sendCommand(String.format(Locale.US, command, languageCode), true);
+    } catch (IOException ioe) {
+      LogUtils.e(TAG, ioe.toString());
+    } finally {
+      connection.destroyResponses();
+    }
+  }
+
+  public void closeNewUserTutorial() throws MessagingException {
+    ImapConnection connection = mImapStore.getConnection();
+    try {
+      String command = getConfig().getProtocol().getCommand(OmtpConstants.IMAP_CLOSE_NUT);
+      connection.executeSimpleCommand(command, false);
+    } catch (IOException ioe) {
+      throw new MessagingException(MessagingException.SERVER_ERROR, ioe.toString());
+    } finally {
+      connection.destroyResponses();
+    }
+  }
+
+  @ChangePinResult
+  private static int getChangePinResultFromImapResponse(ImapResponse response)
+      throws MessagingException {
+    if (!response.isTagged()) {
+      throw new MessagingException(MessagingException.SERVER_ERROR, "tagged response expected");
+    }
+    if (!response.isOk()) {
+      String message = response.getStringOrEmpty(1).getString();
+      LogUtils.d(TAG, "change PIN failed: " + message);
+      if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_SHORT.equals(message)) {
+        return OmtpConstants.CHANGE_PIN_TOO_SHORT;
+      }
+      if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_LONG.equals(message)) {
+        return OmtpConstants.CHANGE_PIN_TOO_LONG;
+      }
+      if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_WEAK.equals(message)) {
+        return OmtpConstants.CHANGE_PIN_TOO_WEAK;
+      }
+      if (OmtpConstants.RESPONSE_CHANGE_PIN_MISMATCH.equals(message)) {
+        return OmtpConstants.CHANGE_PIN_MISMATCH;
+      }
+      if (OmtpConstants.RESPONSE_CHANGE_PIN_INVALID_CHARACTER.equals(message)) {
+        return OmtpConstants.CHANGE_PIN_INVALID_CHARACTER;
+      }
+      return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR;
+    }
+    LogUtils.d(TAG, "change PIN succeeded");
+    return OmtpConstants.CHANGE_PIN_SUCCESS;
+  }
+
+  public void updateQuota() {
+    try {
+      mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+      if (mFolder == null) {
+        // This means we were unable to successfully open the folder.
+        return;
+      }
+      updateQuota(mFolder);
+    } catch (MessagingException e) {
+      LogUtils.e(TAG, e, "Messaging Exception");
+    } finally {
+      closeImapFolder();
+    }
+  }
+
+  public int getOccuupiedQuota() {
+    return mQuotaOccupied;
+  }
+
+  public int getTotalQuota() {
+    return mQuotaTotal;
+  }
+
+  private void updateQuota(ImapFolder folder) throws MessagingException {
+    setQuota(folder.getQuota());
+  }
+
+  private void setQuota(ImapFolder.Quota quota) {
+    if (quota == null) {
+      return;
+    }
+    if (quota.occupied == mQuotaOccupied && quota.total == mQuotaTotal) {
+      VvmLog.v(TAG, "Quota hasn't changed");
+      return;
+    }
+    mQuotaOccupied = quota.occupied;
+    mQuotaTotal = quota.total;
+    VoicemailStatus.edit(mContext, mPhoneAccount).setQuota(mQuotaOccupied, mQuotaTotal).apply();
+    mPrefs
+        .edit()
+        .putInt(PREF_KEY_QUOTA_OCCUPIED, mQuotaOccupied)
+        .putInt(PREF_KEY_QUOTA_TOTAL, mQuotaTotal)
+        .apply();
+    VvmLog.v(TAG, "Quota changed to " + mQuotaOccupied + "/" + mQuotaTotal);
+  }
+
+  /**
+   * A wrapper to hold a message with its header details and the structure for transcriptions (so
+   * they can be fetched in the future).
+   */
+  public static class MessageStructureWrapper {
+
+    public Message messageStructure;
+    public BodyPart transcriptionBodyPart;
+
+    public MessageStructureWrapper() {}
+  }
+
+  /** Listener for the message structure being fetched. */
+  private final class MessageStructureFetchedListener
+      implements ImapFolder.MessageRetrievalListener {
+
+    private MessageStructureWrapper mMessageStructure;
+
+    public MessageStructureFetchedListener() {}
+
+    public MessageStructureWrapper getMessageStructure() {
+      return mMessageStructure;
+    }
+
+    @Override
+    public void messageRetrieved(Message message) {
+      LogUtils.d(TAG, "Fetched message structure for " + message.getUid());
+      LogUtils.d(TAG, "Message retrieved: " + message);
+      try {
+        mMessageStructure = getMessageOrNull(message);
+        if (mMessageStructure == null) {
+          LogUtils.d(TAG, "This voicemail does not have an attachment...");
+          return;
+        }
+      } catch (MessagingException e) {
+        LogUtils.e(TAG, e, "Messaging Exception");
+        closeImapFolder();
+      }
+    }
+
+    /**
+     * Check if this IMAP message is a valid voicemail and whether it contains a transcription.
+     *
+     * @param message The IMAP message.
+     * @return The MessageStructureWrapper object corresponding to an IMAP message and
+     *     transcription.
+     */
+    private MessageStructureWrapper getMessageOrNull(Message message) throws MessagingException {
+      if (!message.getMimeType().startsWith("multipart/")) {
+        LogUtils.w(TAG, "Ignored non multi-part message");
+        return null;
+      }
+
+      MessageStructureWrapper messageStructureWrapper = new MessageStructureWrapper();
+
+      Multipart multipart = (Multipart) message.getBody();
+      for (int i = 0; i < multipart.getCount(); ++i) {
+        BodyPart bodyPart = multipart.getBodyPart(i);
+        String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
+        LogUtils.d(TAG, "bodyPart mime type: " + bodyPartMimeType);
+
+        if (bodyPartMimeType.startsWith("audio/")) {
+          messageStructureWrapper.messageStructure = message;
+        } else if (bodyPartMimeType.startsWith("text/")) {
+          messageStructureWrapper.transcriptionBodyPart = bodyPart;
+        } else {
+          VvmLog.v(TAG, "Unknown bodyPart MIME: " + bodyPartMimeType);
+        }
+      }
+
+      if (messageStructureWrapper.messageStructure != null) {
+        return messageStructureWrapper;
+      }
+
+      // No attachment found, this is not a voicemail.
+      return null;
+    }
+  }
+
+  /** Listener for the message body being fetched. */
+  private final class MessageBodyFetchedListener implements ImapFolder.MessageRetrievalListener {
+
+    private VoicemailPayload mVoicemailPayload;
+
+    /** Returns the fetch voicemail payload. */
+    public VoicemailPayload getVoicemailPayload() {
+      return mVoicemailPayload;
+    }
+
+    @Override
+    public void messageRetrieved(Message message) {
+      LogUtils.d(TAG, "Fetched message body for " + message.getUid());
+      LogUtils.d(TAG, "Message retrieved: " + message);
+      try {
+        mVoicemailPayload = getVoicemailPayloadFromMessage(message);
+      } catch (MessagingException e) {
+        LogUtils.e(TAG, "Messaging Exception:", e);
+      } catch (IOException e) {
+        LogUtils.e(TAG, "IO Exception:", e);
+      }
+    }
+
+    private VoicemailPayload getVoicemailPayloadFromMessage(Message message)
+        throws MessagingException, IOException {
+      Multipart multipart = (Multipart) message.getBody();
+      List<String> mimeTypes = new ArrayList<>();
+      for (int i = 0; i < multipart.getCount(); ++i) {
+        BodyPart bodyPart = multipart.getBodyPart(i);
+        String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
+        mimeTypes.add(bodyPartMimeType);
+        if (bodyPartMimeType.startsWith("audio/")) {
+          byte[] bytes = getDataFromBody(bodyPart.getBody());
+          LogUtils.d(TAG, String.format("Fetched %s bytes of data", bytes.length));
+          return new VoicemailPayload(bodyPartMimeType, bytes);
+        }
+      }
+      LogUtils.e(TAG, "No audio attachment found on this voicemail, mimeTypes:" + mimeTypes);
+      return null;
+    }
+  }
+
+  /** Listener for the transcription being fetched. */
+  private final class TranscriptionFetchedListener implements ImapFolder.MessageRetrievalListener {
+
+    private String mVoicemailTranscription;
+
+    /** Returns the fetched voicemail transcription. */
+    public String getVoicemailTranscription() {
+      return mVoicemailTranscription;
+    }
+
+    @Override
+    public void messageRetrieved(Message message) {
+      LogUtils.d(TAG, "Fetched transcription for " + message.getUid());
+      try {
+        mVoicemailTranscription = new String(getDataFromBody(message.getBody()));
+      } catch (MessagingException e) {
+        LogUtils.e(TAG, "Messaging Exception:", e);
+      } catch (IOException e) {
+        LogUtils.e(TAG, "IO Exception:", e);
+      }
+    }
+  }
+
+  private ImapFolder openImapFolder(String modeReadWrite) {
+    try {
+      if (mImapStore == null) {
+        return null;
+      }
+      ImapFolder folder = new ImapFolder(mImapStore, ImapConstants.INBOX);
+      folder.open(modeReadWrite);
+      return folder;
+    } catch (MessagingException e) {
+      LogUtils.e(TAG, e, "Messaging Exception");
+    }
+    return null;
+  }
+
+  private Message[] convertToImapMessages(List<Voicemail> voicemails) {
+    Message[] messages = new Message[voicemails.size()];
+    for (int i = 0; i < voicemails.size(); ++i) {
+      messages[i] = new MimeMessage();
+      messages[i].setUid(voicemails.get(i).getSourceData());
+    }
+    return messages;
+  }
+
+  private void closeImapFolder() {
+    if (mFolder != null) {
+      mFolder.close(true);
+    }
+  }
+
+  private byte[] getDataFromBody(Body body) throws IOException, MessagingException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    BufferedOutputStream bufferedOut = new BufferedOutputStream(out);
+    try {
+      body.writeTo(bufferedOut);
+      return Base64.decode(out.toByteArray(), Base64.DEFAULT);
+    } finally {
+      IOUtils.closeQuietly(bufferedOut);
+      IOUtils.closeQuietly(out);
+    }
+  }
+}
diff --git a/java/com/android/voicemailomtp/imap/VoicemailPayload.java b/java/com/android/voicemail/impl/imap/VoicemailPayload.java
similarity index 61%
rename from java/com/android/voicemailomtp/imap/VoicemailPayload.java
rename to java/com/android/voicemail/impl/imap/VoicemailPayload.java
index 04c69de..69befb4 100644
--- a/java/com/android/voicemailomtp/imap/VoicemailPayload.java
+++ b/java/com/android/voicemail/impl/imap/VoicemailPayload.java
@@ -14,25 +14,23 @@
  * limitations under the License.
  */
 
-package com.android.voicemailomtp.imap;
+package com.android.voicemail.impl.imap;
 
-/**
- * The payload for a voicemail, usually audio data.
- */
+/** The payload for a voicemail, usually audio data. */
 public class VoicemailPayload {
-    private final String mMimeType;
-    private final byte[] mBytes;
+  private final String mMimeType;
+  private final byte[] mBytes;
 
-    public VoicemailPayload(String mimeType, byte[] bytes) {
-        mMimeType = mimeType;
-        mBytes = bytes;
-    }
+  public VoicemailPayload(String mimeType, byte[] bytes) {
+    mMimeType = mimeType;
+    mBytes = bytes;
+  }
 
-    public byte[] getBytes() {
-        return mBytes;
-    }
+  public byte[] getBytes() {
+    return mBytes;
+  }
 
-    public String getMimeType() {
-        return mMimeType;
-    }
-}
\ No newline at end of file
+  public String getMimeType() {
+    return mMimeType;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/Address.java b/java/com/android/voicemail/impl/mail/Address.java
new file mode 100644
index 0000000..3a7a866
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Address.java
@@ -0,0 +1,520 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.VisibleForTesting;
+import android.text.Html;
+import android.text.TextUtils;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+import org.apache.james.mime4j.codec.EncoderUtil;
+import org.apache.james.mime4j.decoder.DecoderUtil;
+
+/**
+ * This class represent email address.
+ *
+ * <p>RFC822 email address may have following format. "name" <address> (comment) "name" <address>
+ * name <address> address Name and comment part should be MIME/base64 encoded in header if
+ * necessary.
+ */
+public class Address implements Parcelable {
+  public static final String ADDRESS_DELIMETER = ",";
+  /** Address part, in the form local_part@domain_part. No surrounding angle brackets. */
+  private String mAddress;
+
+  /**
+   * Name part. No surrounding double quote, and no MIME/base64 encoding. This must be null if
+   * Address has no name part.
+   */
+  private String mPersonal;
+
+  /**
+   * When personal is set, it will return the first token of the personal string. Otherwise, it will
+   * return the e-mail address up to the '@' sign.
+   */
+  private String mSimplifiedName;
+
+  // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$'
+  private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$");
+  // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$'
+  private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$");
+  // Regex that matches escaped character '\\([\\"])'
+  private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])");
+
+  // TODO: LOCAL_PART and DOMAIN_PART_PART are too permissive and can be improved.
+  // TODO: Fix this to better constrain comments.
+  /** Regex for the local part of an email address. */
+  private static final String LOCAL_PART = "[^@]+";
+  /** Regex for each part of the domain part, i.e. the thing between the dots. */
+  private static final String DOMAIN_PART_PART = "[[\\w][\\d]\\-\\(\\)\\[\\]]+";
+  /** Regex for the domain part, which is two or more {@link #DOMAIN_PART_PART} separated by . */
+  private static final String DOMAIN_PART = "(" + DOMAIN_PART_PART + "\\.)+" + DOMAIN_PART_PART;
+
+  /** Pattern to check if an email address is valid. */
+  private static final Pattern EMAIL_ADDRESS =
+      Pattern.compile("\\A" + LOCAL_PART + "@" + DOMAIN_PART + "\\z");
+
+  private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
+
+  // delimiters are chars that do not appear in an email address, used by fromHeader
+  private static final char LIST_DELIMITER_EMAIL = '\1';
+  private static final char LIST_DELIMITER_PERSONAL = '\2';
+
+  private static final String LOG_TAG = "Email Address";
+
+  @VisibleForTesting
+  public Address(String address) {
+    setAddress(address);
+  }
+
+  public Address(String address, String personal) {
+    setPersonal(personal);
+    setAddress(address);
+  }
+
+  /**
+   * Returns a simplified string for this e-mail address. When a name is known, it will return the
+   * first token of that name. Otherwise, it will return the e-mail address up to the '@' sign.
+   */
+  public String getSimplifiedName() {
+    if (mSimplifiedName == null) {
+      if (TextUtils.isEmpty(mPersonal) && !TextUtils.isEmpty(mAddress)) {
+        int atSign = mAddress.indexOf('@');
+        mSimplifiedName = (atSign != -1) ? mAddress.substring(0, atSign) : "";
+      } else if (!TextUtils.isEmpty(mPersonal)) {
+
+        // TODO: use Contacts' NameSplitter for more reliable first-name extraction
+
+        int end = mPersonal.indexOf(' ');
+        while (end > 0 && mPersonal.charAt(end - 1) == ',') {
+          end--;
+        }
+        mSimplifiedName = (end < 1) ? mPersonal : mPersonal.substring(0, end);
+
+      } else {
+        LogUtils.w(LOG_TAG, "Unable to get a simplified name");
+        mSimplifiedName = "";
+      }
+    }
+    return mSimplifiedName;
+  }
+
+  public static synchronized Address getEmailAddress(String rawAddress) {
+    if (TextUtils.isEmpty(rawAddress)) {
+      return null;
+    }
+    String name, address;
+    final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(rawAddress);
+    if (tokens.length > 0) {
+      final String tokenizedName = tokens[0].getName();
+      name = tokenizedName != null ? Html.fromHtml(tokenizedName.trim()).toString() : "";
+      address = Html.fromHtml(tokens[0].getAddress()).toString();
+    } else {
+      name = "";
+      address = rawAddress == null ? "" : Html.fromHtml(rawAddress).toString();
+    }
+    return new Address(address, name);
+  }
+
+  public String getAddress() {
+    return mAddress;
+  }
+
+  public void setAddress(String address) {
+    mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");
+  }
+
+  /**
+   * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
+   *
+   * @return Name part of email address. Returns null if it is omitted.
+   */
+  public String getPersonal() {
+    return mPersonal;
+  }
+
+  /**
+   * Set personal part from UTF-16 string. Optional surrounding double quote will be removed. It
+   * will be also unquoted and MIME/base64 decoded.
+   *
+   * @param personal name part of email address as UTF-16 string. Null is acceptable.
+   */
+  public void setPersonal(String personal) {
+    mPersonal = decodeAddressPersonal(personal);
+  }
+
+  /**
+   * Decodes name from UTF-16 string. Optional surrounding double quote will be removed. It will be
+   * also unquoted and MIME/base64 decoded.
+   *
+   * @param personal name part of email address as UTF-16 string. Null is acceptable.
+   */
+  public static String decodeAddressPersonal(String personal) {
+    if (personal != null) {
+      personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
+      personal = UNQUOTE.matcher(personal).replaceAll("$1");
+      personal = DecoderUtil.decodeEncodedWords(personal);
+      if (personal.length() == 0) {
+        personal = null;
+      }
+    }
+    return personal;
+  }
+
+  /**
+   * This method is used to check that all the addresses that the user entered in a list (e.g. To:)
+   * are valid, so that none is dropped.
+   */
+  @VisibleForTesting
+  public static boolean isAllValid(String addressList) {
+    // This code mimics the parse() method below.
+    // I don't know how to better avoid the code-duplication.
+    if (addressList != null && addressList.length() > 0) {
+      Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
+      for (int i = 0, length = tokens.length; i < length; ++i) {
+        Rfc822Token token = tokens[i];
+        String address = token.getAddress();
+        if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
+          return false;
+        }
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Parse a comma-delimited list of addresses in RFC822 format and return an array of Address
+   * objects.
+   *
+   * @param addressList Address list in comma-delimited string.
+   * @return An array of 0 or more Addresses.
+   */
+  public static Address[] parse(String addressList) {
+    if (addressList == null || addressList.length() == 0) {
+      return EMPTY_ADDRESS_ARRAY;
+    }
+    Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
+    ArrayList<Address> addresses = new ArrayList<Address>();
+    for (int i = 0, length = tokens.length; i < length; ++i) {
+      Rfc822Token token = tokens[i];
+      String address = token.getAddress();
+      if (!TextUtils.isEmpty(address)) {
+        if (isValidAddress(address)) {
+          String name = token.getName();
+          if (TextUtils.isEmpty(name)) {
+            name = null;
+          }
+          addresses.add(new Address(address, name));
+        }
+      }
+    }
+    return addresses.toArray(new Address[addresses.size()]);
+  }
+
+  /** Checks whether a string email address is valid. E.g. name@domain.com is valid. */
+  @VisibleForTesting
+  static boolean isValidAddress(final String address) {
+    return EMAIL_ADDRESS.matcher(address).find();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o instanceof Address) {
+      // It seems that the spec says that the "user" part is case-sensitive,
+      // while the domain part in case-insesitive.
+      // So foo@yahoo.com and Foo@yahoo.com are different.
+      // This may seem non-intuitive from the user POV, so we
+      // may re-consider it if it creates UI trouble.
+      // A problem case is "replyAll" sending to both
+      // a@b.c and to A@b.c, which turn out to be the same on the server.
+      // Leave unchanged for now (i.e. case-sensitive).
+      return getAddress().equals(((Address) o).getAddress());
+    }
+    return super.equals(o);
+  }
+
+  @Override
+  public int hashCode() {
+    return getAddress().hashCode();
+  }
+
+  /**
+   * Get human readable address string. Do not use this for email header.
+   *
+   * @return Human readable address string. Not quoted and not encoded.
+   */
+  @Override
+  public String toString() {
+    if (mPersonal != null && !mPersonal.equals(mAddress)) {
+      if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
+        return ensureQuotedString(mPersonal) + " <" + mAddress + ">";
+      } else {
+        return mPersonal + " <" + mAddress + ">";
+      }
+    } else {
+      return mAddress;
+    }
+  }
+
+  /**
+   * Ensures that the given string starts and ends with the double quote character. The string is
+   * not modified in any way except to add the double quote character to start and end if it's not
+   * already there.
+   *
+   * <p>sample -> "sample" "sample" -> "sample" ""sample"" -> "sample" "sample"" -> "sample"
+   * sa"mp"le -> "sa"mp"le" "sa"mp"le" -> "sa"mp"le" (empty string) -> "" " -> ""
+   */
+  private static String ensureQuotedString(String s) {
+    if (s == null) {
+      return null;
+    }
+    if (!s.matches("^\".*\"$")) {
+      return "\"" + s + "\"";
+    } else {
+      return s;
+    }
+  }
+
+  /**
+   * Get human readable comma-delimited address string.
+   *
+   * @param addresses Address array
+   * @return Human readable comma-delimited address string.
+   */
+  @VisibleForTesting
+  public static String toString(Address[] addresses) {
+    return toString(addresses, ADDRESS_DELIMETER);
+  }
+
+  /**
+   * Get human readable address strings joined with the specified separator.
+   *
+   * @param addresses Address array
+   * @param separator Separator
+   * @return Human readable comma-delimited address string.
+   */
+  public static String toString(Address[] addresses, String separator) {
+    if (addresses == null || addresses.length == 0) {
+      return null;
+    }
+    if (addresses.length == 1) {
+      return addresses[0].toString();
+    }
+    StringBuilder sb = new StringBuilder(addresses[0].toString());
+    for (int i = 1; i < addresses.length; i++) {
+      sb.append(separator);
+      // TODO: investigate why this .trim() is needed.
+      sb.append(addresses[i].toString().trim());
+    }
+    return sb.toString();
+  }
+
+  /**
+   * Get RFC822/MIME compatible address string.
+   *
+   * @return RFC822/MIME compatible address string. It may be surrounded by double quote or quoted
+   *     and MIME/base64 encoded if necessary.
+   */
+  public String toHeader() {
+    if (mPersonal != null) {
+      return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
+    } else {
+      return mAddress;
+    }
+  }
+
+  /**
+   * Get RFC822/MIME compatible comma-delimited address string.
+   *
+   * @param addresses Address array
+   * @return RFC822/MIME compatible comma-delimited address string. it may be surrounded by double
+   *     quoted or quoted and MIME/base64 encoded if necessary.
+   */
+  public static String toHeader(Address[] addresses) {
+    if (addresses == null || addresses.length == 0) {
+      return null;
+    }
+    if (addresses.length == 1) {
+      return addresses[0].toHeader();
+    }
+    StringBuilder sb = new StringBuilder(addresses[0].toHeader());
+    for (int i = 1; i < addresses.length; i++) {
+      // We need space character to be able to fold line.
+      sb.append(", ");
+      sb.append(addresses[i].toHeader());
+    }
+    return sb.toString();
+  }
+
+  /**
+   * Get Human friendly address string.
+   *
+   * @return the personal part of this Address, or the address part if the personal part is not
+   *     available
+   */
+  @VisibleForTesting
+  public String toFriendly() {
+    if (mPersonal != null && mPersonal.length() > 0) {
+      return mPersonal;
+    } else {
+      return mAddress;
+    }
+  }
+
+  /**
+   * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
+   * details on the per-address conversion).
+   *
+   * @param addresses Array of Address[] values
+   * @return A comma-delimited string listing all of the addresses supplied. Null if source was null
+   *     or empty.
+   */
+  @VisibleForTesting
+  public static String toFriendly(Address[] addresses) {
+    if (addresses == null || addresses.length == 0) {
+      return null;
+    }
+    if (addresses.length == 1) {
+      return addresses[0].toFriendly();
+    }
+    StringBuilder sb = new StringBuilder(addresses[0].toFriendly());
+    for (int i = 1; i < addresses.length; i++) {
+      sb.append(", ");
+      sb.append(addresses[i].toFriendly());
+    }
+    return sb.toString();
+  }
+
+  /** Returns exactly the same result as Address.toString(Address.fromHeader(addressList)). */
+  @VisibleForTesting
+  public static String fromHeaderToString(String addressList) {
+    return toString(fromHeader(addressList));
+  }
+
+  /** Returns exactly the same result as Address.toHeader(Address.parse(addressList)). */
+  @VisibleForTesting
+  public static String parseToHeader(String addressList) {
+    return Address.toHeader(Address.parse(addressList));
+  }
+
+  /**
+   * Returns null if the addressList has 0 addresses, otherwise returns the first address. The same
+   * as Address.fromHeader(addressList)[0] for non-empty list. This is an utility method that offers
+   * some performance optimization opportunities.
+   */
+  @VisibleForTesting
+  public static Address firstAddress(String addressList) {
+    Address[] array = fromHeader(addressList);
+    return array.length > 0 ? array[0] : null;
+  }
+
+  /**
+   * This method exists to convert an address list formatted in a deprecated legacy format to the
+   * standard RFC822 header format. {@link #fromHeader(String)} is capable of reading the legacy
+   * format and the RFC822 format. {@link #toHeader()} always produces the RFC822 format.
+   *
+   * <p>This implementation is brute-force, and could be replaced with a more efficient version if
+   * desired.
+   */
+  public static String reformatToHeader(String addressList) {
+    return toHeader(fromHeader(addressList));
+  }
+
+  /**
+   * @param addressList a CSV of RFC822 addresses or the deprecated legacy string format
+   * @return array of addresses parsed from <code>addressList</code>
+   */
+  @VisibleForTesting
+  public static Address[] fromHeader(String addressList) {
+    if (addressList == null || addressList.length() == 0) {
+      return EMPTY_ADDRESS_ARRAY;
+    }
+    // IF we're CSV, just parse
+    if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1)
+        && (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) {
+      return Address.parse(addressList);
+    }
+    // Otherwise, do backward-compatible unpack
+    ArrayList<Address> addresses = new ArrayList<Address>();
+    int length = addressList.length();
+    int pairStartIndex = 0;
+    int pairEndIndex;
+
+    /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
+       is used, not for every email address; i.e. not for every iteration of the while().
+       This reduces the theoretical complexity from quadratic to linear,
+       and provides some speed-up in practice by removing redundant scans of the string.
+    */
+    int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
+
+    while (pairStartIndex < length) {
+      pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
+      if (pairEndIndex == -1) {
+        pairEndIndex = length;
+      }
+      Address address;
+      if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
+        // in this case the DELIMITER_PERSONAL is in a future pair,
+        // so don't use personal, and don't update addressEndIndex
+        address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
+      } else {
+        address =
+            new Address(
+                addressList.substring(pairStartIndex, addressEndIndex),
+                addressList.substring(addressEndIndex + 1, pairEndIndex));
+        // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
+        addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
+      }
+      addresses.add(address);
+      pairStartIndex = pairEndIndex + 1;
+    }
+    return addresses.toArray(new Address[addresses.size()]);
+  }
+
+  public static final Creator<Address> CREATOR =
+      new Creator<Address>() {
+        @Override
+        public Address createFromParcel(Parcel parcel) {
+          return new Address(parcel);
+        }
+
+        @Override
+        public Address[] newArray(int size) {
+          return new Address[size];
+        }
+      };
+
+  public Address(Parcel in) {
+    setPersonal(in.readString());
+    setAddress(in.readString());
+  }
+
+  @Override
+  public int describeContents() {
+    return 0;
+  }
+
+  @Override
+  public void writeToParcel(Parcel out, int flags) {
+    out.writeString(mPersonal);
+    out.writeString(mAddress);
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/AuthenticationFailedException.java b/java/com/android/voicemail/impl/mail/AuthenticationFailedException.java
new file mode 100644
index 0000000..c9fa087
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/AuthenticationFailedException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail;
+
+public class AuthenticationFailedException extends MessagingException {
+  public static final long serialVersionUID = -1;
+
+  public AuthenticationFailedException(String message) {
+    super(MessagingException.AUTHENTICATION_FAILED, message);
+  }
+
+  public AuthenticationFailedException(int exceptionType, String message) {
+    super(exceptionType, message);
+  }
+
+  public AuthenticationFailedException(String message, Throwable throwable) {
+    super(MessagingException.AUTHENTICATION_FAILED, message, throwable);
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/Base64Body.java b/java/com/android/voicemail/impl/mail/Base64Body.java
new file mode 100644
index 0000000..def94db
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Base64Body.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import android.util.Base64;
+import android.util.Base64OutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.commons.io.IOUtils;
+
+public class Base64Body implements Body {
+  private final InputStream mSource;
+  // Because we consume the input stream, we can only write out once
+  private boolean mAlreadyWritten;
+
+  public Base64Body(InputStream source) {
+    mSource = source;
+  }
+
+  @Override
+  public InputStream getInputStream() throws MessagingException {
+    return mSource;
+  }
+
+  /**
+   * This method consumes the input stream, so can only be called once
+   *
+   * @param out Stream to write to
+   * @throws IllegalStateException If called more than once
+   * @throws IOException
+   * @throws MessagingException
+   */
+  @Override
+  public void writeTo(OutputStream out)
+      throws IllegalStateException, IOException, MessagingException {
+    if (mAlreadyWritten) {
+      throw new IllegalStateException("Base64Body can only be written once");
+    }
+    mAlreadyWritten = true;
+    try {
+      final Base64OutputStream b64out = new Base64OutputStream(out, Base64.DEFAULT);
+      IOUtils.copyLarge(mSource, b64out);
+    } finally {
+      mSource.close();
+    }
+  }
+}
diff --git a/java/com/android/voicemailomtp/mail/Body.java b/java/com/android/voicemail/impl/mail/Body.java
similarity index 79%
rename from java/com/android/voicemailomtp/mail/Body.java
rename to java/com/android/voicemail/impl/mail/Body.java
index 393e182..3ad81bc 100644
--- a/java/com/android/voicemailomtp/mail/Body.java
+++ b/java/com/android/voicemail/impl/mail/Body.java
@@ -13,13 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.voicemailomtp.mail;
+package com.android.voicemail.impl.mail;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 
 public interface Body {
-    public InputStream getInputStream() throws MessagingException;
-    public void writeTo(OutputStream out) throws IOException, MessagingException;
+  public InputStream getInputStream() throws MessagingException;
+
+  public void writeTo(OutputStream out) throws IOException, MessagingException;
 }
diff --git a/java/com/android/voicemailomtp/mail/BodyPart.java b/java/com/android/voicemail/impl/mail/BodyPart.java
similarity index 82%
rename from java/com/android/voicemailomtp/mail/BodyPart.java
rename to java/com/android/voicemail/impl/mail/BodyPart.java
index 62390a4..3d15d4b 100644
--- a/java/com/android/voicemailomtp/mail/BodyPart.java
+++ b/java/com/android/voicemail/impl/mail/BodyPart.java
@@ -13,12 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.voicemailomtp.mail;
+package com.android.voicemail.impl.mail;
 
 public abstract class BodyPart implements Part {
-    protected Multipart mParent;
+  protected Multipart mParent;
 
-    public Multipart getParent() {
-        return mParent;
-    }
+  public Multipart getParent() {
+    return mParent;
+  }
 }
diff --git a/java/com/android/voicemailomtp/mail/CertificateValidationException.java b/java/com/android/voicemail/impl/mail/CertificateValidationException.java
similarity index 63%
rename from java/com/android/voicemailomtp/mail/CertificateValidationException.java
rename to java/com/android/voicemail/impl/mail/CertificateValidationException.java
index 8ebe548..6f3bb2f 100644
--- a/java/com/android/voicemailomtp/mail/CertificateValidationException.java
+++ b/java/com/android/voicemail/impl/mail/CertificateValidationException.java
@@ -14,16 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.voicemailomtp.mail;
+package com.android.voicemail.impl.mail;
 
 public class CertificateValidationException extends MessagingException {
-    public static final long serialVersionUID = -1;
+  public static final long serialVersionUID = -1;
 
-    public CertificateValidationException(String message) {
-        super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message);
-    }
+  public CertificateValidationException(String message) {
+    super(CERTIFICATE_VALIDATION_ERROR, message);
+  }
 
-    public CertificateValidationException(String message, Throwable throwable) {
-        super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message, throwable);
-    }
-}
\ No newline at end of file
+  public CertificateValidationException(String message, Throwable throwable) {
+    super(CERTIFICATE_VALIDATION_ERROR, message, throwable);
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/FetchProfile.java b/java/com/android/voicemail/impl/mail/FetchProfile.java
new file mode 100644
index 0000000..28a7080
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/FetchProfile.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import java.util.ArrayList;
+
+/**
+ *
+ *
+ * <pre>
+ * A FetchProfile is a list of items that should be downloaded in bulk for a set of messages.
+ * FetchProfile can contain the following objects:
+ *      FetchProfile.Item:      Described below.
+ *      Message:                Indicates that the body of the entire message should be fetched.
+ *                              Synonymous with FetchProfile.Item.BODY.
+ *      Part:                   Indicates that the given Part should be fetched. The provider
+ *                              is expected have previously created the given BodyPart and stored
+ *                              any information it needs to download the content.
+ * </pre>
+ */
+public class FetchProfile extends ArrayList<Fetchable> {
+  /**
+   * Default items available for pre-fetching. It should be expected that any item fetched by using
+   * these items could potentially include all of the previous items.
+   */
+  public enum Item implements Fetchable {
+    /** Download the flags of the message. */
+    FLAGS,
+
+    /**
+     * Download the envelope of the message. This should include at minimum the size and the
+     * following headers: date, subject, from, content-type, to, cc
+     */
+    ENVELOPE,
+
+    /**
+     * Download the structure of the message. This maps directly to IMAP's BODYSTRUCTURE and may map
+     * to other providers. The provider should, if possible, fill in a properly formatted MIME
+     * structure in the message without actually downloading any message data. If the provider is
+     * not capable of this operation it should specifically set the body of the message to null so
+     * that upper levels can detect that a full body download is needed.
+     */
+    STRUCTURE,
+
+    /**
+     * A sane portion of the entire message, cut off at a provider determined limit. This should
+     * generally be around 50kB.
+     */
+    BODY_SANE,
+
+    /** The entire message. */
+    BODY,
+  }
+
+  /**
+   * @return the first {@link Part} in this collection, or null if it doesn't contain {@link Part}.
+   */
+  public Part getFirstPart() {
+    for (Fetchable o : this) {
+      if (o instanceof Part) {
+        return (Part) o;
+      }
+    }
+    return null;
+  }
+}
diff --git a/java/com/android/voicemailomtp/mail/Fetchable.java b/java/com/android/voicemail/impl/mail/Fetchable.java
similarity index 82%
rename from java/com/android/voicemailomtp/mail/Fetchable.java
rename to java/com/android/voicemail/impl/mail/Fetchable.java
index 1d8d000..237ef69 100644
--- a/java/com/android/voicemailomtp/mail/Fetchable.java
+++ b/java/com/android/voicemail/impl/mail/Fetchable.java
@@ -13,11 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.voicemailomtp.mail;
+package com.android.voicemail.impl.mail;
 
 /**
- * Interface for classes that can be added to {@link FetchProfile}.
- * i.e. {@link Part} and its subclasses, and {@link FetchProfile.Item}.
+ * Interface for classes that can be added to {@link FetchProfile}. i.e. {@link Part} and its
+ * subclasses, and {@link FetchProfile.Item}.
  */
-public interface Fetchable {
-}
+public interface Fetchable {}
diff --git a/java/com/android/voicemail/impl/mail/FixedLengthInputStream.java b/java/com/android/voicemail/impl/mail/FixedLengthInputStream.java
new file mode 100644
index 0000000..bd3c164
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/FixedLengthInputStream.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A filtering InputStream that stops allowing reads after the given length has been read. This is
+ * used to allow a client to read directly from an underlying protocol stream without reading past
+ * where the protocol handler intended the client to read.
+ */
+public class FixedLengthInputStream extends InputStream {
+  private final InputStream mIn;
+  private final int mLength;
+  private int mCount;
+
+  public FixedLengthInputStream(InputStream in, int length) {
+    this.mIn = in;
+    this.mLength = length;
+  }
+
+  @Override
+  public int available() throws IOException {
+    return mLength - mCount;
+  }
+
+  @Override
+  public int read() throws IOException {
+    if (mCount < mLength) {
+      mCount++;
+      return mIn.read();
+    } else {
+      return -1;
+    }
+  }
+
+  @Override
+  public int read(byte[] b, int offset, int length) throws IOException {
+    if (mCount < mLength) {
+      int d = mIn.read(b, offset, Math.min(mLength - mCount, length));
+      if (d == -1) {
+        return -1;
+      } else {
+        mCount += d;
+        return d;
+      }
+    } else {
+      return -1;
+    }
+  }
+
+  @Override
+  public int read(byte[] b) throws IOException {
+    return read(b, 0, b.length);
+  }
+
+  public int getLength() {
+    return mLength;
+  }
+
+  @Override
+  public String toString() {
+    return String.format("FixedLengthInputStream(in=%s, length=%d)", mIn.toString(), mLength);
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/Flag.java b/java/com/android/voicemail/impl/mail/Flag.java
new file mode 100644
index 0000000..72b5c1f
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Flag.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+/** Flags that can be applied to Messages. */
+public class Flag {
+  // If adding new flags: ALL FLAGS MUST BE UPPER CASE.
+  public static final String DELETED = "deleted";
+  public static final String SEEN = "seen";
+  public static final String ANSWERED = "answered";
+  public static final String FLAGGED = "flagged";
+  public static final String DRAFT = "draft";
+  public static final String RECENT = "recent";
+}
diff --git a/java/com/android/voicemail/impl/mail/MailTransport.java b/java/com/android/voicemail/impl/mail/MailTransport.java
new file mode 100644
index 0000000..3df36d5
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/MailTransport.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import android.content.Context;
+import android.net.Network;
+import android.support.annotation.VisibleForTesting;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.mail.store.ImapStore;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+/** Make connection and perform operations on mail server by reading and writing lines. */
+public class MailTransport {
+  private static final String TAG = "MailTransport";
+
+  // TODO protected eventually
+  /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000;
+  /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000;
+
+  private static final HostnameVerifier HOSTNAME_VERIFIER =
+      HttpsURLConnection.getDefaultHostnameVerifier();
+
+  private final Context mContext;
+  private final ImapHelper mImapHelper;
+  private final Network mNetwork;
+  private final String mHost;
+  private final int mPort;
+  private Socket mSocket;
+  private BufferedInputStream mIn;
+  private BufferedOutputStream mOut;
+  private final int mFlags;
+  private SocketCreator mSocketCreator;
+  private InetSocketAddress mAddress;
+
+  public MailTransport(
+      Context context,
+      ImapHelper imapHelper,
+      Network network,
+      String address,
+      int port,
+      int flags) {
+    mContext = context;
+    mImapHelper = imapHelper;
+    mNetwork = network;
+    mHost = address;
+    mPort = port;
+    mFlags = flags;
+  }
+
+  /**
+   * Returns a new transport, using the current transport as a model. The new transport is
+   * configured identically, but not opened or connected in any way.
+   */
+  @Override
+  public MailTransport clone() {
+    return new MailTransport(mContext, mImapHelper, mNetwork, mHost, mPort, mFlags);
+  }
+
+  public boolean canTrySslSecurity() {
+    return (mFlags & ImapStore.FLAG_SSL) != 0;
+  }
+
+  public boolean canTrustAllCertificates() {
+    return (mFlags & ImapStore.FLAG_TRUST_ALL) != 0;
+  }
+
+  /**
+   * Attempts to open a connection using the Uri supplied for connection parameters. Will attempt an
+   * SSL connection if indicated.
+   */
+  public void open() throws MessagingException {
+    LogUtils.d(TAG, "*** IMAP open " + mHost + ":" + String.valueOf(mPort));
+
+    List<InetSocketAddress> socketAddresses = new ArrayList<InetSocketAddress>();
+
+    if (mNetwork == null) {
+      socketAddresses.add(new InetSocketAddress(mHost, mPort));
+    } else {
+      try {
+        InetAddress[] inetAddresses = mNetwork.getAllByName(mHost);
+        if (inetAddresses.length == 0) {
+          throw new MessagingException(
+              MessagingException.IOERROR,
+              "Host name " + mHost + "cannot be resolved on designated network");
+        }
+        for (int i = 0; i < inetAddresses.length; i++) {
+          socketAddresses.add(new InetSocketAddress(inetAddresses[i], mPort));
+        }
+      } catch (IOException ioe) {
+        LogUtils.d(TAG, ioe.toString());
+        mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_RESOLVE_HOST_ON_NETWORK);
+        throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+      }
+    }
+
+    boolean success = false;
+    while (socketAddresses.size() > 0) {
+      mSocket = createSocket();
+      try {
+        mAddress = socketAddresses.remove(0);
+        mSocket.connect(mAddress, SOCKET_CONNECT_TIMEOUT);
+
+        if (canTrySslSecurity()) {
+          /*
+          SSLSocket cannot be created with a connection timeout, so instead of doing a
+          direct SSL connection, we connect with a normal connection and upgrade it into
+          SSL
+           */
+          reopenTls();
+        } else {
+          mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
+          mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
+          mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
+        }
+        success = true;
+        return;
+      } catch (IOException ioe) {
+        LogUtils.d(TAG, ioe.toString());
+        if (socketAddresses.size() == 0) {
+          // Only throw an error when there are no more sockets to try.
+          mImapHelper.handleEvent(OmtpEvents.DATA_ALL_SOCKET_CONNECTION_FAILED);
+          throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+        }
+      } finally {
+        if (!success) {
+          try {
+            mSocket.close();
+            mSocket = null;
+          } catch (IOException ioe) {
+            throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+          }
+        }
+      }
+    }
+  }
+
+  // For testing. We need something that can replace the behavior of "new Socket()"
+  @VisibleForTesting
+  interface SocketCreator {
+
+    Socket createSocket() throws MessagingException;
+  }
+
+  @VisibleForTesting
+  void setSocketCreator(SocketCreator creator) {
+    mSocketCreator = creator;
+  }
+
+  protected Socket createSocket() throws MessagingException {
+    if (mSocketCreator != null) {
+      return mSocketCreator.createSocket();
+    }
+
+    if (mNetwork == null) {
+      LogUtils.v(TAG, "createSocket: network not specified");
+      return new Socket();
+    }
+
+    try {
+      LogUtils.v(TAG, "createSocket: network specified");
+      return mNetwork.getSocketFactory().createSocket();
+    } catch (IOException ioe) {
+      LogUtils.d(TAG, ioe.toString());
+      throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+    }
+  }
+
+  /** Attempts to reopen a normal connection into a TLS connection. */
+  public void reopenTls() throws MessagingException {
+    try {
+      LogUtils.d(TAG, "open: converting to TLS socket");
+      mSocket =
+          HttpsURLConnection.getDefaultSSLSocketFactory()
+              .createSocket(mSocket, mAddress.getHostName(), mAddress.getPort(), true);
+      // After the socket connects to an SSL server, confirm that the hostname is as
+      // expected
+      if (!canTrustAllCertificates()) {
+        verifyHostname(mSocket, mHost);
+      }
+      mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
+      mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
+      mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
+
+    } catch (SSLException e) {
+      LogUtils.d(TAG, e.toString());
+      throw new CertificateValidationException(e.getMessage(), e);
+    } catch (IOException ioe) {
+      LogUtils.d(TAG, ioe.toString());
+      throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+    }
+  }
+
+  /**
+   * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this service
+   * but is not in the public API.
+   *
+   * <p>Verify the hostname of the certificate used by the other end of a connected socket. It is
+   * harmless to call this method redundantly if the hostname has already been verified.
+   *
+   * <p>Wildcard certificates are allowed to verify any matching hostname, so "foo.bar.example.com"
+   * is verified if the peer has a certificate for "*.example.com".
+   *
+   * @param socket An SSL socket which has been connected to a server
+   * @param hostname The expected hostname of the remote server
+   * @throws IOException if something goes wrong handshaking with the server
+   * @throws SSLPeerUnverifiedException if the server cannot prove its identity
+   */
+  private void verifyHostname(Socket socket, String hostname) throws IOException {
+    // The code at the start of OpenSSLSocketImpl.startHandshake()
+    // ensures that the call is idempotent, so we can safely call it.
+    SSLSocket ssl = (SSLSocket) socket;
+    ssl.startHandshake();
+
+    SSLSession session = ssl.getSession();
+    if (session == null) {
+      mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_ESTABLISH_SSL_SESSION);
+      throw new SSLException("Cannot verify SSL socket without session");
+    }
+    // TODO: Instead of reporting the name of the server we think we're connecting to,
+    // we should be reporting the bad name in the certificate.  Unfortunately this is buried
+    // in the verifier code and is not available in the verifier API, and extracting the
+    // CN & alts is beyond the scope of this patch.
+    if (!HOSTNAME_VERIFIER.verify(hostname, session)) {
+      mImapHelper.handleEvent(OmtpEvents.DATA_SSL_INVALID_HOST_NAME);
+      throw new SSLPeerUnverifiedException(
+          "Certificate hostname not useable for server: " + session.getPeerPrincipal());
+    }
+  }
+
+  public boolean isOpen() {
+    return (mIn != null
+        && mOut != null
+        && mSocket != null
+        && mSocket.isConnected()
+        && !mSocket.isClosed());
+  }
+
+  /** Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. */
+  public void close() {
+    try {
+      mIn.close();
+    } catch (Exception e) {
+      // May fail if the connection is already closed.
+    }
+    try {
+      mOut.close();
+    } catch (Exception e) {
+      // May fail if the connection is already closed.
+    }
+    try {
+      mSocket.close();
+    } catch (Exception e) {
+      // May fail if the connection is already closed.
+    }
+    mIn = null;
+    mOut = null;
+    mSocket = null;
+  }
+
+  public String getHost() {
+    return mHost;
+  }
+
+  public InputStream getInputStream() {
+    return mIn;
+  }
+
+  public OutputStream getOutputStream() {
+    return mOut;
+  }
+
+  /** Writes a single line to the server using \r\n termination. */
+  public void writeLine(String s, String sensitiveReplacement) throws IOException {
+    if (sensitiveReplacement != null) {
+      LogUtils.d(TAG, ">>> " + sensitiveReplacement);
+    } else {
+      LogUtils.d(TAG, ">>> " + s);
+    }
+
+    OutputStream out = getOutputStream();
+    out.write(s.getBytes());
+    out.write('\r');
+    out.write('\n');
+    out.flush();
+  }
+
+  /**
+   * Reads a single line from the server, using either \r\n or \n as the delimiter. The delimiter
+   * char(s) are not included in the result.
+   */
+  public String readLine(boolean loggable) throws IOException {
+    StringBuffer sb = new StringBuffer();
+    InputStream in = getInputStream();
+    int d;
+    while ((d = in.read()) != -1) {
+      if (((char) d) == '\r') {
+        continue;
+      } else if (((char) d) == '\n') {
+        break;
+      } else {
+        sb.append((char) d);
+      }
+    }
+    if (d == -1) {
+      LogUtils.d(TAG, "End of stream reached while trying to read line.");
+    }
+    String ret = sb.toString();
+    if (loggable) {
+      LogUtils.d(TAG, "<<< " + ret);
+    }
+    return ret;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/MeetingInfo.java b/java/com/android/voicemail/impl/mail/MeetingInfo.java
new file mode 100644
index 0000000..9fe953d
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/MeetingInfo.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+public class MeetingInfo {
+  // Predefined tags; others can be added
+  public static final String MEETING_DTSTAMP = "DTSTAMP";
+  public static final String MEETING_UID = "UID";
+  public static final String MEETING_ORGANIZER_EMAIL = "ORGMAIL";
+  public static final String MEETING_DTSTART = "DTSTART";
+  public static final String MEETING_DTEND = "DTEND";
+  public static final String MEETING_TITLE = "TITLE";
+  public static final String MEETING_LOCATION = "LOC";
+  public static final String MEETING_RESPONSE_REQUESTED = "RESPONSE";
+  public static final String MEETING_ALL_DAY = "ALLDAY";
+}
diff --git a/java/com/android/voicemail/impl/mail/Message.java b/java/com/android/voicemail/impl/mail/Message.java
new file mode 100644
index 0000000..aea5d3e
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Message.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import android.support.annotation.VisibleForTesting;
+import java.util.Date;
+import java.util.HashSet;
+
+public abstract class Message implements Part, Body {
+  public static final Message[] EMPTY_ARRAY = new Message[0];
+
+  public static final String RECIPIENT_TYPE_TO = "to";
+  public static final String RECIPIENT_TYPE_CC = "cc";
+  public static final String RECIPIENT_TYPE_BCC = "bcc";
+
+  public enum RecipientType {
+    TO,
+    CC,
+    BCC,
+  }
+
+  protected String mUid;
+
+  private HashSet<String> mFlags = null;
+
+  protected Date mInternalDate;
+
+  public String getUid() {
+    return mUid;
+  }
+
+  public void setUid(String uid) {
+    this.mUid = uid;
+  }
+
+  public abstract String getSubject() throws MessagingException;
+
+  public abstract void setSubject(String subject) throws MessagingException;
+
+  public Date getInternalDate() {
+    return mInternalDate;
+  }
+
+  public void setInternalDate(Date internalDate) {
+    this.mInternalDate = internalDate;
+  }
+
+  public abstract Date getReceivedDate() throws MessagingException;
+
+  public abstract Date getSentDate() throws MessagingException;
+
+  public abstract void setSentDate(Date sentDate) throws MessagingException;
+
+  public abstract Address[] getRecipients(String type) throws MessagingException;
+
+  public abstract void setRecipients(String type, Address[] addresses) throws MessagingException;
+
+  public void setRecipient(String type, Address address) throws MessagingException {
+    setRecipients(type, new Address[] {address});
+  }
+
+  public abstract Address[] getFrom() throws MessagingException;
+
+  public abstract void setFrom(Address from) throws MessagingException;
+
+  public abstract Address[] getReplyTo() throws MessagingException;
+
+  public abstract void setReplyTo(Address[] from) throws MessagingException;
+
+  // Always use these instead of getHeader("Message-ID") or setHeader("Message-ID");
+  public abstract void setMessageId(String messageId) throws MessagingException;
+
+  public abstract String getMessageId() throws MessagingException;
+
+  @Override
+  public boolean isMimeType(String mimeType) throws MessagingException {
+    return getContentType().startsWith(mimeType);
+  }
+
+  private HashSet<String> getFlagSet() {
+    if (mFlags == null) {
+      mFlags = new HashSet<String>();
+    }
+    return mFlags;
+  }
+
+  /*
+   * TODO Refactor Flags at some point to be able to store user defined flags.
+   */
+  public String[] getFlags() {
+    return getFlagSet().toArray(new String[] {});
+  }
+
+  /**
+   * Set/clear a flag directly, without involving overrides of {@link #setFlag} in subclasses. Only
+   * used for testing.
+   */
+  @VisibleForTesting
+  private final void setFlagDirectlyForTest(String flag, boolean set) throws MessagingException {
+    if (set) {
+      getFlagSet().add(flag);
+    } else {
+      getFlagSet().remove(flag);
+    }
+  }
+
+  public void setFlag(String flag, boolean set) throws MessagingException {
+    setFlagDirectlyForTest(flag, set);
+  }
+
+  /**
+   * This method calls setFlag(String, boolean)
+   *
+   * @param flags
+   * @param set
+   */
+  public void setFlags(String[] flags, boolean set) throws MessagingException {
+    for (String flag : flags) {
+      setFlag(flag, set);
+    }
+  }
+
+  public boolean isSet(String flag) {
+    return getFlagSet().contains(flag);
+  }
+
+  public abstract void saveChanges() throws MessagingException;
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + ':' + mUid;
+  }
+}
diff --git a/java/com/android/voicemailomtp/mail/MessageDateComparator.java b/java/com/android/voicemail/impl/mail/MessageDateComparator.java
similarity index 63%
rename from java/com/android/voicemailomtp/mail/MessageDateComparator.java
rename to java/com/android/voicemail/impl/mail/MessageDateComparator.java
index 3707103..89231f6 100644
--- a/java/com/android/voicemailomtp/mail/MessageDateComparator.java
+++ b/java/com/android/voicemail/impl/mail/MessageDateComparator.java
@@ -13,22 +13,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.voicemailomtp.mail;
+package com.android.voicemail.impl.mail;
 
 import java.util.Comparator;
 
 public class MessageDateComparator implements Comparator<Message> {
-    @Override
-    public int compare(Message o1, Message o2) {
-        try {
-            if (o1.getSentDate() == null) {
-                return 1;
-            } else if (o2.getSentDate() == null) {
-                return -1;
-            } else
-                return o2.getSentDate().compareTo(o1.getSentDate());
-        } catch (Exception e) {
-            return 0;
-        }
+  @Override
+  public int compare(Message o1, Message o2) {
+    try {
+      if (o1.getSentDate() == null) {
+        return 1;
+      } else if (o2.getSentDate() == null) {
+        return -1;
+      } else {
+        return o2.getSentDate().compareTo(o1.getSentDate());
+      }
+    } catch (Exception e) {
+      return 0;
     }
+  }
 }
diff --git a/java/com/android/voicemail/impl/mail/MessagingException.java b/java/com/android/voicemail/impl/mail/MessagingException.java
new file mode 100644
index 0000000..c1e3051
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/MessagingException.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail;
+
+/**
+ * This exception is used for most types of failures that occur during server interactions.
+ *
+ * <p>Data passed through this exception should be considered non-localized. Any strings should
+ * either be internal-only (for debugging) or server-generated.
+ *
+ * <p>TO DO: Does it make sense to further collapse AuthenticationFailedException and
+ * CertificateValidationException and any others into this?
+ */
+public class MessagingException extends Exception {
+  public static final long serialVersionUID = -1;
+
+  public static final int NO_ERROR = -1;
+  /** Any exception that does not specify a specific issue */
+  public static final int UNSPECIFIED_EXCEPTION = 0;
+  /** Connection or IO errors */
+  public static final int IOERROR = 1;
+  /** The configuration requested TLS but the server did not support it. */
+  public static final int TLS_REQUIRED = 2;
+  /** Authentication is required but the server did not support it. */
+  public static final int AUTH_REQUIRED = 3;
+  /** General security failures */
+  public static final int GENERAL_SECURITY = 4;
+  /** Authentication failed */
+  public static final int AUTHENTICATION_FAILED = 5;
+  /** Attempt to create duplicate account */
+  public static final int DUPLICATE_ACCOUNT = 6;
+  /** Required security policies reported - advisory only */
+  public static final int SECURITY_POLICIES_REQUIRED = 7;
+  /** Required security policies not supported */
+  public static final int SECURITY_POLICIES_UNSUPPORTED = 8;
+  /** The protocol (or protocol version) isn't supported */
+  public static final int PROTOCOL_VERSION_UNSUPPORTED = 9;
+  /** The server's SSL certificate couldn't be validated */
+  public static final int CERTIFICATE_VALIDATION_ERROR = 10;
+  /** Authentication failed during autodiscover */
+  public static final int AUTODISCOVER_AUTHENTICATION_FAILED = 11;
+  /** Autodiscover completed with a result (non-error) */
+  public static final int AUTODISCOVER_AUTHENTICATION_RESULT = 12;
+  /** Ambiguous failure; server error or bad credentials */
+  public static final int AUTHENTICATION_FAILED_OR_SERVER_ERROR = 13;
+  /** The server refused access */
+  public static final int ACCESS_DENIED = 14;
+  /** The server refused access */
+  public static final int ATTACHMENT_NOT_FOUND = 15;
+  /** A client SSL certificate is required for connections to the server */
+  public static final int CLIENT_CERTIFICATE_REQUIRED = 16;
+  /** The client SSL certificate specified is invalid */
+  public static final int CLIENT_CERTIFICATE_ERROR = 17;
+  /** The server indicates it does not support OAuth authentication */
+  public static final int OAUTH_NOT_SUPPORTED = 18;
+  /** The server indicates it experienced an internal error */
+  public static final int SERVER_ERROR = 19;
+
+  protected int mExceptionType;
+  // Exception type-specific data
+  protected Object mExceptionData;
+
+  public MessagingException(String message, Throwable throwable) {
+    this(UNSPECIFIED_EXCEPTION, message, throwable);
+  }
+
+  public MessagingException(int exceptionType, String message, Throwable throwable) {
+    super(message, throwable);
+    mExceptionType = exceptionType;
+    mExceptionData = null;
+  }
+
+  /**
+   * Constructs a MessagingException with an exceptionType and a null message.
+   *
+   * @param exceptionType The exception type to set for this exception.
+   */
+  public MessagingException(int exceptionType) {
+    this(exceptionType, null, null);
+  }
+
+  /**
+   * Constructs a MessagingException with a message.
+   *
+   * @param message the message for this exception
+   */
+  public MessagingException(String message) {
+    this(UNSPECIFIED_EXCEPTION, message, null);
+  }
+
+  /**
+   * Constructs a MessagingException with an exceptionType and a message.
+   *
+   * @param exceptionType The exception type to set for this exception.
+   */
+  public MessagingException(int exceptionType, String message) {
+    this(exceptionType, message, null);
+  }
+
+  /**
+   * Constructs a MessagingException with an exceptionType, a message, and data
+   *
+   * @param exceptionType The exception type to set for this exception.
+   * @param message the message for the exception (or null)
+   * @param data exception-type specific data for the exception (or null)
+   */
+  public MessagingException(int exceptionType, String message, Object data) {
+    super(message);
+    mExceptionType = exceptionType;
+    mExceptionData = data;
+  }
+
+  /**
+   * Return the exception type. Will be OTHER_EXCEPTION if not explicitly set.
+   *
+   * @return Returns the exception type.
+   */
+  public int getExceptionType() {
+    return mExceptionType;
+  }
+  /**
+   * Return the exception data. Will be null if not explicitly set.
+   *
+   * @return Returns the exception data.
+   */
+  public Object getExceptionData() {
+    return mExceptionData;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/Multipart.java b/java/com/android/voicemail/impl/mail/Multipart.java
new file mode 100644
index 0000000..e8d5046
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Multipart.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import java.util.ArrayList;
+
+public abstract class Multipart implements Body {
+  protected Part mParent;
+
+  protected ArrayList<BodyPart> mParts = new ArrayList<BodyPart>();
+
+  protected String mContentType;
+
+  public void addBodyPart(BodyPart part) throws MessagingException {
+    mParts.add(part);
+  }
+
+  public void addBodyPart(BodyPart part, int index) throws MessagingException {
+    mParts.add(index, part);
+  }
+
+  public BodyPart getBodyPart(int index) throws MessagingException {
+    return mParts.get(index);
+  }
+
+  public String getContentType() throws MessagingException {
+    return mContentType;
+  }
+
+  public int getCount() throws MessagingException {
+    return mParts.size();
+  }
+
+  public boolean removeBodyPart(BodyPart part) throws MessagingException {
+    return mParts.remove(part);
+  }
+
+  public void removeBodyPart(int index) throws MessagingException {
+    mParts.remove(index);
+  }
+
+  public Part getParent() throws MessagingException {
+    return mParent;
+  }
+
+  public void setParent(Part parent) throws MessagingException {
+    this.mParent = parent;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/PackedString.java b/java/com/android/voicemail/impl/mail/PackedString.java
new file mode 100644
index 0000000..701dab6
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/PackedString.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import android.util.ArrayMap;
+import java.util.Map;
+
+/**
+ * A utility class for creating and modifying Strings that are tagged and packed together.
+ *
+ * <p>Uses non-printable (control chars) for internal delimiters; Intended for regular displayable
+ * strings only, so please use base64 or other encoding if you need to hide any binary data here.
+ *
+ * <p>Binary compatible with Address.pack() format, which should migrate to use this code.
+ */
+public class PackedString {
+
+  /**
+   * Packing format is: element : [ value ] or [ value TAG-DELIMITER tag ] packed-string : [ element
+   * ] [ ELEMENT-DELIMITER [ element ] ]*
+   */
+  private static final char DELIMITER_ELEMENT = '\1';
+
+  private static final char DELIMITER_TAG = '\2';
+
+  private String mString;
+  private ArrayMap<String, String> mExploded;
+  private static final ArrayMap<String, String> EMPTY_MAP = new ArrayMap<String, String>();
+
+  /**
+   * Create a packed string using an already-packed string (e.g. from database)
+   *
+   * @param string packed string
+   */
+  public PackedString(String string) {
+    mString = string;
+    mExploded = null;
+  }
+
+  /**
+   * Get the value referred to by a given tag. If the tag does not exist, return null.
+   *
+   * @param tag identifier of string of interest
+   * @return returns value, or null if no string is found
+   */
+  public String get(String tag) {
+    if (mExploded == null) {
+      mExploded = explode(mString);
+    }
+    return mExploded.get(tag);
+  }
+
+  /**
+   * Return a map of all of the values referred to by a given tag. This is a shallow copy, don't
+   * edit the values.
+   *
+   * @return a map of the values in the packed string
+   */
+  public Map<String, String> unpack() {
+    if (mExploded == null) {
+      mExploded = explode(mString);
+    }
+    return new ArrayMap<String, String>(mExploded);
+  }
+
+  /** Read out all values into a map. */
+  private static ArrayMap<String, String> explode(String packed) {
+    if (packed == null || packed.length() == 0) {
+      return EMPTY_MAP;
+    }
+    ArrayMap<String, String> map = new ArrayMap<String, String>();
+
+    int length = packed.length();
+    int elementStartIndex = 0;
+    int elementEndIndex = 0;
+    int tagEndIndex = packed.indexOf(DELIMITER_TAG);
+
+    while (elementStartIndex < length) {
+      elementEndIndex = packed.indexOf(DELIMITER_ELEMENT, elementStartIndex);
+      if (elementEndIndex == -1) {
+        elementEndIndex = length;
+      }
+      String tag;
+      String value;
+      if (tagEndIndex == -1 || elementEndIndex <= tagEndIndex) {
+        // in this case the DELIMITER_PERSONAL is in a future pair (or not found)
+        // so synthesize a positional tag for the value, and don't update tagEndIndex
+        value = packed.substring(elementStartIndex, elementEndIndex);
+        tag = Integer.toString(map.size());
+      } else {
+        value = packed.substring(elementStartIndex, tagEndIndex);
+        tag = packed.substring(tagEndIndex + 1, elementEndIndex);
+        // scan forward for next tag, if any
+        tagEndIndex = packed.indexOf(DELIMITER_TAG, elementEndIndex + 1);
+      }
+      map.put(tag, value);
+      elementStartIndex = elementEndIndex + 1;
+    }
+
+    return map;
+  }
+
+  /**
+   * Builder class for creating PackedString values. Can also be used for editing existing
+   * PackedString representations.
+   */
+  public static class Builder {
+    ArrayMap<String, String> mMap;
+
+    /** Create a builder that's empty (for filling) */
+    public Builder() {
+      mMap = new ArrayMap<String, String>();
+    }
+
+    /** Create a builder using the values of an existing PackedString (for editing). */
+    public Builder(String packed) {
+      mMap = explode(packed);
+    }
+
+    /**
+     * Add a tagged value
+     *
+     * @param tag identifier of string of interest
+     * @param value the value to record in this position. null to delete entry.
+     */
+    public void put(String tag, String value) {
+      if (value == null) {
+        mMap.remove(tag);
+      } else {
+        mMap.put(tag, value);
+      }
+    }
+
+    /**
+     * Get the value referred to by a given tag. If the tag does not exist, return null.
+     *
+     * @param tag identifier of string of interest
+     * @return returns value, or null if no string is found
+     */
+    public String get(String tag) {
+      return mMap.get(tag);
+    }
+
+    /** Pack the values and return a single, encoded string */
+    @Override
+    public String toString() {
+      StringBuilder sb = new StringBuilder();
+      for (Map.Entry<String, String> entry : mMap.entrySet()) {
+        if (sb.length() > 0) {
+          sb.append(DELIMITER_ELEMENT);
+        }
+        sb.append(entry.getValue());
+        sb.append(DELIMITER_TAG);
+        sb.append(entry.getKey());
+      }
+      return sb.toString();
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/Part.java b/java/com/android/voicemail/impl/mail/Part.java
new file mode 100644
index 0000000..3be5c57
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Part.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public interface Part extends Fetchable {
+  public void addHeader(String name, String value) throws MessagingException;
+
+  public void removeHeader(String name) throws MessagingException;
+
+  public void setHeader(String name, String value) throws MessagingException;
+
+  public Body getBody() throws MessagingException;
+
+  public String getContentType() throws MessagingException;
+
+  public String getDisposition() throws MessagingException;
+
+  public String getContentId() throws MessagingException;
+
+  public String[] getHeader(String name) throws MessagingException;
+
+  public void setExtendedHeader(String name, String value) throws MessagingException;
+
+  public String getExtendedHeader(String name) throws MessagingException;
+
+  public int getSize() throws MessagingException;
+
+  public boolean isMimeType(String mimeType) throws MessagingException;
+
+  public String getMimeType() throws MessagingException;
+
+  public void setBody(Body body) throws MessagingException;
+
+  public void writeTo(OutputStream out) throws IOException, MessagingException;
+}
diff --git a/java/com/android/voicemail/impl/mail/PeekableInputStream.java b/java/com/android/voicemail/impl/mail/PeekableInputStream.java
new file mode 100644
index 0000000..08f867f
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/PeekableInputStream.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A filtering InputStream that allows single byte "peeks" without consuming the byte. The client of
+ * this stream can call peek() to see the next available byte in the stream and a subsequent read
+ * will still return the peeked byte.
+ */
+public class PeekableInputStream extends InputStream {
+  private final InputStream mIn;
+  private boolean mPeeked;
+  private int mPeekedByte;
+
+  public PeekableInputStream(InputStream in) {
+    this.mIn = in;
+  }
+
+  @Override
+  public int read() throws IOException {
+    if (!mPeeked) {
+      return mIn.read();
+    } else {
+      mPeeked = false;
+      return mPeekedByte;
+    }
+  }
+
+  public int peek() throws IOException {
+    if (!mPeeked) {
+      mPeekedByte = read();
+      mPeeked = true;
+    }
+    return mPeekedByte;
+  }
+
+  @Override
+  public int read(byte[] b, int offset, int length) throws IOException {
+    if (!mPeeked) {
+      return mIn.read(b, offset, length);
+    } else {
+      b[0] = (byte) mPeekedByte;
+      mPeeked = false;
+      int r = mIn.read(b, offset + 1, length - 1);
+      if (r == -1) {
+        return 1;
+      } else {
+        return r + 1;
+      }
+    }
+  }
+
+  @Override
+  public int read(byte[] b) throws IOException {
+    return read(b, 0, b.length);
+  }
+
+  @Override
+  public String toString() {
+    return String.format(
+        "PeekableInputStream(in=%s, peeked=%b, peekedByte=%d)",
+        mIn.toString(), mPeeked, mPeekedByte);
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/TempDirectory.java b/java/com/android/voicemail/impl/mail/TempDirectory.java
new file mode 100644
index 0000000..42adbeb
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/TempDirectory.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail;
+
+import android.content.Context;
+import java.io.File;
+
+/**
+ * TempDirectory caches the directory used for caching file. It is set up during application
+ * initialization.
+ */
+public class TempDirectory {
+  private static File sTempDirectory = null;
+
+  public static void setTempDirectory(Context context) {
+    sTempDirectory = context.getCacheDir();
+  }
+
+  public static File getTempDirectory() {
+    if (sTempDirectory == null) {
+      throw new RuntimeException(
+          "TempDirectory not set.  "
+              + "If in a unit test, call Email.setTempDirectory(context) in setUp().");
+    }
+    return sTempDirectory;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/BinaryTempFileBody.java b/java/com/android/voicemail/impl/mail/internet/BinaryTempFileBody.java
new file mode 100644
index 0000000..753b70f
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/BinaryTempFileBody.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.internet;
+
+import android.util.Base64;
+import android.util.Base64OutputStream;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.TempDirectory;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.commons.io.IOUtils;
+
+/**
+ * A Body that is backed by a temp file. The Body exposes a getOutputStream method that allows the
+ * user to write to the temp file. After the write the body is available via getInputStream and
+ * writeTo one time. After writeTo is called, or the InputStream returned from getInputStream is
+ * closed the file is deleted and the Body should be considered disposed of.
+ */
+public class BinaryTempFileBody implements Body {
+  private File mFile;
+
+  /**
+   * An alternate way to put data into a BinaryTempFileBody is to simply supply an already- created
+   * file. Note that this file will be deleted after it is read.
+   *
+   * @param filePath The file containing the data to be stored on disk temporarily
+   */
+  public void setFile(String filePath) {
+    mFile = new File(filePath);
+  }
+
+  public OutputStream getOutputStream() throws IOException {
+    mFile = File.createTempFile("body", null, TempDirectory.getTempDirectory());
+    mFile.deleteOnExit();
+    return new FileOutputStream(mFile);
+  }
+
+  @Override
+  public InputStream getInputStream() throws MessagingException {
+    try {
+      return new BinaryTempFileBodyInputStream(new FileInputStream(mFile));
+    } catch (IOException ioe) {
+      throw new MessagingException("Unable to open body", ioe);
+    }
+  }
+
+  @Override
+  public void writeTo(OutputStream out) throws IOException, MessagingException {
+    InputStream in = getInputStream();
+    Base64OutputStream base64Out = new Base64OutputStream(out, Base64.CRLF | Base64.NO_CLOSE);
+    IOUtils.copy(in, base64Out);
+    base64Out.close();
+    mFile.delete();
+    in.close();
+  }
+
+  class BinaryTempFileBodyInputStream extends FilterInputStream {
+    public BinaryTempFileBodyInputStream(InputStream in) {
+      super(in);
+    }
+
+    @Override
+    public void close() throws IOException {
+      super.close();
+      mFile.delete();
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/MimeBodyPart.java b/java/com/android/voicemail/impl/mail/internet/MimeBodyPart.java
new file mode 100644
index 0000000..2add76c
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/MimeBodyPart.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.internet;
+
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.BodyPart;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Multipart;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.regex.Pattern;
+
+/** TODO this is a close approximation of Message, need to update along with Message. */
+public class MimeBodyPart extends BodyPart {
+  protected MimeHeader mHeader = new MimeHeader();
+  protected MimeHeader mExtendedHeader;
+  protected Body mBody;
+  protected int mSize;
+
+  // regex that matches content id surrounded by "<>" optionally.
+  private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
+  // regex that matches end of line.
+  private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
+
+  public MimeBodyPart() throws MessagingException {
+    this(null);
+  }
+
+  public MimeBodyPart(Body body) throws MessagingException {
+    this(body, null);
+  }
+
+  public MimeBodyPart(Body body, String mimeType) throws MessagingException {
+    if (mimeType != null) {
+      setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType);
+    }
+    setBody(body);
+  }
+
+  protected String getFirstHeader(String name) throws MessagingException {
+    return mHeader.getFirstHeader(name);
+  }
+
+  @Override
+  public void addHeader(String name, String value) throws MessagingException {
+    mHeader.addHeader(name, value);
+  }
+
+  @Override
+  public void setHeader(String name, String value) throws MessagingException {
+    mHeader.setHeader(name, value);
+  }
+
+  @Override
+  public String[] getHeader(String name) throws MessagingException {
+    return mHeader.getHeader(name);
+  }
+
+  @Override
+  public void removeHeader(String name) throws MessagingException {
+    mHeader.removeHeader(name);
+  }
+
+  @Override
+  public Body getBody() throws MessagingException {
+    return mBody;
+  }
+
+  @Override
+  public void setBody(Body body) throws MessagingException {
+    this.mBody = body;
+    if (body instanceof Multipart) {
+      Multipart multipart =
+          ((Multipart) body);
+      multipart.setParent(this);
+      setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
+    } else if (body instanceof TextBody) {
+      String contentType = String.format("%s;\n charset=utf-8", getMimeType());
+      String name = MimeUtility.getHeaderParameter(getContentType(), "name");
+      if (name != null) {
+        contentType += String.format(";\n name=\"%s\"", name);
+      }
+      setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
+      setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+    }
+  }
+
+  @Override
+  public String getContentType() throws MessagingException {
+    String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+    if (contentType == null) {
+      return "text/plain";
+    } else {
+      return contentType;
+    }
+  }
+
+  @Override
+  public String getDisposition() throws MessagingException {
+    String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+    if (contentDisposition == null) {
+      return null;
+    } else {
+      return contentDisposition;
+    }
+  }
+
+  @Override
+  public String getContentId() throws MessagingException {
+    String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+    if (contentId == null) {
+      return null;
+    } else {
+      // remove optionally surrounding brackets.
+      return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+    }
+  }
+
+  @Override
+  public String getMimeType() throws MessagingException {
+    return MimeUtility.getHeaderParameter(getContentType(), null);
+  }
+
+  @Override
+  public boolean isMimeType(String mimeType) throws MessagingException {
+    return getMimeType().equals(mimeType);
+  }
+
+  public void setSize(int size) {
+    this.mSize = size;
+  }
+
+  @Override
+  public int getSize() throws MessagingException {
+    return mSize;
+  }
+
+  /**
+   * Set extended header
+   *
+   * @param name Extended header name
+   * @param value header value - flattened by removing CR-NL if any remove header if value is null
+   * @throws MessagingException
+   */
+  @Override
+  public void setExtendedHeader(String name, String value) throws MessagingException {
+    if (value == null) {
+      if (mExtendedHeader != null) {
+        mExtendedHeader.removeHeader(name);
+      }
+      return;
+    }
+    if (mExtendedHeader == null) {
+      mExtendedHeader = new MimeHeader();
+    }
+    mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
+  }
+
+  /**
+   * Get extended header
+   *
+   * @param name Extended header name
+   * @return header value - null if header does not exist
+   * @throws MessagingException
+   */
+  @Override
+  public String getExtendedHeader(String name) throws MessagingException {
+    if (mExtendedHeader == null) {
+      return null;
+    }
+    return mExtendedHeader.getFirstHeader(name);
+  }
+
+  /** Write the MimeMessage out in MIME format. */
+  @Override
+  public void writeTo(OutputStream out) throws IOException, MessagingException {
+    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+    mHeader.writeTo(out);
+    writer.write("\r\n");
+    writer.flush();
+    if (mBody != null) {
+      mBody.writeTo(out);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/MimeHeader.java b/java/com/android/voicemail/impl/mail/internet/MimeHeader.java
new file mode 100644
index 0000000..d41cdb3
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/MimeHeader.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.internet;
+
+import com.android.voicemail.impl.mail.MessagingException;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+
+public class MimeHeader {
+  /**
+   * Application specific header that contains Store specific information about an attachment. In
+   * IMAP this contains the IMAP BODYSTRUCTURE part id so that the ImapStore can later retrieve the
+   * attachment at will from the server. The info is recorded from this header on
+   * LocalStore.appendMessage and is put back into the MIME data by LocalStore.fetch.
+   */
+  public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA =
+      "X-Android-Attachment-StoreData";
+
+  public static final String HEADER_CONTENT_TYPE = "Content-Type";
+  public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
+  public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
+  public static final String HEADER_CONTENT_ID = "Content-ID";
+
+  /** Fields that should be omitted when writing the header using writeTo() */
+  private static final String[] WRITE_OMIT_FIELDS = {
+    //        HEADER_ANDROID_ATTACHMENT_DOWNLOADED,
+    //        HEADER_ANDROID_ATTACHMENT_ID,
+    HEADER_ANDROID_ATTACHMENT_STORE_DATA
+  };
+
+  protected final ArrayList<Field> mFields = new ArrayList<Field>();
+
+  public void clear() {
+    mFields.clear();
+  }
+
+  public String getFirstHeader(String name) throws MessagingException {
+    String[] header = getHeader(name);
+    if (header == null) {
+      return null;
+    }
+    return header[0];
+  }
+
+  public void addHeader(String name, String value) throws MessagingException {
+    mFields.add(new Field(name, value));
+  }
+
+  public void setHeader(String name, String value) throws MessagingException {
+    if (name == null || value == null) {
+      return;
+    }
+    removeHeader(name);
+    addHeader(name, value);
+  }
+
+  public String[] getHeader(String name) throws MessagingException {
+    ArrayList<String> values = new ArrayList<String>();
+    for (Field field : mFields) {
+      if (field.name.equalsIgnoreCase(name)) {
+        values.add(field.value);
+      }
+    }
+    if (values.size() == 0) {
+      return null;
+    }
+    return values.toArray(new String[] {});
+  }
+
+  public void removeHeader(String name) throws MessagingException {
+    ArrayList<Field> removeFields = new ArrayList<Field>();
+    for (Field field : mFields) {
+      if (field.name.equalsIgnoreCase(name)) {
+        removeFields.add(field);
+      }
+    }
+    mFields.removeAll(removeFields);
+  }
+
+  /**
+   * Write header into String
+   *
+   * @return CR-NL separated header string except the headers in writeOmitFields null if header is
+   *     empty
+   */
+  public String writeToString() {
+    if (mFields.size() == 0) {
+      return null;
+    }
+    StringBuilder builder = new StringBuilder();
+    for (Field field : mFields) {
+      if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) {
+        builder.append(field.name + ": " + field.value + "\r\n");
+      }
+    }
+    return builder.toString();
+  }
+
+  public void writeTo(OutputStream out) throws IOException, MessagingException {
+    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+    for (Field field : mFields) {
+      if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) {
+        writer.write(field.name + ": " + field.value + "\r\n");
+      }
+    }
+    writer.flush();
+  }
+
+  private static class Field {
+    final String name;
+    final String value;
+
+    public Field(String name, String value) {
+      this.name = name;
+      this.value = value;
+    }
+
+    @Override
+    public String toString() {
+      return name + "=" + value;
+    }
+  }
+
+  @Override
+  public String toString() {
+    return (mFields == null) ? null : mFields.toString();
+  }
+
+  public static final boolean arrayContains(Object[] a, Object o) {
+    int index = arrayIndex(a, o);
+    return (index >= 0);
+  }
+
+  public static final int arrayIndex(Object[] a, Object o) {
+    for (int i = 0, count = a.length; i < count; i++) {
+      if (a[i].equals(o)) {
+        return i;
+      }
+    }
+    return -1;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/MimeMessage.java b/java/com/android/voicemail/impl/mail/internet/MimeMessage.java
new file mode 100644
index 0000000..dfb7d7c
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/MimeMessage.java
@@ -0,0 +1,676 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.internet;
+
+import android.text.TextUtils;
+import com.android.voicemail.impl.mail.Address;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.BodyPart;
+import com.android.voicemail.impl.mail.Message;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Multipart;
+import com.android.voicemail.impl.mail.Part;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Stack;
+import java.util.regex.Pattern;
+import org.apache.james.mime4j.BodyDescriptor;
+import org.apache.james.mime4j.ContentHandler;
+import org.apache.james.mime4j.EOLConvertingInputStream;
+import org.apache.james.mime4j.MimeStreamParser;
+import org.apache.james.mime4j.field.DateTimeField;
+import org.apache.james.mime4j.field.Field;
+
+/**
+ * An implementation of Message that stores all of its metadata in RFC 822 and RFC 2045 style
+ * headers.
+ *
+ * <p>NOTE: Automatic generation of a local message-id is becoming unwieldy and should be removed.
+ * It would be better to simply do it explicitly on local creation of new outgoing messages.
+ */
+public class MimeMessage extends Message {
+  private MimeHeader mHeader;
+  private MimeHeader mExtendedHeader;
+
+  // NOTE:  The fields here are transcribed out of headers, and values stored here will supersede
+  // the values found in the headers.  Use caution to prevent any out-of-phase errors.  In
+  // particular, any adds/changes/deletes here must be echoed by changes in the parse() function.
+  private Address[] mFrom;
+  private Address[] mTo;
+  private Address[] mCc;
+  private Address[] mBcc;
+  private Address[] mReplyTo;
+  private Date mSentDate;
+  private Body mBody;
+  protected int mSize;
+  private boolean mInhibitLocalMessageId = false;
+  private boolean mComplete = true;
+
+  // Shared random source for generating local message-id values
+  private static final java.util.Random sRandom = new java.util.Random();
+
+  // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
+  // "Jan", not the other localized format like "Ene" (meaning January in locale es).
+  // This conversion is used when generating outgoing MIME messages. Incoming MIME date
+  // headers are parsed by org.apache.james.mime4j.field.DateTimeField which does not have any
+  // localization code.
+  private static final SimpleDateFormat DATE_FORMAT =
+      new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
+
+  // regex that matches content id surrounded by "<>" optionally.
+  private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
+  // regex that matches end of line.
+  private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
+
+  public MimeMessage() {
+    mHeader = null;
+  }
+
+  /**
+   * Generate a local message id. This is only used when none has been assigned, and is installed
+   * lazily. Any remote (typically server-assigned) message id takes precedence.
+   *
+   * @return a long, locally-generated message-ID value
+   */
+  private static String generateMessageId() {
+    final StringBuilder sb = new StringBuilder();
+    sb.append("<");
+    for (int i = 0; i < 24; i++) {
+      // We'll use a 5-bit range (0..31)
+      final int value = sRandom.nextInt() & 31;
+      final char c = "0123456789abcdefghijklmnopqrstuv".charAt(value);
+      sb.append(c);
+    }
+    sb.append(".");
+    sb.append(Long.toString(System.currentTimeMillis()));
+    sb.append("@email.android.com>");
+    return sb.toString();
+  }
+
+  /**
+   * Parse the given InputStream using Apache Mime4J to build a MimeMessage.
+   *
+   * @param in InputStream providing message content
+   * @throws IOException
+   * @throws MessagingException
+   */
+  public MimeMessage(InputStream in) throws IOException, MessagingException {
+    parse(in);
+  }
+
+  private MimeStreamParser init() {
+    // Before parsing the input stream, clear all local fields that may be superceded by
+    // the new incoming message.
+    getMimeHeaders().clear();
+    mInhibitLocalMessageId = true;
+    mFrom = null;
+    mTo = null;
+    mCc = null;
+    mBcc = null;
+    mReplyTo = null;
+    mSentDate = null;
+    mBody = null;
+
+    final MimeStreamParser parser = new MimeStreamParser();
+    parser.setContentHandler(new MimeMessageBuilder());
+    return parser;
+  }
+
+  protected void parse(InputStream in) throws IOException, MessagingException {
+    final MimeStreamParser parser = init();
+    parser.parse(new EOLConvertingInputStream(in));
+    mComplete = !parser.getPrematureEof();
+  }
+
+  public void parse(InputStream in, EOLConvertingInputStream.Callback callback)
+      throws IOException, MessagingException {
+    final MimeStreamParser parser = init();
+    parser.parse(new EOLConvertingInputStream(in, getSize(), callback));
+    mComplete = !parser.getPrematureEof();
+  }
+
+  /**
+   * Return the internal mHeader value, with very lazy initialization. The goal is to save memory by
+   * not creating the headers until needed.
+   */
+  private MimeHeader getMimeHeaders() {
+    if (mHeader == null) {
+      mHeader = new MimeHeader();
+    }
+    return mHeader;
+  }
+
+  @Override
+  public Date getReceivedDate() throws MessagingException {
+    return null;
+  }
+
+  @Override
+  public Date getSentDate() throws MessagingException {
+    if (mSentDate == null) {
+      try {
+        DateTimeField field =
+            (DateTimeField)
+                Field.parse("Date: " + MimeUtility.unfoldAndDecode(getFirstHeader("Date")));
+        mSentDate = field.getDate();
+        // TODO: We should make it more clear what exceptions can be thrown here,
+        // and whether they reflect a normal or error condition.
+      } catch (Exception e) {
+        LogUtils.v(LogUtils.TAG, "Message missing Date header");
+      }
+    }
+    if (mSentDate == null) {
+      // If we still don't have a date, fall back to "Delivery-date"
+      try {
+        DateTimeField field =
+            (DateTimeField)
+                Field.parse(
+                    "Date: " + MimeUtility.unfoldAndDecode(getFirstHeader("Delivery-date")));
+        mSentDate = field.getDate();
+        // TODO: We should make it more clear what exceptions can be thrown here,
+        // and whether they reflect a normal or error condition.
+      } catch (Exception e) {
+        LogUtils.v(LogUtils.TAG, "Message also missing Delivery-Date header");
+      }
+    }
+    return mSentDate;
+  }
+
+  @Override
+  public void setSentDate(Date sentDate) throws MessagingException {
+    setHeader("Date", DATE_FORMAT.format(sentDate));
+    this.mSentDate = sentDate;
+  }
+
+  @Override
+  public String getContentType() throws MessagingException {
+    final String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+    if (contentType == null) {
+      return "text/plain";
+    } else {
+      return contentType;
+    }
+  }
+
+  @Override
+  public String getDisposition() throws MessagingException {
+    return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+  }
+
+  @Override
+  public String getContentId() throws MessagingException {
+    final String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+    if (contentId == null) {
+      return null;
+    } else {
+      // remove optionally surrounding brackets.
+      return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+    }
+  }
+
+  public boolean isComplete() {
+    return mComplete;
+  }
+
+  @Override
+  public String getMimeType() throws MessagingException {
+    return MimeUtility.getHeaderParameter(getContentType(), null);
+  }
+
+  @Override
+  public int getSize() throws MessagingException {
+    return mSize;
+  }
+
+  /**
+   * Returns a list of the given recipient type from this message. If no addresses are found the
+   * method returns an empty array.
+   */
+  @Override
+  public Address[] getRecipients(String type) throws MessagingException {
+    if (type == RECIPIENT_TYPE_TO) {
+      if (mTo == null) {
+        mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
+      }
+      return mTo;
+    } else if (type == RECIPIENT_TYPE_CC) {
+      if (mCc == null) {
+        mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
+      }
+      return mCc;
+    } else if (type == RECIPIENT_TYPE_BCC) {
+      if (mBcc == null) {
+        mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
+      }
+      return mBcc;
+    } else {
+      throw new MessagingException("Unrecognized recipient type.");
+    }
+  }
+
+  @Override
+  public void setRecipients(String type, Address[] addresses) throws MessagingException {
+    final int toLength = 4; // "To: "
+    final int ccLength = 4; // "Cc: "
+    final int bccLength = 5; // "Bcc: "
+    if (type == RECIPIENT_TYPE_TO) {
+      if (addresses == null || addresses.length == 0) {
+        removeHeader("To");
+        this.mTo = null;
+      } else {
+        setHeader("To", MimeUtility.fold(Address.toHeader(addresses), toLength));
+        this.mTo = addresses;
+      }
+    } else if (type == RECIPIENT_TYPE_CC) {
+      if (addresses == null || addresses.length == 0) {
+        removeHeader("CC");
+        this.mCc = null;
+      } else {
+        setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), ccLength));
+        this.mCc = addresses;
+      }
+    } else if (type == RECIPIENT_TYPE_BCC) {
+      if (addresses == null || addresses.length == 0) {
+        removeHeader("BCC");
+        this.mBcc = null;
+      } else {
+        setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), bccLength));
+        this.mBcc = addresses;
+      }
+    } else {
+      throw new MessagingException("Unrecognized recipient type.");
+    }
+  }
+
+  /** Returns the unfolded, decoded value of the Subject header. */
+  @Override
+  public String getSubject() throws MessagingException {
+    return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
+  }
+
+  @Override
+  public void setSubject(String subject) throws MessagingException {
+    final int headerNameLength = 9; // "Subject: "
+    setHeader("Subject", MimeUtility.foldAndEncode2(subject, headerNameLength));
+  }
+
+  @Override
+  public Address[] getFrom() throws MessagingException {
+    if (mFrom == null) {
+      String list = MimeUtility.unfold(getFirstHeader("From"));
+      if (list == null || list.length() == 0) {
+        list = MimeUtility.unfold(getFirstHeader("Sender"));
+      }
+      mFrom = Address.parse(list);
+    }
+    return mFrom;
+  }
+
+  @Override
+  public void setFrom(Address from) throws MessagingException {
+    final int fromLength = 6; // "From: "
+    if (from != null) {
+      setHeader("From", MimeUtility.fold(from.toHeader(), fromLength));
+      this.mFrom = new Address[] {from};
+    } else {
+      this.mFrom = null;
+    }
+  }
+
+  @Override
+  public Address[] getReplyTo() throws MessagingException {
+    if (mReplyTo == null) {
+      mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
+    }
+    return mReplyTo;
+  }
+
+  @Override
+  public void setReplyTo(Address[] replyTo) throws MessagingException {
+    final int replyToLength = 10; // "Reply-to: "
+    if (replyTo == null || replyTo.length == 0) {
+      removeHeader("Reply-to");
+      mReplyTo = null;
+    } else {
+      setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), replyToLength));
+      mReplyTo = replyTo;
+    }
+  }
+
+  /**
+   * Set the mime "Message-ID" header
+   *
+   * @param messageId the new Message-ID value
+   * @throws MessagingException
+   */
+  @Override
+  public void setMessageId(String messageId) throws MessagingException {
+    setHeader("Message-ID", messageId);
+  }
+
+  /**
+   * Get the mime "Message-ID" header. This value will be preloaded with a locally-generated random
+   * ID, if the value has not previously been set. Local generation can be inhibited/ overridden by
+   * explicitly clearing the headers, removing the message-id header, etc.
+   *
+   * @return the Message-ID header string, or null if explicitly has been set to null
+   */
+  @Override
+  public String getMessageId() throws MessagingException {
+    String messageId = getFirstHeader("Message-ID");
+    if (messageId == null && !mInhibitLocalMessageId) {
+      messageId = generateMessageId();
+      setMessageId(messageId);
+    }
+    return messageId;
+  }
+
+  @Override
+  public void saveChanges() throws MessagingException {
+    throw new MessagingException("saveChanges not yet implemented");
+  }
+
+  @Override
+  public Body getBody() throws MessagingException {
+    return mBody;
+  }
+
+  @Override
+  public void setBody(Body body) throws MessagingException {
+    this.mBody = body;
+    if (body instanceof Multipart) {
+      final Multipart multipart = ((Multipart) body);
+      multipart.setParent(this);
+      setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
+      setHeader("MIME-Version", "1.0");
+    } else if (body instanceof TextBody) {
+      setHeader(
+          MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8", getMimeType()));
+      setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+    }
+  }
+
+  protected String getFirstHeader(String name) throws MessagingException {
+    return getMimeHeaders().getFirstHeader(name);
+  }
+
+  @Override
+  public void addHeader(String name, String value) throws MessagingException {
+    getMimeHeaders().addHeader(name, value);
+  }
+
+  @Override
+  public void setHeader(String name, String value) throws MessagingException {
+    getMimeHeaders().setHeader(name, value);
+  }
+
+  @Override
+  public String[] getHeader(String name) throws MessagingException {
+    return getMimeHeaders().getHeader(name);
+  }
+
+  @Override
+  public void removeHeader(String name) throws MessagingException {
+    getMimeHeaders().removeHeader(name);
+    if ("Message-ID".equalsIgnoreCase(name)) {
+      mInhibitLocalMessageId = true;
+    }
+  }
+
+  /**
+   * Set extended header
+   *
+   * @param name Extended header name
+   * @param value header value - flattened by removing CR-NL if any remove header if value is null
+   * @throws MessagingException
+   */
+  @Override
+  public void setExtendedHeader(String name, String value) throws MessagingException {
+    if (value == null) {
+      if (mExtendedHeader != null) {
+        mExtendedHeader.removeHeader(name);
+      }
+      return;
+    }
+    if (mExtendedHeader == null) {
+      mExtendedHeader = new MimeHeader();
+    }
+    mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
+  }
+
+  /**
+   * Get extended header
+   *
+   * @param name Extended header name
+   * @return header value - null if header does not exist
+   * @throws MessagingException
+   */
+  @Override
+  public String getExtendedHeader(String name) throws MessagingException {
+    if (mExtendedHeader == null) {
+      return null;
+    }
+    return mExtendedHeader.getFirstHeader(name);
+  }
+
+  /**
+   * Set entire extended headers from String
+   *
+   * @param headers Extended header and its value - "CR-NL-separated pairs if null or empty, remove
+   *     entire extended headers
+   * @throws MessagingException
+   */
+  public void setExtendedHeaders(String headers) throws MessagingException {
+    if (TextUtils.isEmpty(headers)) {
+      mExtendedHeader = null;
+    } else {
+      mExtendedHeader = new MimeHeader();
+      for (final String header : END_OF_LINE.split(headers)) {
+        final String[] tokens = header.split(":", 2);
+        if (tokens.length != 2) {
+          throw new MessagingException("Illegal extended headers: " + headers);
+        }
+        mExtendedHeader.setHeader(tokens[0].trim(), tokens[1].trim());
+      }
+    }
+  }
+
+  /**
+   * Get entire extended headers as String
+   *
+   * @return "CR-NL-separated extended headers - null if extended header does not exist
+   */
+  public String getExtendedHeaders() {
+    if (mExtendedHeader != null) {
+      return mExtendedHeader.writeToString();
+    }
+    return null;
+  }
+
+  /**
+   * Write message header and body to output stream
+   *
+   * @param out Output steam to write message header and body.
+   */
+  @Override
+  public void writeTo(OutputStream out) throws IOException, MessagingException {
+    final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+    // Force creation of local message-id
+    getMessageId();
+    getMimeHeaders().writeTo(out);
+    // mExtendedHeader will not be write out to external output stream,
+    // because it is intended to internal use.
+    writer.write("\r\n");
+    writer.flush();
+    if (mBody != null) {
+      mBody.writeTo(out);
+    }
+  }
+
+  @Override
+  public InputStream getInputStream() throws MessagingException {
+    return null;
+  }
+
+  class MimeMessageBuilder implements ContentHandler {
+    private final Stack<Object> stack = new Stack<Object>();
+
+    public MimeMessageBuilder() {}
+
+    private void expect(Class<?> c) {
+      if (!c.isInstance(stack.peek())) {
+        throw new IllegalStateException(
+            "Internal stack error: "
+                + "Expected '"
+                + c.getName()
+                + "' found '"
+                + stack.peek().getClass().getName()
+                + "'");
+      }
+    }
+
+    @Override
+    public void startMessage() {
+      if (stack.isEmpty()) {
+        stack.push(MimeMessage.this);
+      } else {
+        expect(Part.class);
+        try {
+          final MimeMessage m = new MimeMessage();
+          ((Part) stack.peek()).setBody(m);
+          stack.push(m);
+        } catch (MessagingException me) {
+          throw new Error(me);
+        }
+      }
+    }
+
+    @Override
+    public void endMessage() {
+      expect(MimeMessage.class);
+      stack.pop();
+    }
+
+    @Override
+    public void startHeader() {
+      expect(Part.class);
+    }
+
+    @Override
+    public void field(String fieldData) {
+      expect(Part.class);
+      try {
+        final String[] tokens = fieldData.split(":", 2);
+        ((Part) stack.peek()).addHeader(tokens[0], tokens[1].trim());
+      } catch (MessagingException me) {
+        throw new Error(me);
+      }
+    }
+
+    @Override
+    public void endHeader() {
+      expect(Part.class);
+    }
+
+    @Override
+    public void startMultipart(BodyDescriptor bd) {
+      expect(Part.class);
+
+      final Part e = (Part) stack.peek();
+      try {
+        final MimeMultipart multiPart = new MimeMultipart(e.getContentType());
+        e.setBody(multiPart);
+        stack.push(multiPart);
+      } catch (MessagingException me) {
+        throw new Error(me);
+      }
+    }
+
+    @Override
+    public void body(BodyDescriptor bd, InputStream in) throws IOException {
+      expect(Part.class);
+      final Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
+      try {
+        ((Part) stack.peek()).setBody(body);
+      } catch (MessagingException me) {
+        throw new Error(me);
+      }
+    }
+
+    @Override
+    public void endMultipart() {
+      stack.pop();
+    }
+
+    @Override
+    public void startBodyPart() {
+      expect(MimeMultipart.class);
+
+      try {
+        final MimeBodyPart bodyPart = new MimeBodyPart();
+        ((MimeMultipart) stack.peek()).addBodyPart(bodyPart);
+        stack.push(bodyPart);
+      } catch (MessagingException me) {
+        throw new Error(me);
+      }
+    }
+
+    @Override
+    public void endBodyPart() {
+      expect(BodyPart.class);
+      stack.pop();
+    }
+
+    @Override
+    public void epilogue(InputStream is) throws IOException {
+      expect(MimeMultipart.class);
+      final StringBuilder sb = new StringBuilder();
+      int b;
+      while ((b = is.read()) != -1) {
+        sb.append((char) b);
+      }
+      // TODO: why is this commented out?
+      // ((Multipart) stack.peek()).setEpilogue(sb.toString());
+    }
+
+    @Override
+    public void preamble(InputStream is) throws IOException {
+      expect(MimeMultipart.class);
+      final StringBuilder sb = new StringBuilder();
+      int b;
+      while ((b = is.read()) != -1) {
+        sb.append((char) b);
+      }
+      try {
+        ((MimeMultipart) stack.peek()).setPreamble(sb.toString());
+      } catch (MessagingException me) {
+        throw new Error(me);
+      }
+    }
+
+    @Override
+    public void raw(InputStream is) throws IOException {
+      throw new UnsupportedOperationException("Not supported");
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/MimeMultipart.java b/java/com/android/voicemail/impl/mail/internet/MimeMultipart.java
new file mode 100644
index 0000000..87b88b5
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/MimeMultipart.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.internet;
+
+import com.android.voicemail.impl.mail.BodyPart;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Multipart;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+
+public class MimeMultipart extends Multipart {
+  protected String mPreamble;
+
+  protected String mContentType;
+
+  protected String mBoundary;
+
+  protected String mSubType;
+
+  public MimeMultipart() throws MessagingException {
+    mBoundary = generateBoundary();
+    setSubType("mixed");
+  }
+
+  public MimeMultipart(String contentType) throws MessagingException {
+    this.mContentType = contentType;
+    try {
+      mSubType = MimeUtility.getHeaderParameter(contentType, null).split("/")[1];
+      mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary");
+      if (mBoundary == null) {
+        throw new MessagingException("MultiPart does not contain boundary: " + contentType);
+      }
+    } catch (Exception e) {
+      throw new MessagingException(
+          "Invalid MultiPart Content-Type; must contain subtype and boundary. ("
+              + contentType
+              + ")",
+          e);
+    }
+  }
+
+  public String generateBoundary() {
+    StringBuffer sb = new StringBuffer();
+    sb.append("----");
+    for (int i = 0; i < 30; i++) {
+      sb.append(Integer.toString((int) (Math.random() * 35), 36));
+    }
+    return sb.toString().toUpperCase();
+  }
+
+  public String getPreamble() throws MessagingException {
+    return mPreamble;
+  }
+
+  public void setPreamble(String preamble) throws MessagingException {
+    this.mPreamble = preamble;
+  }
+
+  @Override
+  public String getContentType() throws MessagingException {
+    return mContentType;
+  }
+
+  public void setSubType(String subType) throws MessagingException {
+    this.mSubType = subType;
+    mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary);
+  }
+
+  @Override
+  public void writeTo(OutputStream out) throws IOException, MessagingException {
+    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+
+    if (mPreamble != null) {
+      writer.write(mPreamble + "\r\n");
+    }
+
+    for (int i = 0, count = mParts.size(); i < count; i++) {
+      BodyPart bodyPart = mParts.get(i);
+      writer.write("--" + mBoundary + "\r\n");
+      writer.flush();
+      bodyPart.writeTo(out);
+      writer.write("\r\n");
+    }
+
+    writer.write("--" + mBoundary + "--\r\n");
+    writer.flush();
+  }
+
+  @Override
+  public InputStream getInputStream() throws MessagingException {
+    return null;
+  }
+
+  public String getSubTypeForTest() {
+    return mSubType;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/MimeUtility.java b/java/com/android/voicemail/impl/mail/internet/MimeUtility.java
new file mode 100644
index 0000000..9984602
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/MimeUtility.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.internet;
+
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Base64DataException;
+import android.util.Base64InputStream;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.BodyPart;
+import com.android.voicemail.impl.mail.Message;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Multipart;
+import com.android.voicemail.impl.mail.Part;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.commons.io.IOUtils;
+import org.apache.james.mime4j.codec.EncoderUtil;
+import org.apache.james.mime4j.decoder.DecoderUtil;
+import org.apache.james.mime4j.decoder.QuotedPrintableInputStream;
+import org.apache.james.mime4j.util.CharsetUtil;
+
+public class MimeUtility {
+  private static final String LOG_TAG = "Email";
+
+  public static final String MIME_TYPE_RFC822 = "message/rfc822";
+  private static final Pattern PATTERN_CR_OR_LF = Pattern.compile("\r|\n");
+
+  /**
+   * Replace sequences of CRLF+WSP with WSP. Tries to preserve original string object whenever
+   * possible.
+   */
+  public static String unfold(String s) {
+    if (s == null) {
+      return null;
+    }
+    Matcher patternMatcher = PATTERN_CR_OR_LF.matcher(s);
+    if (patternMatcher.find()) {
+      patternMatcher.reset();
+      s = patternMatcher.replaceAll("");
+    }
+    return s;
+  }
+
+  public static String decode(String s) {
+    if (s == null) {
+      return null;
+    }
+    return DecoderUtil.decodeEncodedWords(s);
+  }
+
+  public static String unfoldAndDecode(String s) {
+    return decode(unfold(s));
+  }
+
+  // TODO implement proper foldAndEncode
+  // NOTE: When this really works, we *must* remove all calls to foldAndEncode2() to prevent
+  // duplication of encoding.
+  public static String foldAndEncode(String s) {
+    return s;
+  }
+
+  /**
+   * INTERIM version of foldAndEncode that will be used only by Subject: headers. This is safer than
+   * implementing foldAndEncode() (see above) and risking unknown damage to other headers.
+   *
+   * <p>TODO: Copy this code to foldAndEncode(), get rid of this function, confirm all working OK.
+   *
+   * @param s original string to encode and fold
+   * @param usedCharacters number of characters already used up by header name
+   * @return the String ready to be transmitted
+   */
+  public static String foldAndEncode2(String s, int usedCharacters) {
+    // james.mime4j.codec.EncoderUtil.java
+    // encode:  encodeIfNecessary(text, usage, numUsedInHeaderName)
+    // Usage.TEXT_TOKENlooks like the right thing for subjects
+    // use WORD_ENTITY for address/names
+
+    String encoded = EncoderUtil.encodeIfNecessary(s, EncoderUtil.Usage.TEXT_TOKEN, usedCharacters);
+
+    return fold(encoded, usedCharacters);
+  }
+
+  /**
+   * INTERIM: From newer version of org.apache.james (but we don't want to import the entire
+   * MimeUtil class).
+   *
+   * <p>Splits the specified string into a multiple-line representation with lines no longer than 76
+   * characters (because the line might contain encoded words; see <a
+   * href='http://www.faqs.org/rfcs/rfc2047.html'>RFC 2047</a> section 2). If the string contains
+   * non-whitespace sequences longer than 76 characters a line break is inserted at the whitespace
+   * character following the sequence resulting in a line longer than 76 characters.
+   *
+   * @param s string to split.
+   * @param usedCharacters number of characters already used up. Usually the number of characters
+   *     for header field name plus colon and one space.
+   * @return a multiple-line representation of the given string.
+   */
+  public static String fold(String s, int usedCharacters) {
+    final int maxCharacters = 76;
+
+    final int length = s.length();
+    if (usedCharacters + length <= maxCharacters) {
+      return s;
+    }
+
+    StringBuilder sb = new StringBuilder();
+
+    int lastLineBreak = -usedCharacters;
+    int wspIdx = indexOfWsp(s, 0);
+    while (true) {
+      if (wspIdx == length) {
+        sb.append(s.substring(Math.max(0, lastLineBreak)));
+        return sb.toString();
+      }
+
+      int nextWspIdx = indexOfWsp(s, wspIdx + 1);
+
+      if (nextWspIdx - lastLineBreak > maxCharacters) {
+        sb.append(s.substring(Math.max(0, lastLineBreak), wspIdx));
+        sb.append("\r\n");
+        lastLineBreak = wspIdx;
+      }
+
+      wspIdx = nextWspIdx;
+    }
+  }
+
+  /**
+   * INTERIM: From newer version of org.apache.james (but we don't want to import the entire
+   * MimeUtil class).
+   *
+   * <p>Search for whitespace.
+   */
+  private static int indexOfWsp(String s, int fromIndex) {
+    final int len = s.length();
+    for (int index = fromIndex; index < len; index++) {
+      char c = s.charAt(index);
+      if (c == ' ' || c == '\t') {
+        return index;
+      }
+    }
+    return len;
+  }
+
+  /**
+   * Returns the named parameter of a header field. If name is null the first parameter is returned,
+   * or if there are no additional parameters in the field the entire field is returned. Otherwise
+   * the named parameter is searched for in a case insensitive fashion and returned. If the
+   * parameter cannot be found the method returns null.
+   *
+   * <p>TODO: quite inefficient with the inner trimming & splitting. TODO: Also has a latent bug:
+   * uses "startsWith" to match the name, which can false-positive. TODO: The doc says that for a
+   * null name you get the first param, but you get the header. Should probably just fix the doc,
+   * but if other code assumes that behavior, fix the code. TODO: Need to decode %-escaped strings,
+   * as in: filename="ab%22d". ('+' -> ' ' conversion too? check RFC)
+   *
+   * @param header
+   * @param name
+   * @return the entire header (if name=null), the found parameter, or null
+   */
+  public static String getHeaderParameter(String header, String name) {
+    if (header == null) {
+      return null;
+    }
+    String[] parts = unfold(header).split(";");
+    if (name == null) {
+      return parts[0].trim();
+    }
+    String lowerCaseName = name.toLowerCase();
+    for (String part : parts) {
+      if (part.trim().toLowerCase().startsWith(lowerCaseName)) {
+        String[] parameterParts = part.split("=", 2);
+        if (parameterParts.length < 2) {
+          return null;
+        }
+        String parameter = parameterParts[1].trim();
+        if (parameter.startsWith("\"") && parameter.endsWith("\"")) {
+          return parameter.substring(1, parameter.length() - 1);
+        } else {
+          return parameter;
+        }
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Reads the Part's body and returns a String based on any charset conversion that needed to be
+   * done.
+   *
+   * @param part The part containing a body
+   * @return a String containing the converted text in the body, or null if there was no text or an
+   *     error during conversion.
+   */
+  public static String getTextFromPart(Part part) {
+    try {
+      if (part != null && part.getBody() != null) {
+        InputStream in = part.getBody().getInputStream();
+        String mimeType = part.getMimeType();
+        if (mimeType != null && MimeUtility.mimeTypeMatches(mimeType, "text/*")) {
+          /*
+           * Now we read the part into a buffer for further processing. Because
+           * the stream is now wrapped we'll remove any transfer encoding at this point.
+           */
+          ByteArrayOutputStream out = new ByteArrayOutputStream();
+          IOUtils.copy(in, out);
+          in.close();
+          in = null; // we want all of our memory back, and close might not release
+
+          /*
+           * We've got a text part, so let's see if it needs to be processed further.
+           */
+          String charset = getHeaderParameter(part.getContentType(), "charset");
+          if (charset != null) {
+            /*
+             * See if there is conversion from the MIME charset to the Java one.
+             */
+            charset = CharsetUtil.toJavaCharset(charset);
+          }
+          /*
+           * No encoding, so use us-ascii, which is the standard.
+           */
+          if (charset == null) {
+            charset = "ASCII";
+          }
+          /*
+           * Convert and return as new String
+           */
+          String result = out.toString(charset);
+          out.close();
+          return result;
+        }
+      }
+
+    } catch (OutOfMemoryError oom) {
+      /*
+       * If we are not able to process the body there's nothing we can do about it. Return
+       * null and let the upper layers handle the missing content.
+       */
+      VvmLog.e(LOG_TAG, "Unable to getTextFromPart " + oom.toString());
+    } catch (Exception e) {
+      /*
+       * If we are not able to process the body there's nothing we can do about it. Return
+       * null and let the upper layers handle the missing content.
+       */
+      VvmLog.e(LOG_TAG, "Unable to getTextFromPart " + e.toString());
+    }
+    return null;
+  }
+
+  /**
+   * Returns true if the given mimeType matches the matchAgainst specification. The comparison
+   * ignores case and the matchAgainst string may include "*" for a wildcard (e.g. "image/*").
+   *
+   * @param mimeType A MIME type to check.
+   * @param matchAgainst A MIME type to check against. May include wildcards.
+   * @return true if the mimeType matches
+   */
+  public static boolean mimeTypeMatches(String mimeType, String matchAgainst) {
+    Pattern p = Pattern.compile(matchAgainst.replaceAll("\\*", "\\.\\*"), Pattern.CASE_INSENSITIVE);
+    return p.matcher(mimeType).matches();
+  }
+
+  /**
+   * Returns true if the given mimeType matches any of the matchAgainst specifications. The
+   * comparison ignores case and the matchAgainst strings may include "*" for a wildcard (e.g.
+   * "image/*").
+   *
+   * @param mimeType A MIME type to check.
+   * @param matchAgainst An array of MIME types to check against. May include wildcards.
+   * @return true if the mimeType matches any of the matchAgainst strings
+   */
+  public static boolean mimeTypeMatches(String mimeType, String[] matchAgainst) {
+    for (String matchType : matchAgainst) {
+      if (mimeTypeMatches(mimeType, matchType)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Given an input stream and a transfer encoding, return a wrapped input stream for that encoding
+   * (or the original if none is required)
+   *
+   * @param in the input stream
+   * @param contentTransferEncoding the content transfer encoding
+   * @return a properly wrapped stream
+   */
+  public static InputStream getInputStreamForContentTransferEncoding(
+      InputStream in, String contentTransferEncoding) {
+    if (contentTransferEncoding != null) {
+      contentTransferEncoding = MimeUtility.getHeaderParameter(contentTransferEncoding, null);
+      if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) {
+        in = new QuotedPrintableInputStream(in);
+      } else if ("base64".equalsIgnoreCase(contentTransferEncoding)) {
+        in = new Base64InputStream(in, Base64.DEFAULT);
+      }
+    }
+    return in;
+  }
+
+  /** Removes any content transfer encoding from the stream and returns a Body. */
+  public static Body decodeBody(InputStream in, String contentTransferEncoding) throws IOException {
+    /*
+     * We'll remove any transfer encoding by wrapping the stream.
+     */
+    in = getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
+    BinaryTempFileBody tempBody = new BinaryTempFileBody();
+    OutputStream out = tempBody.getOutputStream();
+    try {
+      IOUtils.copy(in, out);
+    } catch (Base64DataException bde) {
+      // TODO Need to fix this somehow
+      //String warning = "\n\n" + Email.getMessageDecodeErrorString();
+      //out.write(warning.getBytes());
+    } finally {
+      out.close();
+    }
+    return tempBody;
+  }
+
+  /**
+   * Recursively scan a Part (usually a Message) and sort out which of its children will be
+   * "viewable" and which will be attachments.
+   *
+   * @param part The part to be broken down
+   * @param viewables This arraylist will be populated with all parts that appear to be the
+   *     "message" (e.g. text/plain & text/html)
+   * @param attachments This arraylist will be populated with all parts that appear to be
+   *     attachments (including inlines)
+   * @throws MessagingException
+   */
+  public static void collectParts(Part part, ArrayList<Part> viewables, ArrayList<Part> attachments)
+      throws MessagingException {
+    String disposition = part.getDisposition();
+    String dispositionType = MimeUtility.getHeaderParameter(disposition, null);
+    // If a disposition is not specified, default to "inline"
+    boolean inline =
+        TextUtils.isEmpty(dispositionType) || "inline".equalsIgnoreCase(dispositionType);
+    // The lower-case mime type
+    String mimeType = part.getMimeType().toLowerCase();
+
+    if (part.getBody() instanceof Multipart) {
+      // If the part is Multipart but not alternative it's either mixed or
+      // something we don't know about, which means we treat it as mixed
+      // per the spec. We just process its pieces recursively.
+      MimeMultipart mp = (MimeMultipart) part.getBody();
+      boolean foundHtml = false;
+      if (mp.getSubTypeForTest().equals("alternative")) {
+        for (int i = 0; i < mp.getCount(); i++) {
+          if (mp.getBodyPart(i).isMimeType("text/html")) {
+            foundHtml = true;
+            break;
+          }
+        }
+      }
+      for (int i = 0; i < mp.getCount(); i++) {
+        // See if we have text and html
+        BodyPart bp = mp.getBodyPart(i);
+        // If there's html, don't bother loading text
+        if (foundHtml && bp.isMimeType("text/plain")) {
+          continue;
+        }
+        collectParts(bp, viewables, attachments);
+      }
+    } else if (part.getBody() instanceof Message) {
+      // If the part is an embedded message we just continue to process
+      // it, pulling any viewables or attachments into the running list.
+      Message message = (Message) part.getBody();
+      collectParts(message, viewables, attachments);
+    } else if (inline && (mimeType.startsWith("text") || (mimeType.startsWith("image")))) {
+      // We'll treat text and images as viewables
+      viewables.add(part);
+    } else {
+      // Everything else is an attachment.
+      attachments.add(part);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/TextBody.java b/java/com/android/voicemail/impl/mail/internet/TextBody.java
new file mode 100644
index 0000000..dae5625
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/TextBody.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.internet;
+
+import android.util.Base64;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.MessagingException;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+public class TextBody implements Body {
+  String mBody;
+
+  public TextBody(String body) {
+    this.mBody = body;
+  }
+
+  @Override
+  public void writeTo(OutputStream out) throws IOException, MessagingException {
+    byte[] bytes = mBody.getBytes("UTF-8");
+    out.write(Base64.encode(bytes, Base64.CRLF));
+  }
+
+  /**
+   * Get the text of the body in it's unencoded format.
+   *
+   * @return
+   */
+  public String getText() {
+    return mBody;
+  }
+
+  /** Returns an InputStream that reads this body's text in UTF-8 format. */
+  @Override
+  public InputStream getInputStream() throws MessagingException {
+    try {
+      byte[] b = mBody.getBytes("UTF-8");
+      return new ByteArrayInputStream(b);
+    } catch (UnsupportedEncodingException usee) {
+      return null;
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/ImapConnection.java b/java/com/android/voicemail/impl/mail/store/ImapConnection.java
new file mode 100644
index 0000000..0a48dfc
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/ImapConnection.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.store;
+
+import android.util.ArraySet;
+import android.util.Base64;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.mail.AuthenticationFailedException;
+import com.android.voicemail.impl.mail.CertificateValidationException;
+import com.android.voicemail.impl.mail.MailTransport;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.store.ImapStore.ImapException;
+import com.android.voicemail.impl.mail.store.imap.DigestMd5Utils;
+import com.android.voicemail.impl.mail.store.imap.ImapConstants;
+import com.android.voicemail.impl.mail.store.imap.ImapResponse;
+import com.android.voicemail.impl.mail.store.imap.ImapResponseParser;
+import com.android.voicemail.impl.mail.store.imap.ImapUtility;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.net.ssl.SSLException;
+
+/** A cacheable class that stores the details for a single IMAP connection. */
+public class ImapConnection {
+  private final String TAG = "ImapConnection";
+
+  private String mLoginPhrase;
+  private ImapStore mImapStore;
+  private MailTransport mTransport;
+  private ImapResponseParser mParser;
+  private Set<String> mCapabilities = new ArraySet<>();
+
+  static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
+
+  /**
+   * Next tag to use. All connections associated to the same ImapStore instance share the same
+   * counter to make tests simpler. (Some of the tests involve multiple connections but only have a
+   * single counter to track the tag.)
+   */
+  private final AtomicInteger mNextCommandTag = new AtomicInteger(0);
+
+  ImapConnection(ImapStore store) {
+    setStore(store);
+  }
+
+  void setStore(ImapStore store) {
+    // TODO: maybe we should throw an exception if the connection is not closed here,
+    // if it's not currently closed, then we won't reopen it, so if the credentials have
+    // changed, the connection will not be reestablished.
+    mImapStore = store;
+    mLoginPhrase = null;
+  }
+
+  /**
+   * Generates and returns the phrase to be used for authentication. This will be a LOGIN with
+   * username and password.
+   *
+   * @return the login command string to sent to the IMAP server
+   */
+  String getLoginPhrase() {
+    if (mLoginPhrase == null) {
+      if (mImapStore.getUsername() != null && mImapStore.getPassword() != null) {
+        // build the LOGIN string once (instead of over-and-over again.)
+        // apply the quoting here around the built-up password
+        mLoginPhrase =
+            ImapConstants.LOGIN
+                + " "
+                + mImapStore.getUsername()
+                + " "
+                + ImapUtility.imapQuoted(mImapStore.getPassword());
+      }
+    }
+    return mLoginPhrase;
+  }
+
+  public void open() throws IOException, MessagingException {
+    if (mTransport != null && mTransport.isOpen()) {
+      return;
+    }
+
+    try {
+      // copy configuration into a clean transport, if necessary
+      if (mTransport == null) {
+        mTransport = mImapStore.cloneTransport();
+      }
+
+      mTransport.open();
+
+      createParser();
+
+      // The server should greet us with something like
+      // * OK IMAP4rev1 Server
+      // consume the response before doing anything else.
+      ImapResponse response = mParser.readResponse(false);
+      if (!response.isOk()) {
+        mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_INVALID_INITIAL_SERVER_RESPONSE);
+        throw new MessagingException(
+            MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR,
+            "Invalid server initial response");
+      }
+
+      queryCapability();
+
+      maybeDoStartTls();
+
+      // LOGIN
+      doLogin();
+    } catch (SSLException e) {
+      LogUtils.d(TAG, "SSLException ", e);
+      mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_SSL_EXCEPTION);
+      throw new CertificateValidationException(e.getMessage(), e);
+    } catch (IOException ioe) {
+      LogUtils.d(TAG, "IOException", ioe);
+      mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_IOE_ON_OPEN);
+      throw ioe;
+    } finally {
+      destroyResponses();
+    }
+  }
+
+  void logout() {
+    try {
+      sendCommand(ImapConstants.LOGOUT, false);
+      if (!mParser.readResponse(true).is(0, ImapConstants.BYE)) {
+        VvmLog.e(TAG, "Server did not respond LOGOUT with BYE");
+      }
+      if (!mParser.readResponse(false).isOk()) {
+        VvmLog.e(TAG, "Server did not respond OK after LOGOUT");
+      }
+    } catch (IOException | MessagingException e) {
+      VvmLog.e(TAG, "Error while logging out:" + e);
+    }
+  }
+
+  /**
+   * Closes the connection and releases all resources. This connection can not be used again until
+   * {@link #setStore(ImapStore)} is called.
+   */
+  void close() {
+    if (mTransport != null) {
+      logout();
+      mTransport.close();
+      mTransport = null;
+    }
+    destroyResponses();
+    mParser = null;
+    mImapStore = null;
+  }
+
+  /** Attempts to convert the connection into secure connection. */
+  private void maybeDoStartTls() throws IOException, MessagingException {
+    // STARTTLS is required in the OMTP standard but not every implementation support it.
+    // Make sure the server does have this capability
+    if (hasCapability(ImapConstants.CAPABILITY_STARTTLS)) {
+      executeSimpleCommand(ImapConstants.STARTTLS);
+      mTransport.reopenTls();
+      createParser();
+      // The cached capabilities should be refreshed after TLS is established.
+      queryCapability();
+    }
+  }
+
+  /** Logs into the IMAP server */
+  private void doLogin() throws IOException, MessagingException, AuthenticationFailedException {
+    try {
+      if (mCapabilities.contains(ImapConstants.CAPABILITY_AUTH_DIGEST_MD5)) {
+        doDigestMd5Auth();
+      } else {
+        executeSimpleCommand(getLoginPhrase(), true);
+      }
+    } catch (ImapException ie) {
+      LogUtils.d(TAG, "ImapException", ie);
+      String status = ie.getStatus();
+      String statusMessage = ie.getStatusMessage();
+      String alertText = ie.getAlertText();
+
+      if (ImapConstants.NO.equals(status)) {
+        switch (statusMessage) {
+          case ImapConstants.NO_UNKNOWN_USER:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_USER);
+            break;
+          case ImapConstants.NO_UNKNOWN_CLIENT:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_DEVICE);
+            break;
+          case ImapConstants.NO_INVALID_PASSWORD:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_INVALID_PASSWORD);
+            break;
+          case ImapConstants.NO_MAILBOX_NOT_INITIALIZED:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_MAILBOX_NOT_INITIALIZED);
+            break;
+          case ImapConstants.NO_SERVICE_IS_NOT_PROVISIONED:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_PROVISIONED);
+            break;
+          case ImapConstants.NO_SERVICE_IS_NOT_ACTIVATED:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_ACTIVATED);
+            break;
+          case ImapConstants.NO_USER_IS_BLOCKED:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_USER_IS_BLOCKED);
+            break;
+          case ImapConstants.NO_APPLICATION_ERROR:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE);
+            break;
+          default:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_BAD_IMAP_CREDENTIAL);
+        }
+        throw new AuthenticationFailedException(alertText, ie);
+      }
+
+      mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE);
+      throw new MessagingException(alertText, ie);
+    }
+  }
+
+  private void doDigestMd5Auth() throws IOException, MessagingException {
+
+    //  Initiate the authentication.
+    //  The server will issue us a challenge, asking to run MD5 on the nonce with our password
+    //  and other data, including the cnonce we randomly generated.
+    //
+    //  C: a AUTHENTICATE DIGEST-MD5
+    //  S: (BASE64) realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",qop="auth",
+    //             algorithm=md5-sess,charset=utf-8
+    List<ImapResponse> responses =
+        executeSimpleCommand(ImapConstants.AUTHENTICATE + " " + ImapConstants.AUTH_DIGEST_MD5);
+    String decodedChallenge = decodeBase64(responses.get(0).getStringOrEmpty(0).getString());
+
+    Map<String, String> challenge = DigestMd5Utils.parseDigestMessage(decodedChallenge);
+    DigestMd5Utils.Data data = new DigestMd5Utils.Data(mImapStore, mTransport, challenge);
+
+    String response = data.createResponse();
+    //  Respond to the challenge. If the server accepts it, it will reply a response-auth which
+    //  is the MD5 of our password and the cnonce we've provided, to prove the server does know
+    //  the password.
+    //
+    //  C: (BASE64) charset=utf-8,username="chris",realm="elwood.innosoft.com",
+    //              nonce="OA6MG9tEQGm2hh",nc=00000001,cnonce="OA6MHXh6VqTrRk",
+    //              digest-uri="imap/elwood.innosoft.com",
+    //              response=d388dad90d4bbd760a152321f2143af7,qop=auth
+    //  S: (BASE64) rspauth=ea40f60335c427b5527b84dbabcdfffd
+
+    responses = executeContinuationResponse(encodeBase64(response), true);
+
+    // Verify response-auth.
+    // If failed verifyResponseAuth() will throw a MessagingException, terminating the
+    // connection
+    String decodedResponseAuth = decodeBase64(responses.get(0).getStringOrEmpty(0).getString());
+    data.verifyResponseAuth(decodedResponseAuth);
+
+    //  Send a empty response to indicate we've accepted the response-auth
+    //
+    //  C: (empty)
+    //  S: a OK User logged in
+    executeContinuationResponse("", false);
+  }
+
+  private static String decodeBase64(String string) {
+    return new String(Base64.decode(string, Base64.DEFAULT));
+  }
+
+  private static String encodeBase64(String string) {
+    return Base64.encodeToString(string.getBytes(), Base64.NO_WRAP);
+  }
+
+  private void queryCapability() throws IOException, MessagingException {
+    List<ImapResponse> responses = executeSimpleCommand(ImapConstants.CAPABILITY);
+    mCapabilities.clear();
+    Set<String> disabledCapabilities =
+        mImapStore.getImapHelper().getConfig().getDisabledCapabilities();
+    for (ImapResponse response : responses) {
+      if (response.isTagged()) {
+        continue;
+      }
+      for (int i = 0; i < response.size(); i++) {
+        String capability = response.getStringOrEmpty(i).getString();
+        if (disabledCapabilities != null) {
+          if (!disabledCapabilities.contains(capability)) {
+            mCapabilities.add(capability);
+          }
+        } else {
+          mCapabilities.add(capability);
+        }
+      }
+    }
+
+    LogUtils.d(TAG, "Capabilities: " + mCapabilities.toString());
+  }
+
+  private boolean hasCapability(String capability) {
+    return mCapabilities.contains(capability);
+  }
+  /**
+   * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and set it to
+   * {@link #mParser}.
+   *
+   * <p>If we already have an {@link ImapResponseParser}, we {@link #destroyResponses()} and throw
+   * it away.
+   */
+  private void createParser() {
+    destroyResponses();
+    mParser = new ImapResponseParser(mTransport.getInputStream());
+  }
+
+  public void destroyResponses() {
+    if (mParser != null) {
+      mParser.destroyResponses();
+    }
+  }
+
+  public ImapResponse readResponse() throws IOException, MessagingException {
+    return mParser.readResponse(false);
+  }
+
+  public List<ImapResponse> executeSimpleCommand(String command)
+      throws IOException, MessagingException {
+    return executeSimpleCommand(command, false);
+  }
+
+  /**
+   * Send a single command to the server. The command will be preceded by an IMAP command tag and
+   * followed by \r\n (caller need not supply them). Execute a simple command at the server, a
+   * simple command being one that is sent in a single line of text
+   *
+   * @param command the command to send to the server
+   * @param sensitive whether the command should be redacted in logs (used for login)
+   * @return a list of ImapResponses
+   * @throws IOException
+   * @throws MessagingException
+   */
+  public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
+      throws IOException, MessagingException {
+    // TODO: It may be nice to catch IOExceptions and close the connection here.
+    // Currently, we expect callers to do that, but if they fail to we'll be in a broken state.
+    sendCommand(command, sensitive);
+    return getCommandResponses();
+  }
+
+  public String sendCommand(String command, boolean sensitive)
+      throws IOException, MessagingException {
+    open();
+
+    if (mTransport == null) {
+      throw new IOException("Null transport");
+    }
+    String tag = Integer.toString(mNextCommandTag.incrementAndGet());
+    String commandToSend = tag + " " + command;
+    mTransport.writeLine(commandToSend, (sensitive ? IMAP_REDACTED_LOG : command));
+    return tag;
+  }
+
+  List<ImapResponse> executeContinuationResponse(String response, boolean sensitive)
+      throws IOException, MessagingException {
+    mTransport.writeLine(response, (sensitive ? IMAP_REDACTED_LOG : response));
+    return getCommandResponses();
+  }
+
+  /**
+   * Read and return all of the responses from the most recent command sent to the server
+   *
+   * @return a list of ImapResponses
+   * @throws IOException
+   * @throws MessagingException
+   */
+  List<ImapResponse> getCommandResponses() throws IOException, MessagingException {
+    final List<ImapResponse> responses = new ArrayList<ImapResponse>();
+    ImapResponse response;
+    do {
+      response = mParser.readResponse(false);
+      responses.add(response);
+    } while (!(response.isTagged() || response.isContinuationRequest()));
+
+    if (!(response.isOk() || response.isContinuationRequest())) {
+      final String toString = response.toString();
+      final String status = response.getStatusOrEmpty().getString();
+      final String statusMessage = response.getStatusResponseTextOrEmpty().getString();
+      final String alert = response.getAlertTextOrEmpty().getString();
+      final String responseCode = response.getResponseCodeOrEmpty().getString();
+      destroyResponses();
+      throw new ImapException(toString, status, statusMessage, alert, responseCode);
+    }
+    return responses;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/ImapFolder.java b/java/com/android/voicemail/impl/mail/store/ImapFolder.java
new file mode 100644
index 0000000..1d9b011
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/ImapFolder.java
@@ -0,0 +1,797 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.store;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Base64DataException;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.mail.AuthenticationFailedException;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.FetchProfile;
+import com.android.voicemail.impl.mail.Flag;
+import com.android.voicemail.impl.mail.Message;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Part;
+import com.android.voicemail.impl.mail.internet.BinaryTempFileBody;
+import com.android.voicemail.impl.mail.internet.MimeBodyPart;
+import com.android.voicemail.impl.mail.internet.MimeHeader;
+import com.android.voicemail.impl.mail.internet.MimeMultipart;
+import com.android.voicemail.impl.mail.internet.MimeUtility;
+import com.android.voicemail.impl.mail.store.ImapStore.ImapException;
+import com.android.voicemail.impl.mail.store.ImapStore.ImapMessage;
+import com.android.voicemail.impl.mail.store.imap.ImapConstants;
+import com.android.voicemail.impl.mail.store.imap.ImapElement;
+import com.android.voicemail.impl.mail.store.imap.ImapList;
+import com.android.voicemail.impl.mail.store.imap.ImapResponse;
+import com.android.voicemail.impl.mail.store.imap.ImapString;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import com.android.voicemail.impl.mail.utils.Utility;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+
+public class ImapFolder {
+  private static final String TAG = "ImapFolder";
+  private static final String[] PERMANENT_FLAGS = {
+    Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED
+  };
+  private static final int COPY_BUFFER_SIZE = 16 * 1024;
+
+  private final ImapStore mStore;
+  private final String mName;
+  private int mMessageCount = -1;
+  private ImapConnection mConnection;
+  private String mMode;
+  private boolean mExists;
+  /** A set of hashes that can be used to track dirtiness */
+  Object mHash[];
+
+  public static final String MODE_READ_ONLY = "mode_read_only";
+  public static final String MODE_READ_WRITE = "mode_read_write";
+
+  public ImapFolder(ImapStore store, String name) {
+    mStore = store;
+    mName = name;
+  }
+
+  /** Callback for each message retrieval. */
+  public interface MessageRetrievalListener {
+    public void messageRetrieved(Message message);
+  }
+
+  private void destroyResponses() {
+    if (mConnection != null) {
+      mConnection.destroyResponses();
+    }
+  }
+
+  public void open(String mode) throws MessagingException {
+    try {
+      if (isOpen()) {
+        throw new AssertionError("Duplicated open on ImapFolder");
+      }
+      synchronized (this) {
+        mConnection = mStore.getConnection();
+      }
+      // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
+      // $MDNSent)
+      // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
+      // NonJunk $MDNSent \*)] Flags permitted.
+      // * 23 EXISTS
+      // * 0 RECENT
+      // * OK [UIDVALIDITY 1125022061] UIDs valid
+      // * OK [UIDNEXT 57576] Predicted next UID
+      // 2 OK [READ-WRITE] Select completed.
+      try {
+        doSelect();
+      } catch (IOException ioe) {
+        throw ioExceptionHandler(mConnection, ioe);
+      } finally {
+        destroyResponses();
+      }
+    } catch (AuthenticationFailedException e) {
+      // Don't cache this connection, so we're forced to try connecting/login again
+      mConnection = null;
+      close(false);
+      throw e;
+    } catch (MessagingException e) {
+      mExists = false;
+      close(false);
+      throw e;
+    }
+  }
+
+  public boolean isOpen() {
+    return mExists && mConnection != null;
+  }
+
+  public String getMode() {
+    return mMode;
+  }
+
+  public void close(boolean expunge) {
+    if (expunge) {
+      try {
+        expunge();
+      } catch (MessagingException e) {
+        LogUtils.e(TAG, e, "Messaging Exception");
+      }
+    }
+    mMessageCount = -1;
+    synchronized (this) {
+      mConnection = null;
+    }
+  }
+
+  public int getMessageCount() {
+    return mMessageCount;
+  }
+
+  String[] getSearchUids(List<ImapResponse> responses) {
+    // S: * SEARCH 2 3 6
+    final ArrayList<String> uids = new ArrayList<String>();
+    for (ImapResponse response : responses) {
+      if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
+        continue;
+      }
+      // Found SEARCH response data
+      for (int i = 1; i < response.size(); i++) {
+        ImapString s = response.getStringOrEmpty(i);
+        if (s.isString()) {
+          uids.add(s.getString());
+        }
+      }
+    }
+    return uids.toArray(Utility.EMPTY_STRINGS);
+  }
+
+  @VisibleForTesting
+  String[] searchForUids(String searchCriteria) throws MessagingException {
+    checkOpen();
+    try {
+      try {
+        final String command = ImapConstants.UID_SEARCH + " " + searchCriteria;
+        final String[] result = getSearchUids(mConnection.executeSimpleCommand(command));
+        LogUtils.d(TAG, "searchForUids '" + searchCriteria + "' results: " + result.length);
+        return result;
+      } catch (ImapException me) {
+        LogUtils.d(TAG, "ImapException in search: " + searchCriteria, me);
+        return Utility.EMPTY_STRINGS; // Not found
+      } catch (IOException ioe) {
+        LogUtils.d(TAG, "IOException in search: " + searchCriteria, ioe);
+        mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+        throw ioExceptionHandler(mConnection, ioe);
+      }
+    } finally {
+      destroyResponses();
+    }
+  }
+
+  @Nullable
+  public Message getMessage(String uid) throws MessagingException {
+    checkOpen();
+
+    final String[] uids = searchForUids(ImapConstants.UID + " " + uid);
+    for (int i = 0; i < uids.length; i++) {
+      if (uids[i].equals(uid)) {
+        return new ImapMessage(uid, this);
+      }
+    }
+    LogUtils.e(TAG, "UID " + uid + " not found on server");
+    return null;
+  }
+
+  @VisibleForTesting
+  protected static boolean isAsciiString(String str) {
+    int len = str.length();
+    for (int i = 0; i < len; i++) {
+      char c = str.charAt(i);
+      if (c >= 128) return false;
+    }
+    return true;
+  }
+
+  public Message[] getMessages(String[] uids) throws MessagingException {
+    if (uids == null) {
+      uids = searchForUids("1:* NOT DELETED");
+    }
+    return getMessagesInternal(uids);
+  }
+
+  public Message[] getMessagesInternal(String[] uids) {
+    final ArrayList<Message> messages = new ArrayList<Message>(uids.length);
+    for (int i = 0; i < uids.length; i++) {
+      final String uid = uids[i];
+      final ImapMessage message = new ImapMessage(uid, this);
+      messages.add(message);
+    }
+    return messages.toArray(Message.EMPTY_ARRAY);
+  }
+
+  public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
+      throws MessagingException {
+    try {
+      fetchInternal(messages, fp, listener);
+    } catch (RuntimeException e) { // Probably a parser error.
+      LogUtils.w(TAG, "Exception detected: " + e.getMessage());
+      throw e;
+    }
+  }
+
+  public void fetchInternal(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
+      throws MessagingException {
+    if (messages.length == 0) {
+      return;
+    }
+    checkOpen();
+    ArrayMap<String, Message> messageMap = new ArrayMap<String, Message>();
+    for (Message m : messages) {
+      messageMap.put(m.getUid(), m);
+    }
+
+    /*
+     * Figure out what command we are going to run:
+     * FLAGS     - UID FETCH (FLAGS)
+     * ENVELOPE  - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[
+     *                            HEADER.FIELDS (date subject from content-type to cc)])
+     * STRUCTURE - UID FETCH (BODYSTRUCTURE)
+     * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned
+     * BODY      - UID FETCH (BODY.PEEK[])
+     * Part      - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID
+     */
+
+    final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>();
+
+    fetchFields.add(ImapConstants.UID);
+    if (fp.contains(FetchProfile.Item.FLAGS)) {
+      fetchFields.add(ImapConstants.FLAGS);
+    }
+    if (fp.contains(FetchProfile.Item.ENVELOPE)) {
+      fetchFields.add(ImapConstants.INTERNALDATE);
+      fetchFields.add(ImapConstants.RFC822_SIZE);
+      fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS);
+    }
+    if (fp.contains(FetchProfile.Item.STRUCTURE)) {
+      fetchFields.add(ImapConstants.BODYSTRUCTURE);
+    }
+
+    if (fp.contains(FetchProfile.Item.BODY_SANE)) {
+      fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE);
+    }
+    if (fp.contains(FetchProfile.Item.BODY)) {
+      fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK);
+    }
+
+    // TODO Why are we only fetching the first part given?
+    final Part fetchPart = fp.getFirstPart();
+    if (fetchPart != null) {
+      final String[] partIds = fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
+      // TODO Why can a single part have more than one Id? And why should we only fetch
+      // the first id if there are more than one?
+      if (partIds != null) {
+        fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE + "[" + partIds[0] + "]");
+      }
+    }
+
+    try {
+      mConnection.sendCommand(
+          String.format(
+              Locale.US,
+              ImapConstants.UID_FETCH + " %s (%s)",
+              ImapStore.joinMessageUids(messages),
+              Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')),
+          false);
+      ImapResponse response;
+      do {
+        response = null;
+        try {
+          response = mConnection.readResponse();
+
+          if (!response.isDataResponse(1, ImapConstants.FETCH)) {
+            continue; // Ignore
+          }
+          final ImapList fetchList = response.getListOrEmpty(2);
+          final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID).getString();
+          if (TextUtils.isEmpty(uid)) continue;
+
+          ImapMessage message = (ImapMessage) messageMap.get(uid);
+          if (message == null) continue;
+
+          if (fp.contains(FetchProfile.Item.FLAGS)) {
+            final ImapList flags = fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS);
+            for (int i = 0, count = flags.size(); i < count; i++) {
+              final ImapString flag = flags.getStringOrEmpty(i);
+              if (flag.is(ImapConstants.FLAG_DELETED)) {
+                message.setFlagInternal(Flag.DELETED, true);
+              } else if (flag.is(ImapConstants.FLAG_ANSWERED)) {
+                message.setFlagInternal(Flag.ANSWERED, true);
+              } else if (flag.is(ImapConstants.FLAG_SEEN)) {
+                message.setFlagInternal(Flag.SEEN, true);
+              } else if (flag.is(ImapConstants.FLAG_FLAGGED)) {
+                message.setFlagInternal(Flag.FLAGGED, true);
+              }
+            }
+          }
+          if (fp.contains(FetchProfile.Item.ENVELOPE)) {
+            final Date internalDate =
+                fetchList.getKeyedStringOrEmpty(ImapConstants.INTERNALDATE).getDateOrNull();
+            final int size =
+                fetchList.getKeyedStringOrEmpty(ImapConstants.RFC822_SIZE).getNumberOrZero();
+            final String header =
+                fetchList
+                    .getKeyedStringOrEmpty(ImapConstants.BODY_BRACKET_HEADER, true)
+                    .getString();
+
+            message.setInternalDate(internalDate);
+            message.setSize(size);
+            message.parse(Utility.streamFromAsciiString(header));
+          }
+          if (fp.contains(FetchProfile.Item.STRUCTURE)) {
+            ImapList bs = fetchList.getKeyedListOrEmpty(ImapConstants.BODYSTRUCTURE);
+            if (!bs.isEmpty()) {
+              try {
+                parseBodyStructure(bs, message, ImapConstants.TEXT);
+              } catch (MessagingException e) {
+                LogUtils.v(TAG, e, "Error handling message");
+                message.setBody(null);
+              }
+            }
+          }
+          if (fp.contains(FetchProfile.Item.BODY) || fp.contains(FetchProfile.Item.BODY_SANE)) {
+            // Body is keyed by "BODY[]...".
+            // Previously used "BODY[..." but this can be confused with "BODY[HEADER..."
+            // TODO Should we accept "RFC822" as well??
+            ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true);
+            InputStream bodyStream = body.getAsStream();
+            message.parse(bodyStream);
+          }
+          if (fetchPart != null) {
+            InputStream bodyStream = fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream();
+            String encodings[] = fetchPart.getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING);
+
+            String contentTransferEncoding = null;
+            if (encodings != null && encodings.length > 0) {
+              contentTransferEncoding = encodings[0];
+            } else {
+              // According to http://tools.ietf.org/html/rfc2045#section-6.1
+              // "7bit" is the default.
+              contentTransferEncoding = "7bit";
+            }
+
+            try {
+              // TODO Don't create 2 temp files.
+              // decodeBody creates BinaryTempFileBody, but we could avoid this
+              // if we implement ImapStringBody.
+              // (We'll need to share a temp file.  Protect it with a ref-count.)
+              message.setBody(
+                  decodeBody(
+                      mStore.getContext(),
+                      bodyStream,
+                      contentTransferEncoding,
+                      fetchPart.getSize(),
+                      listener));
+            } catch (Exception e) {
+              // TODO: Figure out what kinds of exceptions might actually be thrown
+              // from here. This blanket catch-all is because we're not sure what to
+              // do if we don't have a contentTransferEncoding, and we don't have
+              // time to figure out what exceptions might be thrown.
+              LogUtils.e(TAG, "Error fetching body %s", e);
+            }
+          }
+
+          if (listener != null) {
+            listener.messageRetrieved(message);
+          }
+        } finally {
+          destroyResponses();
+        }
+      } while (!response.isTagged());
+    } catch (IOException ioe) {
+      mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+      throw ioExceptionHandler(mConnection, ioe);
+    }
+  }
+
+  /**
+   * Removes any content transfer encoding from the stream and returns a Body. This code is
+   * taken/condensed from MimeUtility.decodeBody
+   */
+  private static Body decodeBody(
+      Context context,
+      InputStream in,
+      String contentTransferEncoding,
+      int size,
+      MessageRetrievalListener listener)
+      throws IOException {
+    // Get a properly wrapped input stream
+    in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
+    BinaryTempFileBody tempBody = new BinaryTempFileBody();
+    OutputStream out = tempBody.getOutputStream();
+    try {
+      byte[] buffer = new byte[COPY_BUFFER_SIZE];
+      int n = 0;
+      int count = 0;
+      while (-1 != (n = in.read(buffer))) {
+        out.write(buffer, 0, n);
+        count += n;
+      }
+    } catch (Base64DataException bde) {
+      String warning = "\n\nThere was an error while decoding the message.";
+      out.write(warning.getBytes());
+    } finally {
+      out.close();
+    }
+    return tempBody;
+  }
+
+  public String[] getPermanentFlags() {
+    return PERMANENT_FLAGS;
+  }
+
+  /**
+   * Handle any untagged responses that the caller doesn't care to handle themselves.
+   *
+   * @param responses
+   */
+  private void handleUntaggedResponses(List<ImapResponse> responses) {
+    for (ImapResponse response : responses) {
+      handleUntaggedResponse(response);
+    }
+  }
+
+  /**
+   * Handle an untagged response that the caller doesn't care to handle themselves.
+   *
+   * @param response
+   */
+  private void handleUntaggedResponse(ImapResponse response) {
+    if (response.isDataResponse(1, ImapConstants.EXISTS)) {
+      mMessageCount = response.getStringOrEmpty(0).getNumberOrZero();
+    }
+  }
+
+  private static void parseBodyStructure(ImapList bs, Part part, String id)
+      throws MessagingException {
+    if (bs.getElementOrNone(0).isList()) {
+      /*
+       * This is a multipart/*
+       */
+      MimeMultipart mp = new MimeMultipart();
+      for (int i = 0, count = bs.size(); i < count; i++) {
+        ImapElement e = bs.getElementOrNone(i);
+        if (e.isList()) {
+          /*
+           * For each part in the message we're going to add a new BodyPart and parse
+           * into it.
+           */
+          MimeBodyPart bp = new MimeBodyPart();
+          if (id.equals(ImapConstants.TEXT)) {
+            parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1));
+
+          } else {
+            parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1));
+          }
+          mp.addBodyPart(bp);
+
+        } else {
+          if (e.isString()) {
+            mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US));
+          }
+          break; // Ignore the rest of the list.
+        }
+      }
+      part.setBody(mp);
+    } else {
+      /*
+       * This is a body. We need to add as much information as we can find out about
+       * it to the Part.
+       */
+
+      /*
+      body type
+      body subtype
+      body parameter parenthesized list
+      body id
+      body description
+      body encoding
+      body size
+      */
+
+      final ImapString type = bs.getStringOrEmpty(0);
+      final ImapString subType = bs.getStringOrEmpty(1);
+      final String mimeType = (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US);
+
+      final ImapList bodyParams = bs.getListOrEmpty(2);
+      final ImapString cid = bs.getStringOrEmpty(3);
+      final ImapString encoding = bs.getStringOrEmpty(5);
+      final int size = bs.getStringOrEmpty(6).getNumberOrZero();
+
+      if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) {
+        // A body type of type MESSAGE and subtype RFC822
+        // contains, immediately after the basic fields, the
+        // envelope structure, body structure, and size in
+        // text lines of the encapsulated message.
+        // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL,
+        //     [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL]
+        /*
+         * This will be caught by fetch and handled appropriately.
+         */
+        throw new MessagingException(
+            "BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822 + " not yet supported.");
+      }
+
+      /*
+       * Set the content type with as much information as we know right now.
+       */
+      final StringBuilder contentType = new StringBuilder(mimeType);
+
+      /*
+       * If there are body params we might be able to get some more information out
+       * of them.
+       */
+      for (int i = 1, count = bodyParams.size(); i < count; i += 2) {
+
+        // TODO We need to convert " into %22, but
+        // because MimeUtility.getHeaderParameter doesn't recognize it,
+        // we can't fix it for now.
+        contentType.append(
+            String.format(
+                ";\n %s=\"%s\"",
+                bodyParams.getStringOrEmpty(i - 1).getString(),
+                bodyParams.getStringOrEmpty(i).getString()));
+      }
+
+      part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
+
+      // Extension items
+      final ImapList bodyDisposition;
+
+      if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) {
+        // If media-type is TEXT, 9th element might be: [body-fld-lines] := number
+        // So, if it's not a list, use 10th element.
+        // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.)
+        bodyDisposition = bs.getListOrEmpty(9);
+      } else {
+        bodyDisposition = bs.getListOrEmpty(8);
+      }
+
+      final StringBuilder contentDisposition = new StringBuilder();
+
+      if (bodyDisposition.size() > 0) {
+        final String bodyDisposition0Str =
+            bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US);
+        if (!TextUtils.isEmpty(bodyDisposition0Str)) {
+          contentDisposition.append(bodyDisposition0Str);
+        }
+
+        final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1);
+        if (!bodyDispositionParams.isEmpty()) {
+          /*
+           * If there is body disposition information we can pull some more
+           * information about the attachment out.
+           */
+          for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) {
+
+            // TODO We need to convert " into %22.  See above.
+            contentDisposition.append(
+                String.format(
+                    Locale.US,
+                    ";\n %s=\"%s\"",
+                    bodyDispositionParams
+                        .getStringOrEmpty(i - 1)
+                        .getString()
+                        .toLowerCase(Locale.US),
+                    bodyDispositionParams.getStringOrEmpty(i).getString()));
+          }
+        }
+      }
+
+      if ((size > 0)
+          && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size") == null)) {
+        contentDisposition.append(String.format(Locale.US, ";\n size=%d", size));
+      }
+
+      if (contentDisposition.length() > 0) {
+        /*
+         * Set the content disposition containing at least the size. Attachment
+         * handling code will use this down the road.
+         */
+        part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, contentDisposition.toString());
+      }
+
+      /*
+       * Set the Content-Transfer-Encoding header. Attachment code will use this
+       * to parse the body.
+       */
+      if (!encoding.isEmpty()) {
+        part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding.getString());
+      }
+
+      /*
+       * Set the Content-ID header.
+       */
+      if (!cid.isEmpty()) {
+        part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString());
+      }
+
+      if (size > 0) {
+        if (part instanceof ImapMessage) {
+          ((ImapMessage) part).setSize(size);
+        } else if (part instanceof MimeBodyPart) {
+          ((MimeBodyPart) part).setSize(size);
+        } else {
+          throw new MessagingException("Unknown part type " + part.toString());
+        }
+      }
+      part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
+    }
+  }
+
+  public Message[] expunge() throws MessagingException {
+    checkOpen();
+    try {
+      handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE));
+    } catch (IOException ioe) {
+      mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+      throw ioExceptionHandler(mConnection, ioe);
+    } finally {
+      destroyResponses();
+    }
+    return null;
+  }
+
+  public void setFlags(Message[] messages, String[] flags, boolean value)
+      throws MessagingException {
+    checkOpen();
+
+    String allFlags = "";
+    if (flags.length > 0) {
+      StringBuilder flagList = new StringBuilder();
+      for (int i = 0, count = flags.length; i < count; i++) {
+        String flag = flags[i];
+        if (flag == Flag.SEEN) {
+          flagList.append(" " + ImapConstants.FLAG_SEEN);
+        } else if (flag == Flag.DELETED) {
+          flagList.append(" " + ImapConstants.FLAG_DELETED);
+        } else if (flag == Flag.FLAGGED) {
+          flagList.append(" " + ImapConstants.FLAG_FLAGGED);
+        } else if (flag == Flag.ANSWERED) {
+          flagList.append(" " + ImapConstants.FLAG_ANSWERED);
+        }
+      }
+      allFlags = flagList.substring(1);
+    }
+    try {
+      mConnection.executeSimpleCommand(
+          String.format(
+              Locale.US,
+              ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)",
+              ImapStore.joinMessageUids(messages),
+              value ? "+" : "-",
+              allFlags));
+
+    } catch (IOException ioe) {
+      mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+      throw ioExceptionHandler(mConnection, ioe);
+    } finally {
+      destroyResponses();
+    }
+  }
+
+  /**
+   * Selects the folder for use. Before performing any operations on this folder, it must be
+   * selected.
+   */
+  private void doSelect() throws IOException, MessagingException {
+    final List<ImapResponse> responses =
+        mConnection.executeSimpleCommand(
+            String.format(Locale.US, ImapConstants.SELECT + " \"%s\"", mName));
+
+    // Assume the folder is opened read-write; unless we are notified otherwise
+    mMode = MODE_READ_WRITE;
+    int messageCount = -1;
+    for (ImapResponse response : responses) {
+      if (response.isDataResponse(1, ImapConstants.EXISTS)) {
+        messageCount = response.getStringOrEmpty(0).getNumberOrZero();
+      } else if (response.isOk()) {
+        final ImapString responseCode = response.getResponseCodeOrEmpty();
+        if (responseCode.is(ImapConstants.READ_ONLY)) {
+          mMode = MODE_READ_ONLY;
+        } else if (responseCode.is(ImapConstants.READ_WRITE)) {
+          mMode = MODE_READ_WRITE;
+        }
+      } else if (response.isTagged()) { // Not OK
+        mStore.getImapHelper().handleEvent(OmtpEvents.DATA_MAILBOX_OPEN_FAILED);
+        throw new MessagingException(
+            "Can't open mailbox: " + response.getStatusResponseTextOrEmpty());
+      }
+    }
+    if (messageCount == -1) {
+      throw new MessagingException("Did not find message count during select");
+    }
+    mMessageCount = messageCount;
+    mExists = true;
+  }
+
+  public class Quota {
+
+    public final int occupied;
+    public final int total;
+
+    public Quota(int occupied, int total) {
+      this.occupied = occupied;
+      this.total = total;
+    }
+  }
+
+  public Quota getQuota() throws MessagingException {
+    try {
+      final List<ImapResponse> responses =
+          mConnection.executeSimpleCommand(
+              String.format(Locale.US, ImapConstants.GETQUOTAROOT + " \"%s\"", mName));
+
+      for (ImapResponse response : responses) {
+        if (!response.isDataResponse(0, ImapConstants.QUOTA)) {
+          continue;
+        }
+        ImapList list = response.getListOrEmpty(2);
+        for (int i = 0; i < list.size(); i += 3) {
+          if (!list.getStringOrEmpty(i).is("voice")) {
+            continue;
+          }
+          return new Quota(
+              list.getStringOrEmpty(i + 1).getNumber(-1),
+              list.getStringOrEmpty(i + 2).getNumber(-1));
+        }
+      }
+    } catch (IOException ioe) {
+      mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+      throw ioExceptionHandler(mConnection, ioe);
+    } finally {
+      destroyResponses();
+    }
+    return null;
+  }
+
+  private void checkOpen() throws MessagingException {
+    if (!isOpen()) {
+      throw new MessagingException("Folder " + mName + " is not open.");
+    }
+  }
+
+  private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
+    LogUtils.d(TAG, "IO Exception detected: ", ioe);
+    connection.close();
+    if (connection == mConnection) {
+      mConnection = null; // To prevent close() from returning the connection to the pool.
+      close(false);
+    }
+    return new MessagingException(MessagingException.IOERROR, "IO Error", ioe);
+  }
+
+  public Message createMessage(String uid) {
+    return new ImapMessage(uid, this);
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/ImapStore.java b/java/com/android/voicemail/impl/mail/store/ImapStore.java
new file mode 100644
index 0000000..cadbe59
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/ImapStore.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store;
+
+import android.content.Context;
+import android.net.Network;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.mail.MailTransport;
+import com.android.voicemail.impl.mail.Message;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.internet.MimeMessage;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ImapStore {
+  /**
+   * A global suggestion to Store implementors on how much of the body should be returned on
+   * FetchProfile.Item.BODY_SANE requests. We'll use 125k now.
+   */
+  public static final int FETCH_BODY_SANE_SUGGESTED_SIZE = (125 * 1024);
+
+  private final Context mContext;
+  private final ImapHelper mHelper;
+  private final String mUsername;
+  private final String mPassword;
+  private final MailTransport mTransport;
+  private ImapConnection mConnection;
+
+  public static final int FLAG_NONE = 0x00; // No flags
+  public static final int FLAG_SSL = 0x01; // Use SSL
+  public static final int FLAG_TLS = 0x02; // Use TLS
+  public static final int FLAG_AUTHENTICATE = 0x04; // Use name/password for authentication
+  public static final int FLAG_TRUST_ALL = 0x08; // Trust all certificates
+  public static final int FLAG_OAUTH = 0x10; // Use OAuth for authentication
+
+  /** Contains all the information necessary to log into an imap server */
+  public ImapStore(
+      Context context,
+      ImapHelper helper,
+      String username,
+      String password,
+      int port,
+      String serverName,
+      int flags,
+      Network network) {
+    mContext = context;
+    mHelper = helper;
+    mUsername = username;
+    mPassword = password;
+    mTransport = new MailTransport(context, this.getImapHelper(), network, serverName, port, flags);
+  }
+
+  public Context getContext() {
+    return mContext;
+  }
+
+  public ImapHelper getImapHelper() {
+    return mHelper;
+  }
+
+  public String getUsername() {
+    return mUsername;
+  }
+
+  public String getPassword() {
+    return mPassword;
+  }
+
+  /** Returns a clone of the transport associated with this store. */
+  MailTransport cloneTransport() {
+    return mTransport.clone();
+  }
+
+  /** Returns UIDs of Messages joined with "," as the separator. */
+  static String joinMessageUids(Message[] messages) {
+    StringBuilder sb = new StringBuilder();
+    boolean notFirst = false;
+    for (Message m : messages) {
+      if (notFirst) {
+        sb.append(',');
+      }
+      sb.append(m.getUid());
+      notFirst = true;
+    }
+    return sb.toString();
+  }
+
+  static class ImapMessage extends MimeMessage {
+    private ImapFolder mFolder;
+
+    ImapMessage(String uid, ImapFolder folder) {
+      mUid = uid;
+      mFolder = folder;
+    }
+
+    public void setSize(int size) {
+      mSize = size;
+    }
+
+    @Override
+    public void parse(InputStream in) throws IOException, MessagingException {
+      super.parse(in);
+    }
+
+    public void setFlagInternal(String flag, boolean set) throws MessagingException {
+      super.setFlag(flag, set);
+    }
+
+    @Override
+    public void setFlag(String flag, boolean set) throws MessagingException {
+      super.setFlag(flag, set);
+      mFolder.setFlags(new Message[] {this}, new String[] {flag}, set);
+    }
+  }
+
+  static class ImapException extends MessagingException {
+    private static final long serialVersionUID = 1L;
+
+    private final String mStatus;
+    private final String mStatusMessage;
+    private final String mAlertText;
+    private final String mResponseCode;
+
+    public ImapException(
+        String message,
+        String status,
+        String statusMessage,
+        String alertText,
+        String responseCode) {
+      super(message);
+      mStatus = status;
+      mStatusMessage = statusMessage;
+      mAlertText = alertText;
+      mResponseCode = responseCode;
+    }
+
+    public String getStatus() {
+      return mStatus;
+    }
+
+    public String getStatusMessage() {
+      return mStatusMessage;
+    }
+
+    public String getAlertText() {
+      return mAlertText;
+    }
+
+    public String getResponseCode() {
+      return mResponseCode;
+    }
+  }
+
+  public void closeConnection() {
+    if (mConnection != null) {
+      mConnection.close();
+      mConnection = null;
+    }
+  }
+
+  public ImapConnection getConnection() {
+    if (mConnection == null) {
+      mConnection = new ImapConnection(this);
+    }
+    return mConnection;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/DigestMd5Utils.java b/java/com/android/voicemail/impl/mail/store/imap/DigestMd5Utils.java
new file mode 100644
index 0000000..f156f67
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/DigestMd5Utils.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+import android.annotation.TargetApi;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArrayMap;
+import android.util.Base64;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.mail.MailTransport;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.store.ImapStore;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Map;
+
+@SuppressWarnings("AndroidApiChecker") // Map.getOrDefault() is java8
+@TargetApi(VERSION_CODES.O)
+public class DigestMd5Utils {
+
+  private static final String TAG = "DigestMd5Utils";
+
+  private static final String DIGEST_CHARSET = "CHARSET";
+  private static final String DIGEST_USERNAME = "username";
+  private static final String DIGEST_REALM = "realm";
+  private static final String DIGEST_NONCE = "nonce";
+  private static final String DIGEST_NC = "nc";
+  private static final String DIGEST_CNONCE = "cnonce";
+  private static final String DIGEST_URI = "digest-uri";
+  private static final String DIGEST_RESPONSE = "response";
+  private static final String DIGEST_QOP = "qop";
+
+  private static final String RESPONSE_AUTH_HEADER = "rspauth=";
+  private static final String HEX_CHARS = "0123456789abcdef";
+
+  /** Represents the set of data we need to generate the DIGEST-MD5 response. */
+  public static class Data {
+
+    private static final String CHARSET = "utf-8";
+
+    public String username;
+    public String password;
+    public String realm;
+    public String nonce;
+    public String nc;
+    public String cnonce;
+    public String digestUri;
+    public String qop;
+
+    @VisibleForTesting
+    Data() {
+      // Do nothing
+    }
+
+    public Data(ImapStore imapStore, MailTransport transport, Map<String, String> challenge) {
+      username = imapStore.getUsername();
+      password = imapStore.getPassword();
+      realm = challenge.getOrDefault(DIGEST_REALM, "");
+      nonce = challenge.get(DIGEST_NONCE);
+      cnonce = createCnonce();
+      nc = "00000001"; // Subsequent Authentication not supported, nounce count always 1.
+      qop = "auth"; // Other config not supported
+      digestUri = "imap/" + transport.getHost();
+    }
+
+    private static String createCnonce() {
+      SecureRandom generator = new SecureRandom();
+
+      // At least 64 bits of entropy is required
+      byte[] rawBytes = new byte[8];
+      generator.nextBytes(rawBytes);
+
+      return Base64.encodeToString(rawBytes, Base64.NO_WRAP);
+    }
+
+    /** Verify the response-auth returned by the server is correct. */
+    public void verifyResponseAuth(String response) throws MessagingException {
+      if (!response.startsWith(RESPONSE_AUTH_HEADER)) {
+        throw new MessagingException("response-auth expected");
+      }
+      if (!response
+          .substring(RESPONSE_AUTH_HEADER.length())
+          .equals(DigestMd5Utils.getResponse(this, true))) {
+        throw new MessagingException("invalid response-auth return from the server.");
+      }
+    }
+
+    public String createResponse() {
+      String response = getResponse(this, false);
+      ResponseBuilder builder = new ResponseBuilder();
+      builder
+          .append(DIGEST_CHARSET, CHARSET)
+          .appendQuoted(DIGEST_USERNAME, username)
+          .appendQuoted(DIGEST_REALM, realm)
+          .appendQuoted(DIGEST_NONCE, nonce)
+          .append(DIGEST_NC, nc)
+          .appendQuoted(DIGEST_CNONCE, cnonce)
+          .appendQuoted(DIGEST_URI, digestUri)
+          .append(DIGEST_RESPONSE, response)
+          .append(DIGEST_QOP, qop);
+      return builder.toString();
+    }
+
+    private static class ResponseBuilder {
+
+      private StringBuilder mBuilder = new StringBuilder();
+
+      public ResponseBuilder appendQuoted(String key, String value) {
+        if (mBuilder.length() != 0) {
+          mBuilder.append(",");
+        }
+        mBuilder.append(key).append("=\"").append(value).append("\"");
+        return this;
+      }
+
+      public ResponseBuilder append(String key, String value) {
+        if (mBuilder.length() != 0) {
+          mBuilder.append(",");
+        }
+        mBuilder.append(key).append("=").append(value);
+        return this;
+      }
+
+      @Override
+      public String toString() {
+        return mBuilder.toString();
+      }
+    }
+  }
+
+  /*
+     response-value  =
+         toHex( getKeyDigest ( toHex(getMd5(a1)),
+         { nonce-value, ":" nc-value, ":",
+           cnonce-value, ":", qop-value, ":", toHex(getMd5(a2)) }))
+  * @param isResponseAuth is the response the one the server is returning us. response-auth has
+  * different a2 format.
+  */
+  @VisibleForTesting
+  static String getResponse(Data data, boolean isResponseAuth) {
+    StringBuilder a1 = new StringBuilder();
+    a1.append(
+        new String(
+            getMd5(data.username + ":" + data.realm + ":" + data.password),
+            StandardCharsets.ISO_8859_1));
+    a1.append(":").append(data.nonce).append(":").append(data.cnonce);
+
+    StringBuilder a2 = new StringBuilder();
+    if (!isResponseAuth) {
+      a2.append("AUTHENTICATE");
+    }
+    a2.append(":").append(data.digestUri);
+
+    return toHex(
+        getKeyDigest(
+            toHex(getMd5(a1.toString())),
+            data.nonce
+                + ":"
+                + data.nc
+                + ":"
+                + data.cnonce
+                + ":"
+                + data.qop
+                + ":"
+                + toHex(getMd5(a2.toString()))));
+  }
+
+  /** Let getMd5(s) be the 16 octet MD5 hash [RFC 1321] of the octet string s. */
+  private static byte[] getMd5(String s) {
+    try {
+      MessageDigest digester = MessageDigest.getInstance("MD5");
+      digester.update(s.getBytes(StandardCharsets.ISO_8859_1));
+      return digester.digest();
+    } catch (NoSuchAlgorithmException e) {
+      throw new AssertionError(e);
+    }
+  }
+
+  /**
+   * Let getKeyDigest(k, s) be getMd5({k, ":", s}), i.e., the 16 octet hash of the string k, a colon
+   * and the string s.
+   */
+  private static byte[] getKeyDigest(String k, String s) {
+    StringBuilder builder = new StringBuilder(k).append(":").append(s);
+    return getMd5(builder.toString());
+  }
+
+  /**
+   * Let toHex(n) be the representation of the 16 octet MD5 hash n as a string of 32 hex digits
+   * (with alphabetic characters always in lower case, since MD5 is case sensitive).
+   */
+  private static String toHex(byte[] n) {
+    StringBuilder result = new StringBuilder();
+    for (byte b : n) {
+      int unsignedByte = b & 0xFF;
+      result
+          .append(HEX_CHARS.charAt(unsignedByte / 16))
+          .append(HEX_CHARS.charAt(unsignedByte % 16));
+    }
+    return result.toString();
+  }
+
+  public static Map<String, String> parseDigestMessage(String message) throws MessagingException {
+    Map<String, String> result = new DigestMessageParser(message).parse();
+    if (!result.containsKey(DIGEST_NONCE)) {
+      throw new MessagingException("nonce missing from server DIGEST-MD5 challenge");
+    }
+    return result;
+  }
+
+  /** Parse the key-value pair returned by the server. */
+  private static class DigestMessageParser {
+
+    private final String mMessage;
+    private int mPosition = 0;
+    private Map<String, String> mResult = new ArrayMap<>();
+
+    public DigestMessageParser(String message) {
+      mMessage = message;
+    }
+
+    @Nullable
+    public Map<String, String> parse() {
+      try {
+        while (mPosition < mMessage.length()) {
+          parsePair();
+          if (mPosition != mMessage.length()) {
+            expect(',');
+          }
+        }
+      } catch (IndexOutOfBoundsException e) {
+        VvmLog.e(TAG, e.toString());
+        return null;
+      }
+      return mResult;
+    }
+
+    private void parsePair() {
+      String key = parseKey();
+      expect('=');
+      String value = parseValue();
+      mResult.put(key, value);
+    }
+
+    private void expect(char c) {
+      if (pop() != c) {
+        throw new IllegalStateException("unexpected character " + mMessage.charAt(mPosition));
+      }
+    }
+
+    private char pop() {
+      char result = peek();
+      mPosition++;
+      return result;
+    }
+
+    private char peek() {
+      return mMessage.charAt(mPosition);
+    }
+
+    private void goToNext(char c) {
+      while (peek() != c) {
+        mPosition++;
+      }
+    }
+
+    private String parseKey() {
+      int start = mPosition;
+      goToNext('=');
+      return mMessage.substring(start, mPosition);
+    }
+
+    private String parseValue() {
+      if (peek() == '"') {
+        return parseQuotedValue();
+      } else {
+        return parseUnquotedValue();
+      }
+    }
+
+    private String parseQuotedValue() {
+      expect('"');
+      StringBuilder result = new StringBuilder();
+      while (true) {
+        char c = pop();
+        if (c == '\\') {
+          result.append(pop());
+        } else if (c == '"') {
+          break;
+        } else {
+          result.append(c);
+        }
+      }
+      return result.toString();
+    }
+
+    private String parseUnquotedValue() {
+      StringBuilder result = new StringBuilder();
+      while (true) {
+        char c = pop();
+        if (c == '\\') {
+          result.append(pop());
+        } else if (c == ',') {
+          mPosition--;
+          break;
+        } else {
+          result.append(c);
+        }
+
+        if (mPosition == mMessage.length()) {
+          break;
+        }
+      }
+      return result.toString();
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapConstants.java b/java/com/android/voicemail/impl/mail/store/imap/ImapConstants.java
new file mode 100644
index 0000000..88ec0ed
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapConstants.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.mail.store.ImapStore;
+import java.util.Locale;
+
+public final class ImapConstants {
+  private ImapConstants() {}
+
+  public static final String FETCH_FIELD_BODY_PEEK_BARE = "BODY.PEEK";
+  public static final String FETCH_FIELD_BODY_PEEK = FETCH_FIELD_BODY_PEEK_BARE + "[]";
+  public static final String FETCH_FIELD_BODY_PEEK_SANE =
+      String.format(Locale.US, "BODY.PEEK[]<0.%d>", ImapStore.FETCH_BODY_SANE_SUGGESTED_SIZE);
+  public static final String FETCH_FIELD_HEADERS =
+      "BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc message-id)]";
+
+  public static final String ALERT = "ALERT";
+  public static final String APPEND = "APPEND";
+  public static final String AUTHENTICATE = "AUTHENTICATE";
+  public static final String BAD = "BAD";
+  public static final String BADCHARSET = "BADCHARSET";
+  public static final String BODY = "BODY";
+  public static final String BODY_BRACKET_HEADER = "BODY[HEADER";
+  public static final String BODYSTRUCTURE = "BODYSTRUCTURE";
+  public static final String BYE = "BYE";
+  public static final String CAPABILITY = "CAPABILITY";
+  public static final String CHECK = "CHECK";
+  public static final String CLOSE = "CLOSE";
+  public static final String COPY = "COPY";
+  public static final String COPYUID = "COPYUID";
+  public static final String CREATE = "CREATE";
+  public static final String DELETE = "DELETE";
+  public static final String EXAMINE = "EXAMINE";
+  public static final String EXISTS = "EXISTS";
+  public static final String EXPUNGE = "EXPUNGE";
+  public static final String FETCH = "FETCH";
+  public static final String FLAG_ANSWERED = "\\ANSWERED";
+  public static final String FLAG_DELETED = "\\DELETED";
+  public static final String FLAG_FLAGGED = "\\FLAGGED";
+  public static final String FLAG_NO_SELECT = "\\NOSELECT";
+  public static final String FLAG_SEEN = "\\SEEN";
+  public static final String FLAGS = "FLAGS";
+  public static final String FLAGS_SILENT = "FLAGS.SILENT";
+  public static final String ID = "ID";
+  public static final String INBOX = "INBOX";
+  public static final String INTERNALDATE = "INTERNALDATE";
+  public static final String LIST = "LIST";
+  public static final String LOGIN = "LOGIN";
+  public static final String LOGOUT = "LOGOUT";
+  public static final String LSUB = "LSUB";
+  public static final String NAMESPACE = "NAMESPACE";
+  public static final String NO = "NO";
+  public static final String NOOP = "NOOP";
+  public static final String OK = "OK";
+  public static final String PARSE = "PARSE";
+  public static final String PERMANENTFLAGS = "PERMANENTFLAGS";
+  public static final String PREAUTH = "PREAUTH";
+  public static final String READ_ONLY = "READ-ONLY";
+  public static final String READ_WRITE = "READ-WRITE";
+  public static final String RENAME = "RENAME";
+  public static final String RFC822_SIZE = "RFC822.SIZE";
+  public static final String SEARCH = "SEARCH";
+  public static final String SELECT = "SELECT";
+  public static final String STARTTLS = "STARTTLS";
+  public static final String STATUS = "STATUS";
+  public static final String STORE = "STORE";
+  public static final String SUBSCRIBE = "SUBSCRIBE";
+  public static final String TEXT = "TEXT";
+  public static final String TRYCREATE = "TRYCREATE";
+  public static final String UID = "UID";
+  public static final String UID_COPY = "UID COPY";
+  public static final String UID_FETCH = "UID FETCH";
+  public static final String UID_SEARCH = "UID SEARCH";
+  public static final String UID_STORE = "UID STORE";
+  public static final String UIDNEXT = "UIDNEXT";
+  public static final String UIDPLUS = "UIDPLUS";
+  public static final String UIDVALIDITY = "UIDVALIDITY";
+  public static final String UNSEEN = "UNSEEN";
+  public static final String UNSUBSCRIBE = "UNSUBSCRIBE";
+  public static final String XOAUTH2 = "XOAUTH2";
+  public static final String APPENDUID = "APPENDUID";
+  public static final String NIL = "NIL";
+
+  /** NO responses */
+  public static final String NO_COMMAND_NOT_ALLOWED = "command not allowed";
+
+  public static final String NO_RESERVATION_FAILED = "reservation failed";
+  public static final String NO_APPLICATION_ERROR = "application error";
+  public static final String NO_INVALID_PARAMETER = "invalid parameter";
+  public static final String NO_INVALID_COMMAND = "invalid command";
+  public static final String NO_UNKNOWN_COMMAND = "unknown command";
+  // AUTHENTICATE
+  // The subscriber can not be located in the system.
+  public static final String NO_UNKNOWN_USER = "unknown user";
+  // The Client Type or Protocol Version is unknown.
+  public static final String NO_UNKNOWN_CLIENT = "unknown client";
+  // The password received from the client does not match the password defined in the subscriber's
+  // profile.
+  public static final String NO_INVALID_PASSWORD = "invalid password";
+  // The subscriber's mailbox has not yet been initialised via the TUI
+  public static final String NO_MAILBOX_NOT_INITIALIZED = "mailbox not initialized";
+  // The subscriber has not been provisioned for the VVM service.
+  public static final String NO_SERVICE_IS_NOT_PROVISIONED = "service is not provisioned";
+  // The subscriber is provisioned for the VVM service but the VVM service is currently not active
+  public static final String NO_SERVICE_IS_NOT_ACTIVATED = "service is not activated";
+  // The Voice Mail Blocked flag in the subscriber's profile is set to YES.
+  public static final String NO_USER_IS_BLOCKED = "user is blocked";
+
+  /** extensions */
+  public static final String GETQUOTA = "GETQUOTA";
+
+  public static final String GETQUOTAROOT = "GETQUOTAROOT";
+  public static final String QUOTAROOT = "QUOTAROOT";
+  public static final String QUOTA = "QUOTA";
+
+  /** capabilities */
+  public static final String CAPABILITY_AUTH_DIGEST_MD5 = "AUTH=DIGEST-MD5";
+
+  public static final String CAPABILITY_STARTTLS = "STARTTLS";
+
+  /** authentication */
+  public static final String AUTH_DIGEST_MD5 = "DIGEST-MD5";
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapElement.java b/java/com/android/voicemail/impl/mail/store/imap/ImapElement.java
new file mode 100644
index 0000000..ee255d1
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapElement.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+/**
+ * Class representing "element"s in IMAP responses.
+ *
+ * <p>Class hierarchy:
+ *
+ * <pre>
+ * ImapElement
+ *   |
+ *   |-- ImapElement.NONE (for 'index out of range')
+ *   |
+ *   |-- ImapList (isList() == true)
+ *   |   |
+ *   |   |-- ImapList.EMPTY
+ *   |   |
+ *   |   --- ImapResponse
+ *   |
+ *   --- ImapString (isString() == true)
+ *       |
+ *       |-- ImapString.EMPTY
+ *       |
+ *       |-- ImapSimpleString
+ *       |
+ *       |-- ImapMemoryLiteral
+ *       |
+ *       --- ImapTempFileLiteral
+ * </pre>
+ */
+public abstract class ImapElement {
+  /**
+   * An element that is returned by {@link ImapList#getElementOrNone} to indicate an index is out of
+   * range.
+   */
+  public static final ImapElement NONE =
+      new ImapElement() {
+        @Override
+        public void destroy() {
+          // Don't call super.destroy().
+          // It's a shared object.  We don't want the mDestroyed to be set on this.
+        }
+
+        @Override
+        public boolean isList() {
+          return false;
+        }
+
+        @Override
+        public boolean isString() {
+          return false;
+        }
+
+        @Override
+        public String toString() {
+          return "[NO ELEMENT]";
+        }
+
+        @Override
+        public boolean equalsForTest(ImapElement that) {
+          return super.equalsForTest(that);
+        }
+      };
+
+  private boolean mDestroyed = false;
+
+  public abstract boolean isList();
+
+  public abstract boolean isString();
+
+  protected boolean isDestroyed() {
+    return mDestroyed;
+  }
+
+  /**
+   * Clean up the resources used by the instance. It's for removing a temp file used by {@link
+   * ImapTempFileLiteral}.
+   */
+  public void destroy() {
+    mDestroyed = true;
+  }
+
+  /** Throws {@link RuntimeException} if it's already destroyed. */
+  protected final void checkNotDestroyed() {
+    if (mDestroyed) {
+      throw new RuntimeException("Already destroyed");
+    }
+  }
+
+  /**
+   * Return a string that represents this object; it's purely for the debug purpose. Don't mistake
+   * it for {@link ImapString#getString}.
+   *
+   * <p>Abstract to force subclasses to implement it.
+   */
+  @Override
+  public abstract String toString();
+
+  /**
+   * The equals implementation that is intended to be used only for unit testing. (Because it may be
+   * heavy and has a special sense of "equal" for testing.)
+   */
+  public boolean equalsForTest(ImapElement that) {
+    if (that == null) {
+      return false;
+    }
+    return this.getClass() == that.getClass(); // Has to be the same class.
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapList.java b/java/com/android/voicemail/impl/mail/store/imap/ImapList.java
new file mode 100644
index 0000000..e4a6ec0
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapList.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+import java.util.ArrayList;
+
+/** Class represents an IMAP list. */
+public class ImapList extends ImapElement {
+  /** {@link ImapList} representing an empty list. */
+  public static final ImapList EMPTY =
+      new ImapList() {
+        @Override
+        public void destroy() {
+          // Don't call super.destroy().
+          // It's a shared object.  We don't want the mDestroyed to be set on this.
+        }
+
+        @Override
+        void add(ImapElement e) {
+          throw new RuntimeException();
+        }
+      };
+
+  private ArrayList<ImapElement> mList = new ArrayList<ImapElement>();
+
+  /* package */ void add(ImapElement e) {
+    if (e == null) {
+      throw new RuntimeException("Can't add null");
+    }
+    mList.add(e);
+  }
+
+  @Override
+  public final boolean isString() {
+    return false;
+  }
+
+  @Override
+  public final boolean isList() {
+    return true;
+  }
+
+  public final int size() {
+    return mList.size();
+  }
+
+  public final boolean isEmpty() {
+    return size() == 0;
+  }
+
+  /**
+   * Return true if the element at {@code index} exists, is string, and equals to {@code s}. (case
+   * insensitive)
+   */
+  public final boolean is(int index, String s) {
+    return is(index, s, false);
+  }
+
+  /** Same as {@link #is(int, String)}, but does the prefix match if {@code prefixMatch}. */
+  public final boolean is(int index, String s, boolean prefixMatch) {
+    if (!prefixMatch) {
+      return getStringOrEmpty(index).is(s);
+    } else {
+      return getStringOrEmpty(index).startsWith(s);
+    }
+  }
+
+  /**
+   * Return the element at {@code index}. If {@code index} is out of range, returns {@link
+   * ImapElement#NONE}.
+   */
+  public final ImapElement getElementOrNone(int index) {
+    return (index >= mList.size()) ? ImapElement.NONE : mList.get(index);
+  }
+
+  /**
+   * Return the element at {@code index} if it's a list. If {@code index} is out of range or not a
+   * list, returns {@link ImapList#EMPTY}.
+   */
+  public final ImapList getListOrEmpty(int index) {
+    ImapElement el = getElementOrNone(index);
+    return el.isList() ? (ImapList) el : EMPTY;
+  }
+
+  /**
+   * Return the element at {@code index} if it's a string. If {@code index} is out of range or not a
+   * string, returns {@link ImapString#EMPTY}.
+   */
+  public final ImapString getStringOrEmpty(int index) {
+    ImapElement el = getElementOrNone(index);
+    return el.isString() ? (ImapString) el : ImapString.EMPTY;
+  }
+
+  /**
+   * Return an element keyed by {@code key}. Return null if not found. {@code key} has to be at an
+   * even index.
+   */
+  /* package */ final ImapElement getKeyedElementOrNull(String key, boolean prefixMatch) {
+    for (int i = 1; i < size(); i += 2) {
+      if (is(i - 1, key, prefixMatch)) {
+        return mList.get(i);
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Return an {@link ImapList} keyed by {@code key}. Return {@link ImapList#EMPTY} if not found.
+   */
+  public final ImapList getKeyedListOrEmpty(String key) {
+    return getKeyedListOrEmpty(key, false);
+  }
+
+  /**
+   * Return an {@link ImapList} keyed by {@code key}. Return {@link ImapList#EMPTY} if not found.
+   */
+  public final ImapList getKeyedListOrEmpty(String key, boolean prefixMatch) {
+    ImapElement e = getKeyedElementOrNull(key, prefixMatch);
+    return (e != null) ? ((ImapList) e) : ImapList.EMPTY;
+  }
+
+  /**
+   * Return an {@link ImapString} keyed by {@code key}. Return {@link ImapString#EMPTY} if not
+   * found.
+   */
+  public final ImapString getKeyedStringOrEmpty(String key) {
+    return getKeyedStringOrEmpty(key, false);
+  }
+
+  /**
+   * Return an {@link ImapString} keyed by {@code key}. Return {@link ImapString#EMPTY} if not
+   * found.
+   */
+  public final ImapString getKeyedStringOrEmpty(String key, boolean prefixMatch) {
+    ImapElement e = getKeyedElementOrNull(key, prefixMatch);
+    return (e != null) ? ((ImapString) e) : ImapString.EMPTY;
+  }
+
+  /** Return true if it contains {@code s}. */
+  public final boolean contains(String s) {
+    for (int i = 0; i < size(); i++) {
+      if (getStringOrEmpty(i).is(s)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public void destroy() {
+    if (mList != null) {
+      for (ImapElement e : mList) {
+        e.destroy();
+      }
+      mList = null;
+    }
+    super.destroy();
+  }
+
+  @Override
+  public String toString() {
+    return mList.toString();
+  }
+
+  /** Return the text representations of the contents concatenated with ",". */
+  public final String flatten() {
+    return flatten(new StringBuilder()).toString();
+  }
+
+  /**
+   * Returns text representations (i.e. getString()) of contents joined together with "," as the
+   * separator.
+   *
+   * <p>Only used for building the capability string passed to vendor policies.
+   *
+   * <p>We can't use toString(), because it's for debugging (meaning the format may change any
+   * time), and it won't expand literals.
+   */
+  private final StringBuilder flatten(StringBuilder sb) {
+    sb.append('[');
+    for (int i = 0; i < mList.size(); i++) {
+      if (i > 0) {
+        sb.append(',');
+      }
+      final ImapElement e = getElementOrNone(i);
+      if (e.isList()) {
+        getListOrEmpty(i).flatten(sb);
+      } else if (e.isString()) {
+        sb.append(getStringOrEmpty(i).getString());
+      }
+    }
+    sb.append(']');
+    return sb;
+  }
+
+  @Override
+  public boolean equalsForTest(ImapElement that) {
+    if (!super.equalsForTest(that)) {
+      return false;
+    }
+    ImapList thatList = (ImapList) that;
+    if (size() != thatList.size()) {
+      return false;
+    }
+    for (int i = 0; i < size(); i++) {
+      if (!mList.get(i).equalsForTest(thatList.getElementOrNone(i))) {
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapMemoryLiteral.java b/java/com/android/voicemail/impl/mail/store/imap/ImapMemoryLiteral.java
new file mode 100644
index 0000000..96a8c4a
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapMemoryLiteral.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.mail.FixedLengthInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+
+/** Subclass of {@link ImapString} used for literals backed by an in-memory byte array. */
+public class ImapMemoryLiteral extends ImapString {
+  private final String TAG = "ImapMemoryLiteral";
+  private byte[] mData;
+
+  /* package */ ImapMemoryLiteral(FixedLengthInputStream in) throws IOException {
+    // We could use ByteArrayOutputStream and IOUtils.copy, but it'd perform an unnecessary
+    // copy....
+    mData = new byte[in.getLength()];
+    int pos = 0;
+    while (pos < mData.length) {
+      int read = in.read(mData, pos, mData.length - pos);
+      if (read < 0) {
+        break;
+      }
+      pos += read;
+    }
+    if (pos != mData.length) {
+      VvmLog.w(TAG, "length mismatch");
+    }
+  }
+
+  @Override
+  public void destroy() {
+    mData = null;
+    super.destroy();
+  }
+
+  @Override
+  public String getString() {
+    try {
+      return new String(mData, "US-ASCII");
+    } catch (UnsupportedEncodingException e) {
+      VvmLog.e(TAG, "Unsupported encoding: ", e);
+    }
+    return null;
+  }
+
+  @Override
+  public InputStream getAsStream() {
+    return new ByteArrayInputStream(mData);
+  }
+
+  @Override
+  public String toString() {
+    return String.format("{%d byte literal(memory)}", mData.length);
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapResponse.java b/java/com/android/voicemail/impl/mail/store/imap/ImapResponse.java
new file mode 100644
index 0000000..d53d458
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapResponse.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+/** Class represents an IMAP response. */
+public class ImapResponse extends ImapList {
+  private final String mTag;
+  private final boolean mIsContinuationRequest;
+
+  /* package */ ImapResponse(String tag, boolean isContinuationRequest) {
+    mTag = tag;
+    mIsContinuationRequest = isContinuationRequest;
+  }
+
+  /* package */ static boolean isStatusResponse(String symbol) {
+    return ImapConstants.OK.equalsIgnoreCase(symbol)
+        || ImapConstants.NO.equalsIgnoreCase(symbol)
+        || ImapConstants.BAD.equalsIgnoreCase(symbol)
+        || ImapConstants.PREAUTH.equalsIgnoreCase(symbol)
+        || ImapConstants.BYE.equalsIgnoreCase(symbol);
+  }
+
+  /** @return whether it's a tagged response. */
+  public boolean isTagged() {
+    return mTag != null;
+  }
+
+  /** @return whether it's a continuation request. */
+  public boolean isContinuationRequest() {
+    return mIsContinuationRequest;
+  }
+
+  public boolean isStatusResponse() {
+    return isStatusResponse(getStringOrEmpty(0).getString());
+  }
+
+  /** @return whether it's an OK response. */
+  public boolean isOk() {
+    return is(0, ImapConstants.OK);
+  }
+
+  /** @return whether it's an BAD response. */
+  public boolean isBad() {
+    return is(0, ImapConstants.BAD);
+  }
+
+  /** @return whether it's an NO response. */
+  public boolean isNo() {
+    return is(0, ImapConstants.NO);
+  }
+
+  /**
+   * @return whether it's an {@code responseType} data response. (i.e. not tagged).
+   * @param index where {@code responseType} should appear. e.g. 1 for "FETCH"
+   * @param responseType e.g. "FETCH"
+   */
+  public final boolean isDataResponse(int index, String responseType) {
+    return !isTagged() && getStringOrEmpty(index).is(responseType);
+  }
+
+  /**
+   * @return Response code (RFC 3501 7.1) if it's a status response.
+   *     <p>e.g. "ALERT" for "* OK [ALERT] System shutdown in 10 minutes"
+   */
+  public ImapString getResponseCodeOrEmpty() {
+    if (!isStatusResponse()) {
+      return ImapString.EMPTY; // Not a status response.
+    }
+    return getListOrEmpty(1).getStringOrEmpty(0);
+  }
+
+  /**
+   * @return Alert message it it has ALERT response code.
+   *     <p>e.g. "System shutdown in 10 minutes" for "* OK [ALERT] System shutdown in 10 minutes"
+   */
+  public ImapString getAlertTextOrEmpty() {
+    if (!getResponseCodeOrEmpty().is(ImapConstants.ALERT)) {
+      return ImapString.EMPTY; // Not an ALERT
+    }
+    // The 3rd element contains all the rest of line.
+    return getStringOrEmpty(2);
+  }
+
+  /** @return Response text in a status response. */
+  public ImapString getStatusResponseTextOrEmpty() {
+    if (!isStatusResponse()) {
+      return ImapString.EMPTY;
+    }
+    return getStringOrEmpty(getElementOrNone(1).isList() ? 2 : 1);
+  }
+
+  public ImapString getStatusOrEmpty() {
+    if (!isStatusResponse()) {
+      return ImapString.EMPTY;
+    }
+    return getStringOrEmpty(0);
+  }
+
+  @Override
+  public String toString() {
+    String tag = mTag;
+    if (isContinuationRequest()) {
+      tag = "+";
+    }
+    return "#" + tag + "# " + super.toString();
+  }
+
+  @Override
+  public boolean equalsForTest(ImapElement that) {
+    if (!super.equalsForTest(that)) {
+      return false;
+    }
+    final ImapResponse thatResponse = (ImapResponse) that;
+    if (mTag == null) {
+      if (thatResponse.mTag != null) {
+        return false;
+      }
+    } else {
+      if (!mTag.equals(thatResponse.mTag)) {
+        return false;
+      }
+    }
+    if (mIsContinuationRequest != thatResponse.mIsContinuationRequest) {
+      return false;
+    }
+    return true;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapResponseParser.java b/java/com/android/voicemail/impl/mail/store/imap/ImapResponseParser.java
new file mode 100644
index 0000000..e37106a
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapResponseParser.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+import android.text.TextUtils;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.mail.FixedLengthInputStream;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.PeekableInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/** IMAP response parser. */
+public class ImapResponseParser {
+  private static final String TAG = "ImapResponseParser";
+
+  /** Literal larger than this will be stored in temp file. */
+  public static final int LITERAL_KEEP_IN_MEMORY_THRESHOLD = 2 * 1024 * 1024;
+
+  /** Input stream */
+  private final PeekableInputStream mIn;
+
+  private final int mLiteralKeepInMemoryThreshold;
+
+  /** StringBuilder used by readUntil() */
+  private final StringBuilder mBufferReadUntil = new StringBuilder();
+
+  /** StringBuilder used by parseBareString() */
+  private final StringBuilder mParseBareString = new StringBuilder();
+
+  /**
+   * We store all {@link ImapResponse} in it. {@link #destroyResponses()} must be called from time
+   * to time to destroy them and clear it.
+   */
+  private final ArrayList<ImapResponse> mResponsesToDestroy = new ArrayList<ImapResponse>();
+
+  /**
+   * Exception thrown when we receive BYE. It derives from IOException, so it'll be treated in the
+   * same way EOF does.
+   */
+  public static class ByeException extends IOException {
+    public static final String MESSAGE = "Received BYE";
+
+    public ByeException() {
+      super(MESSAGE);
+    }
+  }
+
+  /** Public constructor for normal use. */
+  public ImapResponseParser(InputStream in) {
+    this(in, LITERAL_KEEP_IN_MEMORY_THRESHOLD);
+  }
+
+  /** Constructor for testing to override the literal size threshold. */
+  /* package for test */ ImapResponseParser(InputStream in, int literalKeepInMemoryThreshold) {
+    mIn = new PeekableInputStream(in);
+    mLiteralKeepInMemoryThreshold = literalKeepInMemoryThreshold;
+  }
+
+  private static IOException newEOSException() {
+    final String message = "End of stream reached";
+    VvmLog.d(TAG, message);
+    return new IOException(message);
+  }
+
+  /**
+   * Peek next one byte.
+   *
+   * <p>Throws IOException() if reaches EOF. As long as logical response lines end with \r\n, we
+   * shouldn't see EOF during parsing.
+   */
+  private int peek() throws IOException {
+    final int next = mIn.peek();
+    if (next == -1) {
+      throw newEOSException();
+    }
+    return next;
+  }
+
+  /**
+   * Read and return one byte from {@link #mIn}, and put it in {@link #mDiscourseLogger}.
+   *
+   * <p>Throws IOException() if reaches EOF. As long as logical response lines end with \r\n, we
+   * shouldn't see EOF during parsing.
+   */
+  private int readByte() throws IOException {
+    int next = mIn.read();
+    if (next == -1) {
+      throw newEOSException();
+    }
+    return next;
+  }
+
+  /**
+   * Destroy all the {@link ImapResponse}s stored in the internal storage and clear it.
+   *
+   * @see #readResponse()
+   */
+  public void destroyResponses() {
+    for (ImapResponse r : mResponsesToDestroy) {
+      r.destroy();
+    }
+    mResponsesToDestroy.clear();
+  }
+
+  /**
+   * Reads the next response available on the stream and returns an {@link ImapResponse} object that
+   * represents it.
+   *
+   * <p>When this method successfully returns an {@link ImapResponse}, the {@link ImapResponse} is
+   * stored in the internal storage. When the {@link ImapResponse} is no longer used {@link
+   * #destroyResponses} should be called to destroy all the responses in the array.
+   *
+   * @param byeExpected is a untagged BYE response expected? If not proper cleanup will be done and
+   *     {@link ByeException} will be thrown.
+   * @return the parsed {@link ImapResponse} object.
+   * @exception ByeException when detects BYE and <code>byeExpected</code> is false.
+   */
+  public ImapResponse readResponse(boolean byeExpected) throws IOException, MessagingException {
+    ImapResponse response = null;
+    try {
+      response = parseResponse();
+    } catch (RuntimeException e) {
+      // Parser crash -- log network activities.
+      onParseError(e);
+      throw e;
+    } catch (IOException e) {
+      // Network error, or received an unexpected char.
+      onParseError(e);
+      throw e;
+    }
+
+    // Handle this outside of try-catch.  We don't have to dump protocol log when getting BYE.
+    if (!byeExpected && response.is(0, ImapConstants.BYE)) {
+      VvmLog.w(TAG, ByeException.MESSAGE);
+      response.destroy();
+      throw new ByeException();
+    }
+    mResponsesToDestroy.add(response);
+    return response;
+  }
+
+  private void onParseError(Exception e) {
+    // Read a few more bytes, so that the log will contain some more context, even if the parser
+    // crashes in the middle of a response.
+    // This also makes sure the byte in question will be logged, no matter where it crashes.
+    // e.g. when parseAtom() peeks and finds at an unexpected char, it throws an exception
+    // before actually reading it.
+    // However, we don't want to read too much, because then it may get into an email message.
+    try {
+      for (int i = 0; i < 4; i++) {
+        int b = readByte();
+        if (b == -1 || b == '\n') {
+          break;
+        }
+      }
+    } catch (IOException ignore) {
+    }
+    VvmLog.w(TAG, "Exception detected: " + e.getMessage());
+  }
+
+  /**
+   * Read next byte from stream and throw it away. If the byte is different from {@code expected}
+   * throw {@link MessagingException}.
+   */
+  /* package for test */ void expect(char expected) throws IOException {
+    final int next = readByte();
+    if (expected != next) {
+      throw new IOException(
+          String.format(
+              "Expected %04x (%c) but got %04x (%c)", (int) expected, expected, next, (char) next));
+    }
+  }
+
+  /**
+   * Read bytes until we find {@code end}, and return all as string. The {@code end} will be read
+   * (rather than peeked) and won't be included in the result.
+   */
+  /* package for test */ String readUntil(char end) throws IOException {
+    mBufferReadUntil.setLength(0);
+    for (; ; ) {
+      final int ch = readByte();
+      if (ch != end) {
+        mBufferReadUntil.append((char) ch);
+      } else {
+        return mBufferReadUntil.toString();
+      }
+    }
+  }
+
+  /** Read all bytes until \r\n. */
+  /* package */ String readUntilEol() throws IOException {
+    String ret = readUntil('\r');
+    expect('\n'); // TODO Should this really be error?
+    return ret;
+  }
+
+  /** Parse and return the response line. */
+  private ImapResponse parseResponse() throws IOException, MessagingException {
+    // We need to destroy the response if we get an exception.
+    // So, we first store the response that's being built in responseToDestroy, until it's
+    // completely built, at which point we copy it into responseToReturn and null out
+    // responseToDestroyt.
+    // If responseToDestroy is not null in finally, we destroy it because that means
+    // we got an exception somewhere.
+    ImapResponse responseToDestroy = null;
+    final ImapResponse responseToReturn;
+
+    try {
+      final int ch = peek();
+      if (ch == '+') { // Continuation request
+        readByte(); // skip +
+        expect(' ');
+        responseToDestroy = new ImapResponse(null, true);
+
+        // If it's continuation request, we don't really care what's in it.
+        responseToDestroy.add(new ImapSimpleString(readUntilEol()));
+
+        // Response has successfully been built.  Let's return it.
+        responseToReturn = responseToDestroy;
+        responseToDestroy = null;
+      } else {
+        // Status response or response data
+        final String tag;
+        if (ch == '*') {
+          tag = null;
+          readByte(); // skip *
+          expect(' ');
+        } else {
+          tag = readUntil(' ');
+        }
+        responseToDestroy = new ImapResponse(tag, false);
+
+        final ImapString firstString = parseBareString();
+        responseToDestroy.add(firstString);
+
+        // parseBareString won't eat a space after the string, so we need to skip it,
+        // if exists.
+        // If the next char is not ' ', it should be EOL.
+        if (peek() == ' ') {
+          readByte(); // skip ' '
+
+          if (responseToDestroy.isStatusResponse()) { // It's a status response
+
+            // Is there a response code?
+            final int next = peek();
+            if (next == '[') {
+              responseToDestroy.add(parseList('[', ']'));
+              if (peek() == ' ') { // Skip following space
+                readByte();
+              }
+            }
+
+            String rest = readUntilEol();
+            if (!TextUtils.isEmpty(rest)) {
+              // The rest is free-form text.
+              responseToDestroy.add(new ImapSimpleString(rest));
+            }
+          } else { // It's a response data.
+            parseElements(responseToDestroy, '\0');
+          }
+        } else {
+          expect('\r');
+          expect('\n');
+        }
+
+        // Response has successfully been built.  Let's return it.
+        responseToReturn = responseToDestroy;
+        responseToDestroy = null;
+      }
+    } finally {
+      if (responseToDestroy != null) {
+        // We get an exception.
+        responseToDestroy.destroy();
+      }
+    }
+
+    return responseToReturn;
+  }
+
+  private ImapElement parseElement() throws IOException, MessagingException {
+    final int next = peek();
+    switch (next) {
+      case '(':
+        return parseList('(', ')');
+      case '[':
+        return parseList('[', ']');
+      case '"':
+        readByte(); // Skip "
+        return new ImapSimpleString(readUntil('"'));
+      case '{':
+        return parseLiteral();
+      case '\r': // CR
+        readByte(); // Consume \r
+        expect('\n'); // Should be followed by LF.
+        return null;
+      case '\n': // LF // There shouldn't be a bare LF, but just in case.
+        readByte(); // Consume \n
+        return null;
+      default:
+        return parseBareString();
+    }
+  }
+
+  /**
+   * Parses an atom.
+   *
+   * <p>Special case: If an atom contains '[', everything until the next ']' will be considered a
+   * part of the atom. (e.g. "BODY[HEADER.FIELDS ("DATE" ...)]" will become a single ImapString)
+   *
+   * <p>If the value is "NIL", returns an empty string.
+   */
+  private ImapString parseBareString() throws IOException, MessagingException {
+    mParseBareString.setLength(0);
+    for (; ; ) {
+      final int ch = peek();
+
+      // TODO Can we clean this up?  (This condition is from the old parser.)
+      if (ch == '('
+          || ch == ')'
+          || ch == '{'
+          || ch == ' '
+          ||
+          // ']' is not part of atom (it's in resp-specials)
+          ch == ']'
+          ||
+          // docs claim that flags are \ atom but atom isn't supposed to
+          // contain
+          // * and some flags contain *
+          // ch == '%' || ch == '*' ||
+          ch == '%'
+          ||
+          // TODO probably should not allow \ and should recognize
+          // it as a flag instead
+          // ch == '"' || ch == '\' ||
+          ch == '"'
+          || (0x00 <= ch && ch <= 0x1f)
+          || ch == 0x7f) {
+        if (mParseBareString.length() == 0) {
+          throw new MessagingException("Expected string, none found.");
+        }
+        String s = mParseBareString.toString();
+
+        // NIL will be always converted into the empty string.
+        if (ImapConstants.NIL.equalsIgnoreCase(s)) {
+          return ImapString.EMPTY;
+        }
+        return new ImapSimpleString(s);
+      } else if (ch == '[') {
+        // Eat all until next ']'
+        mParseBareString.append((char) readByte());
+        mParseBareString.append(readUntil(']'));
+        mParseBareString.append(']'); // readUntil won't include the end char.
+      } else {
+        mParseBareString.append((char) readByte());
+      }
+    }
+  }
+
+  private void parseElements(ImapList list, char end) throws IOException, MessagingException {
+    for (; ; ) {
+      for (; ; ) {
+        final int next = peek();
+        if (next == end) {
+          return;
+        }
+        if (next != ' ') {
+          break;
+        }
+        // Skip space
+        readByte();
+      }
+      final ImapElement el = parseElement();
+      if (el == null) { // EOL
+        return;
+      }
+      list.add(el);
+    }
+  }
+
+  private ImapList parseList(char opening, char closing) throws IOException, MessagingException {
+    expect(opening);
+    final ImapList list = new ImapList();
+    parseElements(list, closing);
+    expect(closing);
+    return list;
+  }
+
+  private ImapString parseLiteral() throws IOException, MessagingException {
+    expect('{');
+    final int size;
+    try {
+      size = Integer.parseInt(readUntil('}'));
+    } catch (NumberFormatException nfe) {
+      throw new MessagingException("Invalid length in literal");
+    }
+    if (size < 0) {
+      throw new MessagingException("Invalid negative length in literal");
+    }
+    expect('\r');
+    expect('\n');
+    FixedLengthInputStream in = new FixedLengthInputStream(mIn, size);
+    if (size > mLiteralKeepInMemoryThreshold) {
+      return new ImapTempFileLiteral(in);
+    } else {
+      return new ImapMemoryLiteral(in);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapSimpleString.java b/java/com/android/voicemail/impl/mail/store/imap/ImapSimpleString.java
new file mode 100644
index 0000000..7cc866b
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapSimpleString.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.VvmLog;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+
+/** Subclass of {@link ImapString} used for non literals. */
+public class ImapSimpleString extends ImapString {
+  private final String TAG = "ImapSimpleString";
+  private String mString;
+
+  /* package */ ImapSimpleString(String string) {
+    mString = (string != null) ? string : "";
+  }
+
+  @Override
+  public void destroy() {
+    mString = null;
+    super.destroy();
+  }
+
+  @Override
+  public String getString() {
+    return mString;
+  }
+
+  @Override
+  public InputStream getAsStream() {
+    try {
+      return new ByteArrayInputStream(mString.getBytes("US-ASCII"));
+    } catch (UnsupportedEncodingException e) {
+      VvmLog.e(TAG, "Unsupported encoding: ", e);
+    }
+    return null;
+  }
+
+  @Override
+  public String toString() {
+    // Purposefully not return just mString, in order to prevent using it instead of getString.
+    return "\"" + mString + "\"";
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapString.java b/java/com/android/voicemail/impl/mail/store/imap/ImapString.java
new file mode 100644
index 0000000..d5c5551
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapString.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.VvmLog;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Class represents an IMAP "element" that is not a list.
+ *
+ * <p>An atom, quoted string, literal, are all represented by this. Values like OK, STATUS are too.
+ * Also, this class class may contain more arbitrary value like "BODY[HEADER.FIELDS ("DATE")]". See
+ * {@link ImapResponseParser}.
+ */
+public abstract class ImapString extends ImapElement {
+  private static final byte[] EMPTY_BYTES = new byte[0];
+
+  public static final ImapString EMPTY =
+      new ImapString() {
+        @Override
+        public void destroy() {
+          // Don't call super.destroy().
+          // It's a shared object.  We don't want the mDestroyed to be set on this.
+        }
+
+        @Override
+        public String getString() {
+          return "";
+        }
+
+        @Override
+        public InputStream getAsStream() {
+          return new ByteArrayInputStream(EMPTY_BYTES);
+        }
+
+        @Override
+        public String toString() {
+          return "";
+        }
+      };
+
+  // This is used only for parsing IMAP's FETCH ENVELOPE command, in which
+  // en_US-like date format is used like "01-Jan-2009 11:20:39 -0800", so this should be
+  // handled by Locale.US
+  private static final SimpleDateFormat DATE_TIME_FORMAT =
+      new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.US);
+
+  private boolean mIsInteger;
+  private int mParsedInteger;
+  private Date mParsedDate;
+
+  @Override
+  public final boolean isList() {
+    return false;
+  }
+
+  @Override
+  public final boolean isString() {
+    return true;
+  }
+
+  /**
+   * @return true if and only if the length of the string is larger than 0.
+   *     <p>Note: IMAP NIL is considered an empty string. See {@link ImapResponseParser
+   *     #parseBareString}. On the other hand, a quoted/literal string with value NIL (i.e. "NIL"
+   *     and {3}\r\nNIL) is treated literally.
+   */
+  public final boolean isEmpty() {
+    return getString().length() == 0;
+  }
+
+  public abstract String getString();
+
+  public abstract InputStream getAsStream();
+
+  /** @return whether it can be parsed as a number. */
+  public final boolean isNumber() {
+    if (mIsInteger) {
+      return true;
+    }
+    try {
+      mParsedInteger = Integer.parseInt(getString());
+      mIsInteger = true;
+      return true;
+    } catch (NumberFormatException e) {
+      return false;
+    }
+  }
+
+  /** @return value parsed as a number, or 0 if the string is not a number. */
+  public final int getNumberOrZero() {
+    return getNumber(0);
+  }
+
+  /** @return value parsed as a number, or {@code defaultValue} if the string is not a number. */
+  public final int getNumber(int defaultValue) {
+    if (!isNumber()) {
+      return defaultValue;
+    }
+    return mParsedInteger;
+  }
+
+  /** @return whether it can be parsed as a date using {@link #DATE_TIME_FORMAT}. */
+  public final boolean isDate() {
+    if (mParsedDate != null) {
+      return true;
+    }
+    if (isEmpty()) {
+      return false;
+    }
+    try {
+      mParsedDate = DATE_TIME_FORMAT.parse(getString());
+      return true;
+    } catch (ParseException e) {
+      VvmLog.w("ImapString", getString() + " can't be parsed as a date.");
+      return false;
+    }
+  }
+
+  /** @return value it can be parsed as a {@link Date}, or null otherwise. */
+  public final Date getDateOrNull() {
+    if (!isDate()) {
+      return null;
+    }
+    return mParsedDate;
+  }
+
+  /** @return whether the value case-insensitively equals to {@code s}. */
+  public final boolean is(String s) {
+    if (s == null) {
+      return false;
+    }
+    return getString().equalsIgnoreCase(s);
+  }
+
+  /** @return whether the value case-insensitively starts with {@code s}. */
+  public final boolean startsWith(String prefix) {
+    if (prefix == null) {
+      return false;
+    }
+    final String me = this.getString();
+    if (me.length() < prefix.length()) {
+      return false;
+    }
+    return me.substring(0, prefix.length()).equalsIgnoreCase(prefix);
+  }
+
+  // To force subclasses to implement it.
+  @Override
+  public abstract String toString();
+
+  @Override
+  public final boolean equalsForTest(ImapElement that) {
+    if (!super.equalsForTest(that)) {
+      return false;
+    }
+    ImapString thatString = (ImapString) that;
+    return getString().equals(thatString.getString());
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapTempFileLiteral.java b/java/com/android/voicemail/impl/mail/store/imap/ImapTempFileLiteral.java
new file mode 100644
index 0000000..ab64d85
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapTempFileLiteral.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.mail.FixedLengthInputStream;
+import com.android.voicemail.impl.mail.TempDirectory;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import com.android.voicemail.impl.mail.utils.Utility;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.commons.io.IOUtils;
+
+/** Subclass of {@link ImapString} used for literals backed by a temp file. */
+public class ImapTempFileLiteral extends ImapString {
+  private final String TAG = "ImapTempFileLiteral";
+
+  /* package for test */ final File mFile;
+
+  /** Size is purely for toString() */
+  private final int mSize;
+
+  /* package */ ImapTempFileLiteral(FixedLengthInputStream stream) throws IOException {
+    mSize = stream.getLength();
+    mFile = File.createTempFile("imap", ".tmp", TempDirectory.getTempDirectory());
+
+    // Unfortunately, we can't really use deleteOnExit(), because temp filenames are random
+    // so it'd simply cause a memory leak.
+    // deleteOnExit() simply adds filenames to a static list and the list will never shrink.
+    // mFile.deleteOnExit();
+    OutputStream out = new FileOutputStream(mFile);
+    IOUtils.copy(stream, out);
+    out.close();
+  }
+
+  /**
+   * Make sure we delete the temp file.
+   *
+   * <p>We should always be calling {@link ImapResponse#destroy()}, but it's here as a last resort.
+   */
+  @Override
+  protected void finalize() throws Throwable {
+    try {
+      destroy();
+    } finally {
+      super.finalize();
+    }
+  }
+
+  @Override
+  public InputStream getAsStream() {
+    checkNotDestroyed();
+    try {
+      return new FileInputStream(mFile);
+    } catch (FileNotFoundException e) {
+      // It's probably possible if we're low on storage and the system clears the cache dir.
+      LogUtils.w(TAG, "ImapTempFileLiteral: Temp file not found");
+
+      // Return 0 byte stream as a dummy...
+      return new ByteArrayInputStream(new byte[0]);
+    }
+  }
+
+  @Override
+  public String getString() {
+    checkNotDestroyed();
+    try {
+      byte[] bytes = IOUtils.toByteArray(getAsStream());
+      // Prevent crash from OOM; we've seen this, but only rarely and not reproducibly
+      if (bytes.length > ImapResponseParser.LITERAL_KEEP_IN_MEMORY_THRESHOLD) {
+        throw new IOException();
+      }
+      return Utility.fromAscii(bytes);
+    } catch (IOException e) {
+      LogUtils.w(TAG, "ImapTempFileLiteral: Error while reading temp file", e);
+      return "";
+    }
+  }
+
+  @Override
+  public void destroy() {
+    try {
+      if (!isDestroyed() && mFile.exists()) {
+        mFile.delete();
+      }
+    } catch (RuntimeException re) {
+      // Just log and ignore.
+      LogUtils.w(TAG, "Failed to remove temp file: " + re.getMessage());
+    }
+    super.destroy();
+  }
+
+  @Override
+  public String toString() {
+    return String.format("{%d byte literal(file)}", mSize);
+  }
+
+  public boolean tempFileExistsForTest() {
+    return mFile.exists();
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapUtility.java b/java/com/android/voicemail/impl/mail/store/imap/ImapUtility.java
new file mode 100644
index 0000000..a325cc2
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapUtility.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import java.util.ArrayList;
+
+/** Utility methods for use with IMAP. */
+public class ImapUtility {
+  public static final String TAG = "ImapUtility";
+  /**
+   * Apply quoting rules per IMAP RFC, quoted = DQUOTE *QUOTED-CHAR DQUOTE QUOTED-CHAR = <any
+   * TEXT-CHAR except quoted-specials> / "\" quoted-specials quoted-specials = DQUOTE / "\"
+   *
+   * <p>This is used primarily for IMAP login, but might be useful elsewhere.
+   *
+   * <p>NOTE: Not very efficient - you may wish to preflight this, or perhaps it should check for
+   * trouble chars before calling the replace functions.
+   *
+   * @param s The string to be quoted.
+   * @return A copy of the string, having undergone quoting as described above
+   */
+  public static String imapQuoted(String s) {
+
+    // First, quote any backslashes by replacing \ with \\
+    // regex Pattern:  \\    (Java string const = \\\\)
+    // Substitute:     \\\\  (Java string const = \\\\\\\\)
+    String result = s.replaceAll("\\\\", "\\\\\\\\");
+
+    // Then, quote any double-quotes by replacing " with \"
+    // regex Pattern:  "    (Java string const = \")
+    // Substitute:     \\"  (Java string const = \\\\\")
+    result = result.replaceAll("\"", "\\\\\"");
+
+    // return string with quotes around it
+    return "\"" + result + "\"";
+  }
+
+  /**
+   * Gets all of the values in a sequence set per RFC 3501. Any ranges are expanded into a list of
+   * individual numbers. If the set is invalid, an empty array is returned.
+   *
+   * <pre>
+   * sequence-number = nz-number / "*"
+   * sequence-range  = sequence-number ":" sequence-number
+   * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
+   * </pre>
+   */
+  public static String[] getImapSequenceValues(String set) {
+    ArrayList<String> list = new ArrayList<String>();
+    if (set != null) {
+      String[] setItems = set.split(",");
+      for (String item : setItems) {
+        if (item.indexOf(':') == -1) {
+          // simple item
+          try {
+            Integer.parseInt(item); // Don't need the value; just ensure it's valid
+            list.add(item);
+          } catch (NumberFormatException e) {
+            LogUtils.d(TAG, "Invalid UID value", e);
+          }
+        } else {
+          // range
+          for (String rangeItem : getImapRangeValues(item)) {
+            list.add(rangeItem);
+          }
+        }
+      }
+    }
+    String[] stringList = new String[list.size()];
+    return list.toArray(stringList);
+  }
+
+  /**
+   * Expand the given number range into a list of individual numbers. If the range is not valid, an
+   * empty array is returned.
+   *
+   * <pre>
+   * sequence-number = nz-number / "*"
+   * sequence-range  = sequence-number ":" sequence-number
+   * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
+   * </pre>
+   */
+  public static String[] getImapRangeValues(String range) {
+    ArrayList<String> list = new ArrayList<String>();
+    try {
+      if (range != null) {
+        int colonPos = range.indexOf(':');
+        if (colonPos > 0) {
+          int first = Integer.parseInt(range.substring(0, colonPos));
+          int second = Integer.parseInt(range.substring(colonPos + 1));
+          if (first < second) {
+            for (int i = first; i <= second; i++) {
+              list.add(Integer.toString(i));
+            }
+          } else {
+            for (int i = first; i >= second; i--) {
+              list.add(Integer.toString(i));
+            }
+          }
+        }
+      }
+    } catch (NumberFormatException e) {
+      LogUtils.d(TAG, "Invalid range value", e);
+    }
+    String[] stringList = new String[list.size()];
+    return list.toArray(stringList);
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/utility/CountingOutputStream.java b/java/com/android/voicemail/impl/mail/utility/CountingOutputStream.java
new file mode 100644
index 0000000..c358610
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/utility/CountingOutputStream.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.utility;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * A simple pass-thru OutputStream that also counts how many bytes are written to it and makes that
+ * count available to callers.
+ */
+public class CountingOutputStream extends OutputStream {
+  private long mCount;
+  private final OutputStream mOutputStream;
+
+  public CountingOutputStream(OutputStream outputStream) {
+    mOutputStream = outputStream;
+  }
+
+  public long getCount() {
+    return mCount;
+  }
+
+  @Override
+  public void write(byte[] buffer, int offset, int count) throws IOException {
+    mOutputStream.write(buffer, offset, count);
+    mCount += count;
+  }
+
+  @Override
+  public void write(int oneByte) throws IOException {
+    mOutputStream.write(oneByte);
+    mCount++;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/utility/EOLConvertingOutputStream.java b/java/com/android/voicemail/impl/mail/utility/EOLConvertingOutputStream.java
new file mode 100644
index 0000000..72649ac
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/utility/EOLConvertingOutputStream.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.utility;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class EOLConvertingOutputStream extends FilterOutputStream {
+  int lastChar;
+
+  public EOLConvertingOutputStream(OutputStream out) {
+    super(out);
+  }
+
+  @Override
+  public void write(int oneByte) throws IOException {
+    if (oneByte == '\n') {
+      if (lastChar != '\r') {
+        super.write('\r');
+      }
+    }
+    super.write(oneByte);
+    lastChar = oneByte;
+  }
+
+  @Override
+  public void flush() throws IOException {
+    if (lastChar == '\r') {
+      super.write('\n');
+      lastChar = '\n';
+    }
+    super.flush();
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/utils/LogUtils.java b/java/com/android/voicemail/impl/mail/utils/LogUtils.java
new file mode 100644
index 0000000..f6c3c6b
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/utils/LogUtils.java
@@ -0,0 +1,345 @@
+/**
+ * Copyright (c) 2015 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.utils;
+
+import android.net.Uri;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.voicemail.impl.VvmLog;
+import java.util.List;
+
+public class LogUtils {
+  public static final String TAG = "Email Log";
+
+  private static final String ACCOUNT_PREFIX = "account:";
+
+  /** Priority constant for the println method; use LogUtils.v. */
+  public static final int VERBOSE = Log.VERBOSE;
+
+  /** Priority constant for the println method; use LogUtils.d. */
+  public static final int DEBUG = Log.DEBUG;
+
+  /** Priority constant for the println method; use LogUtils.i. */
+  public static final int INFO = Log.INFO;
+
+  /** Priority constant for the println method; use LogUtils.w. */
+  public static final int WARN = Log.WARN;
+
+  /** Priority constant for the println method; use LogUtils.e. */
+  public static final int ERROR = Log.ERROR;
+
+  /**
+   * Used to enable/disable logging that we don't want included in production releases. This should
+   * be set to DEBUG for production releases, and VERBOSE for internal builds.
+   */
+  private static final int MAX_ENABLED_LOG_LEVEL = DEBUG;
+
+  private static Boolean sDebugLoggingEnabledForTests = null;
+
+  /** Enable debug logging for unit tests. */
+  @VisibleForTesting
+  public static void setDebugLoggingEnabledForTests(boolean enabled) {
+    setDebugLoggingEnabledForTestsInternal(enabled);
+  }
+
+  protected static void setDebugLoggingEnabledForTestsInternal(boolean enabled) {
+    sDebugLoggingEnabledForTests = Boolean.valueOf(enabled);
+  }
+
+  /** Returns true if the build configuration prevents debug logging. */
+  @VisibleForTesting
+  public static boolean buildPreventsDebugLogging() {
+    return MAX_ENABLED_LOG_LEVEL > VERBOSE;
+  }
+
+  /** Returns a boolean indicating whether debug logging is enabled. */
+  protected static boolean isDebugLoggingEnabled(String tag) {
+    if (buildPreventsDebugLogging()) {
+      return false;
+    }
+    if (sDebugLoggingEnabledForTests != null) {
+      return sDebugLoggingEnabledForTests.booleanValue();
+    }
+    return Log.isLoggable(tag, Log.DEBUG) || Log.isLoggable(TAG, Log.DEBUG);
+  }
+
+  /**
+   * Returns a String for the specified content provider uri. This will do sanitation of the uri to
+   * remove PII if debug logging is not enabled.
+   */
+  public static String contentUriToString(final Uri uri) {
+    return contentUriToString(TAG, uri);
+  }
+
+  /**
+   * Returns a String for the specified content provider uri. This will do sanitation of the uri to
+   * remove PII if debug logging is not enabled.
+   */
+  public static String contentUriToString(String tag, Uri uri) {
+    if (isDebugLoggingEnabled(tag)) {
+      // Debug logging has been enabled, so log the uri as is
+      return uri.toString();
+    } else {
+      // Debug logging is not enabled, we want to remove the email address from the uri.
+      List<String> pathSegments = uri.getPathSegments();
+
+      Uri.Builder builder =
+          new Uri.Builder()
+              .scheme(uri.getScheme())
+              .authority(uri.getAuthority())
+              .query(uri.getQuery())
+              .fragment(uri.getFragment());
+
+      // This assumes that the first path segment is the account
+      final String account = pathSegments.get(0);
+
+      builder = builder.appendPath(sanitizeAccountName(account));
+      for (int i = 1; i < pathSegments.size(); i++) {
+        builder.appendPath(pathSegments.get(i));
+      }
+      return builder.toString();
+    }
+  }
+
+  /** Sanitizes an account name. If debug logging is not enabled, a sanitized name is returned. */
+  public static String sanitizeAccountName(String accountName) {
+    if (TextUtils.isEmpty(accountName)) {
+      return "";
+    }
+
+    return ACCOUNT_PREFIX + sanitizeName(TAG, accountName);
+  }
+
+  public static String sanitizeName(final String tag, final String name) {
+    if (TextUtils.isEmpty(name)) {
+      return "";
+    }
+
+    if (isDebugLoggingEnabled(tag)) {
+      return name;
+    }
+
+    return String.valueOf(name.hashCode());
+  }
+
+  /**
+   * Checks to see whether or not a log for the specified tag is loggable at the specified level.
+   */
+  public static boolean isLoggable(String tag, int level) {
+    if (MAX_ENABLED_LOG_LEVEL > level) {
+      return false;
+    }
+    return Log.isLoggable(tag, level) || Log.isLoggable(TAG, level);
+  }
+
+  /**
+   * Send a {@link #VERBOSE} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void v(String tag, String format, Object... args) {
+    if (isLoggable(tag, VERBOSE)) {
+      VvmLog.v(tag, String.format(format, args));
+    }
+  }
+
+  /**
+   * Send a {@link #VERBOSE} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param tr An exception to log
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void v(String tag, Throwable tr, String format, Object... args) {
+    if (isLoggable(tag, VERBOSE)) {
+      VvmLog.v(tag, String.format(format, args), tr);
+    }
+  }
+
+  /**
+   * Send a {@link #DEBUG} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void d(String tag, String format, Object... args) {
+    if (isLoggable(tag, DEBUG)) {
+      VvmLog.d(tag, String.format(format, args));
+    }
+  }
+
+  /**
+   * Send a {@link #DEBUG} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param tr An exception to log
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void d(String tag, Throwable tr, String format, Object... args) {
+    if (isLoggable(tag, DEBUG)) {
+      VvmLog.d(tag, String.format(format, args), tr);
+    }
+  }
+
+  /**
+   * Send a {@link #INFO} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void i(String tag, String format, Object... args) {
+    if (isLoggable(tag, INFO)) {
+      VvmLog.i(tag, String.format(format, args));
+    }
+  }
+
+  /**
+   * Send a {@link #INFO} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param tr An exception to log
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void i(String tag, Throwable tr, String format, Object... args) {
+    if (isLoggable(tag, INFO)) {
+      VvmLog.i(tag, String.format(format, args), tr);
+    }
+  }
+
+  /**
+   * Send a {@link #WARN} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void w(String tag, String format, Object... args) {
+    if (isLoggable(tag, WARN)) {
+      VvmLog.w(tag, String.format(format, args));
+    }
+  }
+
+  /**
+   * Send a {@link #WARN} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param tr An exception to log
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void w(String tag, Throwable tr, String format, Object... args) {
+    if (isLoggable(tag, WARN)) {
+      VvmLog.w(tag, String.format(format, args), tr);
+    }
+  }
+
+  /**
+   * Send a {@link #ERROR} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void e(String tag, String format, Object... args) {
+    if (isLoggable(tag, ERROR)) {
+      VvmLog.e(tag, String.format(format, args));
+    }
+  }
+
+  /**
+   * Send a {@link #ERROR} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param tr An exception to log
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void e(String tag, Throwable tr, String format, Object... args) {
+    if (isLoggable(tag, ERROR)) {
+      VvmLog.e(tag, String.format(format, args), tr);
+    }
+  }
+
+  /**
+   * What a Terrible Failure: Report a condition that should never happen. The error will always be
+   * logged at level ASSERT with the call stack. Depending on system configuration, a report may be
+   * added to the {@link android.os.DropBoxManager} and/or the process may be terminated immediately
+   * with an error dialog.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void wtf(String tag, String format, Object... args) {
+    VvmLog.wtf(tag, String.format(format, args), new Error());
+  }
+
+  /**
+   * What a Terrible Failure: Report a condition that should never happen. The error will always be
+   * logged at level ASSERT with the call stack. Depending on system configuration, a report may be
+   * added to the {@link android.os.DropBoxManager} and/or the process may be terminated immediately
+   * with an error dialog.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param tr An exception to log
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void wtf(String tag, Throwable tr, String format, Object... args) {
+    VvmLog.wtf(tag, String.format(format, args), tr);
+  }
+
+  public static String byteToHex(int b) {
+    return byteToHex(new StringBuilder(), b).toString();
+  }
+
+  public static StringBuilder byteToHex(StringBuilder sb, int b) {
+    b &= 0xFF;
+    sb.append("0123456789ABCDEF".charAt(b >> 4));
+    sb.append("0123456789ABCDEF".charAt(b & 0xF));
+    return sb;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/utils/Utility.java b/java/com/android/voicemail/impl/mail/utils/Utility.java
new file mode 100644
index 0000000..4db1681
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/utils/Utility.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2015 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.voicemail.impl.mail.utils;
+
+import java.io.ByteArrayInputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+
+/** Simple utility methods used in email functions. */
+public class Utility {
+  public static final Charset ASCII = Charset.forName("US-ASCII");
+
+  public static final String[] EMPTY_STRINGS = new String[0];
+
+  /**
+   * Returns a concatenated string containing the output of every Object's toString() method, each
+   * separated by the given separator character.
+   */
+  public static String combine(Object[] parts, char separator) {
+    if (parts == null) {
+      return null;
+    }
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < parts.length; i++) {
+      sb.append(parts[i].toString());
+      if (i < parts.length - 1) {
+        sb.append(separator);
+      }
+    }
+    return sb.toString();
+  }
+
+  /** Converts a String to ASCII bytes */
+  public static byte[] toAscii(String s) {
+    return encode(ASCII, s);
+  }
+
+  /** Builds a String from ASCII bytes */
+  public static String fromAscii(byte[] b) {
+    return decode(ASCII, b);
+  }
+
+  private static byte[] encode(Charset charset, String s) {
+    if (s == null) {
+      return null;
+    }
+    final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s));
+    final byte[] bytes = new byte[buffer.limit()];
+    buffer.get(bytes);
+    return bytes;
+  }
+
+  private static String decode(Charset charset, byte[] b) {
+    if (b == null) {
+      return null;
+    }
+    final CharBuffer cb = charset.decode(ByteBuffer.wrap(b));
+    return new String(cb.array(), 0, cb.length());
+  }
+
+  public static ByteArrayInputStream streamFromAsciiString(String ascii) {
+    return new ByteArrayInputStream(toAscii(ascii));
+  }
+}
diff --git a/java/com/android/voicemail/impl/protocol/CvvmProtocol.java b/java/com/android/voicemail/impl/protocol/CvvmProtocol.java
new file mode 100644
index 0000000..a4b54f6
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/CvvmProtocol.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.protocol;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.sms.OmtpCvvmMessageSender;
+import com.android.voicemail.impl.sms.OmtpMessageSender;
+
+/**
+ * A flavor of OMTP protocol with a different mobile originated (MO) format
+ *
+ * <p>Used by carriers such as T-Mobile
+ */
+public class CvvmProtocol extends VisualVoicemailProtocol {
+
+  private static String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
+  private static String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s";
+  private static String IMAP_CLOSE_NUT = "CLOSE_NUT";
+
+  @Override
+  public OmtpMessageSender createMessageSender(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      short applicationPort,
+      String destinationNumber) {
+    return new OmtpCvvmMessageSender(
+        context, phoneAccountHandle, applicationPort, destinationNumber);
+  }
+
+  @Override
+  public String getCommand(String command) {
+    if (command == OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT) {
+      return IMAP_CHANGE_TUI_PWD_FORMAT;
+    }
+    if (command == OmtpConstants.IMAP_CLOSE_NUT) {
+      return IMAP_CLOSE_NUT;
+    }
+    if (command == OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT) {
+      return IMAP_CHANGE_VM_LANG_FORMAT;
+    }
+    return super.getCommand(command);
+  }
+}
diff --git a/java/com/android/voicemail/impl/protocol/OmtpProtocol.java b/java/com/android/voicemail/impl/protocol/OmtpProtocol.java
new file mode 100644
index 0000000..27aab8a
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/OmtpProtocol.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.protocol;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.sms.OmtpMessageSender;
+import com.android.voicemail.impl.sms.OmtpStandardMessageSender;
+
+public class OmtpProtocol extends VisualVoicemailProtocol {
+
+  @Override
+  public OmtpMessageSender createMessageSender(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      short applicationPort,
+      String destinationNumber) {
+    return new OmtpStandardMessageSender(
+        context,
+        phoneAccountHandle,
+        applicationPort,
+        destinationNumber,
+        OmtpConstants.CLIENT_TYPE_GOOGLE_10,
+        OmtpConstants.PROTOCOL_VERSION1_1,
+        null /*clientPrefix*/);
+  }
+}
diff --git a/java/com/android/voicemail/impl/protocol/ProtocolHelper.java b/java/com/android/voicemail/impl/protocol/ProtocolHelper.java
new file mode 100644
index 0000000..4d2e7cc
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/ProtocolHelper.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.protocol;
+
+import android.text.TextUtils;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.sms.OmtpMessageSender;
+
+public class ProtocolHelper {
+
+  private static final String TAG = "ProtocolHelper";
+
+  public static OmtpMessageSender getMessageSender(
+      VisualVoicemailProtocol protocol, OmtpVvmCarrierConfigHelper config) {
+
+    int applicationPort = config.getApplicationPort();
+    String destinationNumber = config.getDestinationNumber();
+    if (TextUtils.isEmpty(destinationNumber)) {
+      VvmLog.w(TAG, "No destination number for this carrier.");
+      return null;
+    }
+
+    return protocol.createMessageSender(
+        config.getContext(),
+        config.getPhoneAccountHandle(),
+        (short) applicationPort,
+        destinationNumber);
+  }
+}
diff --git a/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocol.java b/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocol.java
new file mode 100644
index 0000000..6cf82f1
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocol.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.protocol;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.DefaultOmtpEventHandler;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.sms.OmtpMessageSender;
+import com.android.voicemail.impl.sms.StatusMessage;
+
+public abstract class VisualVoicemailProtocol {
+
+  /** Activation should cause the carrier to respond with a STATUS SMS. */
+  public void startActivation(OmtpVvmCarrierConfigHelper config, PendingIntent sentIntent) {
+    OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
+    if (messageSender != null) {
+      messageSender.requestVvmActivation(sentIntent);
+    }
+  }
+
+  public void startDeactivation(OmtpVvmCarrierConfigHelper config) {
+    OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
+    if (messageSender != null) {
+      messageSender.requestVvmDeactivation(null);
+    }
+  }
+
+  public boolean supportsProvisioning() {
+    return false;
+  }
+
+  public void startProvisioning(
+      ActivationTask task,
+      PhoneAccountHandle handle,
+      OmtpVvmCarrierConfigHelper config,
+      VoicemailStatus.Editor editor,
+      StatusMessage message,
+      Bundle data) {
+    // Do nothing
+  }
+
+  public void requestStatus(OmtpVvmCarrierConfigHelper config, @Nullable PendingIntent sentIntent) {
+    OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
+    if (messageSender != null) {
+      messageSender.requestVvmStatus(sentIntent);
+    }
+  }
+
+  public abstract OmtpMessageSender createMessageSender(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      short applicationPort,
+      String destinationNumber);
+
+  /**
+   * Translate an OMTP IMAP command to the protocol specific one. For example, changing the TUI
+   * password on OMTP is XCHANGE_TUI_PWD, but on CVVM and VVM3 it is CHANGE_TUI_PWD.
+   *
+   * @param command A String command in {@link OmtpConstants}, the exact
+   *     instance should be used instead of its' value.
+   * @returns Translated command, or {@code null} if not available in this protocol
+   */
+  public String getCommand(String command) {
+    return command;
+  }
+
+  public void handleEvent(
+      Context context,
+      OmtpVvmCarrierConfigHelper config,
+      VoicemailStatus.Editor status,
+      OmtpEvents event) {
+    DefaultOmtpEventHandler.handleEvent(context, config, status, event);
+  }
+
+  /**
+   * Given an VVM SMS with an unknown {@code event}, let the protocol attempt to translate it into
+   * an equivalent STATUS SMS. Returns {@code null} if it cannot be translated.
+   */
+  @Nullable
+  public Bundle translateStatusSmsBundle(
+      OmtpVvmCarrierConfigHelper config, String event, Bundle data) {
+    return null;
+  }
+}
diff --git a/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocolFactory.java b/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocolFactory.java
new file mode 100644
index 0000000..056fb2e
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocolFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.protocol;
+
+import android.content.res.Resources;
+import android.support.annotation.Nullable;
+import android.telephony.TelephonyManager;
+import com.android.voicemail.impl.VvmLog;
+
+public class VisualVoicemailProtocolFactory {
+
+  private static final String TAG = "VvmProtocolFactory";
+
+  private static final String VVM_TYPE_VVM3 = "vvm_type_vvm3";
+
+  @Nullable
+  public static VisualVoicemailProtocol create(Resources resources, String type) {
+    if (type == null) {
+      return null;
+    }
+    switch (type) {
+      case TelephonyManager.VVM_TYPE_OMTP:
+        return new OmtpProtocol();
+      case TelephonyManager.VVM_TYPE_CVVM:
+        return new CvvmProtocol();
+      case VVM_TYPE_VVM3:
+        return new Vvm3Protocol();
+      default:
+        VvmLog.e(TAG, "Unexpected visual voicemail type: " + type);
+    }
+    return null;
+  }
+}
diff --git a/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java b/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java
new file mode 100644
index 0000000..8bc3cc2
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.protocol;
+
+import android.content.Context;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.IntDef;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.DefaultOmtpEventHandler;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpEvents.Type;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.settings.VoicemailChangePinActivity;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Handles {@link OmtpEvents} when {@link Vvm3Protocol} is being used. This handler writes custom
+ * error codes into the voicemail status table so support on the dialer side is required.
+ *
+ * <p>TODO(b/29577838) disable VVM3 by default so support on system dialer can be ensured.
+ */
+public class Vvm3EventHandler {
+
+  private static final String TAG = "Vvm3EventHandler";
+
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({
+    VMS_DNS_FAILURE,
+    VMG_DNS_FAILURE,
+    SPG_DNS_FAILURE,
+    VMS_NO_CELLULAR,
+    VMG_NO_CELLULAR,
+    SPG_NO_CELLULAR,
+    VMS_TIMEOUT,
+    VMG_TIMEOUT,
+    STATUS_SMS_TIMEOUT,
+    SUBSCRIBER_BLOCKED,
+    UNKNOWN_USER,
+    UNKNOWN_DEVICE,
+    INVALID_PASSWORD,
+    MAILBOX_NOT_INITIALIZED,
+    SERVICE_NOT_PROVISIONED,
+    SERVICE_NOT_ACTIVATED,
+    USER_BLOCKED,
+    IMAP_GETQUOTA_ERROR,
+    IMAP_SELECT_ERROR,
+    IMAP_ERROR,
+    VMG_INTERNAL_ERROR,
+    VMG_DB_ERROR,
+    VMG_COMMUNICATION_ERROR,
+    SPG_URL_NOT_FOUND,
+    VMG_UNKNOWN_ERROR,
+    PIN_NOT_SET
+  })
+  public @interface ErrorCode {}
+
+  public static final int VMS_DNS_FAILURE = -9001;
+  public static final int VMG_DNS_FAILURE = -9002;
+  public static final int SPG_DNS_FAILURE = -9003;
+  public static final int VMS_NO_CELLULAR = -9004;
+  public static final int VMG_NO_CELLULAR = -9005;
+  public static final int SPG_NO_CELLULAR = -9006;
+  public static final int VMS_TIMEOUT = -9007;
+  public static final int VMG_TIMEOUT = -9008;
+  public static final int STATUS_SMS_TIMEOUT = -9009;
+
+  public static final int SUBSCRIBER_BLOCKED = -9990;
+  public static final int UNKNOWN_USER = -9991;
+  public static final int UNKNOWN_DEVICE = -9992;
+  public static final int INVALID_PASSWORD = -9993;
+  public static final int MAILBOX_NOT_INITIALIZED = -9994;
+  public static final int SERVICE_NOT_PROVISIONED = -9995;
+  public static final int SERVICE_NOT_ACTIVATED = -9996;
+  public static final int USER_BLOCKED = -9998;
+  public static final int IMAP_GETQUOTA_ERROR = -9997;
+  public static final int IMAP_SELECT_ERROR = -9989;
+  public static final int IMAP_ERROR = -9999;
+
+  public static final int VMG_INTERNAL_ERROR = -101;
+  public static final int VMG_DB_ERROR = -102;
+  public static final int VMG_COMMUNICATION_ERROR = -103;
+  public static final int SPG_URL_NOT_FOUND = -301;
+
+  // Non VVM3 codes:
+  public static final int VMG_UNKNOWN_ERROR = -1;
+  public static final int PIN_NOT_SET = -100;
+  // STATUS SMS returned st=U and rc!=2. The user cannot be provisioned and must contact customer
+  // support.
+  public static final int SUBSCRIBER_UNKNOWN = -99;
+
+  public static void handleEvent(
+      Context context,
+      OmtpVvmCarrierConfigHelper config,
+      VoicemailStatus.Editor status,
+      OmtpEvents event) {
+    boolean handled = false;
+    switch (event.getType()) {
+      case Type.CONFIGURATION:
+        handled = handleConfigurationEvent(context, status, event);
+        break;
+      case Type.DATA_CHANNEL:
+        handled = handleDataChannelEvent(status, event);
+        break;
+      case Type.NOTIFICATION_CHANNEL:
+        handled = handleNotificationChannelEvent(status, event);
+        break;
+      case Type.OTHER:
+        handled = handleOtherEvent(status, event);
+        break;
+      default:
+        VvmLog.wtf(TAG, "invalid event type " + event.getType() + " for " + event);
+    }
+    if (!handled) {
+      DefaultOmtpEventHandler.handleEvent(context, config, status, event);
+    }
+  }
+
+  private static boolean handleConfigurationEvent(
+      Context context, VoicemailStatus.Editor status, OmtpEvents event) {
+    switch (event) {
+      case CONFIG_REQUEST_STATUS_SUCCESS:
+        if (!isPinRandomized(context, status.getPhoneAccountHandle())) {
+          return false;
+        } else {
+          postError(status, PIN_NOT_SET);
+        }
+        break;
+      case CONFIG_ACTIVATING_SUBSEQUENT:
+        if (isPinRandomized(context, status.getPhoneAccountHandle())) {
+          status.setConfigurationState(PIN_NOT_SET);
+        } else {
+          status.setConfigurationState(Status.CONFIGURATION_STATE_OK);
+        }
+        status
+            .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+            .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
+            .apply();
+        break;
+      case CONFIG_DEFAULT_PIN_REPLACED:
+        postError(status, PIN_NOT_SET);
+        break;
+      case CONFIG_STATUS_SMS_TIME_OUT:
+        postError(status, STATUS_SMS_TIMEOUT);
+        break;
+      default:
+        return false;
+    }
+    return true;
+  }
+
+  private static boolean handleDataChannelEvent(VoicemailStatus.Editor status, OmtpEvents event) {
+    switch (event) {
+      case DATA_NO_CONNECTION:
+      case DATA_NO_CONNECTION_CELLULAR_REQUIRED:
+      case DATA_ALL_SOCKET_CONNECTION_FAILED:
+        postError(status, VMS_NO_CELLULAR);
+        break;
+      case DATA_SSL_INVALID_HOST_NAME:
+      case DATA_CANNOT_ESTABLISH_SSL_SESSION:
+      case DATA_IOE_ON_OPEN:
+        postError(status, VMS_TIMEOUT);
+        break;
+      case DATA_CANNOT_RESOLVE_HOST_ON_NETWORK:
+        postError(status, VMS_DNS_FAILURE);
+        break;
+      case DATA_BAD_IMAP_CREDENTIAL:
+        postError(status, IMAP_ERROR);
+        break;
+      case DATA_AUTH_UNKNOWN_USER:
+        postError(status, UNKNOWN_USER);
+        break;
+      case DATA_AUTH_UNKNOWN_DEVICE:
+        postError(status, UNKNOWN_DEVICE);
+        break;
+      case DATA_AUTH_INVALID_PASSWORD:
+        postError(status, INVALID_PASSWORD);
+        break;
+      case DATA_AUTH_MAILBOX_NOT_INITIALIZED:
+        postError(status, MAILBOX_NOT_INITIALIZED);
+        break;
+      case DATA_AUTH_SERVICE_NOT_PROVISIONED:
+        postError(status, SERVICE_NOT_PROVISIONED);
+        break;
+      case DATA_AUTH_SERVICE_NOT_ACTIVATED:
+        postError(status, SERVICE_NOT_ACTIVATED);
+        break;
+      case DATA_AUTH_USER_IS_BLOCKED:
+        postError(status, USER_BLOCKED);
+        break;
+      case DATA_REJECTED_SERVER_RESPONSE:
+      case DATA_INVALID_INITIAL_SERVER_RESPONSE:
+      case DATA_SSL_EXCEPTION:
+        postError(status, IMAP_ERROR);
+        break;
+      default:
+        return false;
+    }
+    return true;
+  }
+
+  private static boolean handleNotificationChannelEvent(
+      VoicemailStatus.Editor unusedStatus, OmtpEvents unusedEvent) {
+    return false;
+  }
+
+  private static boolean handleOtherEvent(VoicemailStatus.Editor status, OmtpEvents event) {
+    switch (event) {
+      case VVM3_NEW_USER_SETUP_FAILED:
+        postError(status, MAILBOX_NOT_INITIALIZED);
+        break;
+      case VVM3_VMG_DNS_FAILURE:
+        postError(status, VMG_DNS_FAILURE);
+        break;
+      case VVM3_SPG_DNS_FAILURE:
+        postError(status, SPG_DNS_FAILURE);
+        break;
+      case VVM3_VMG_CONNECTION_FAILED:
+        postError(status, VMG_NO_CELLULAR);
+        break;
+      case VVM3_SPG_CONNECTION_FAILED:
+        postError(status, SPG_NO_CELLULAR);
+        break;
+      case VVM3_VMG_TIMEOUT:
+        postError(status, VMG_TIMEOUT);
+        break;
+      case VVM3_SUBSCRIBER_PROVISIONED:
+        postError(status, SERVICE_NOT_ACTIVATED);
+        break;
+      case VVM3_SUBSCRIBER_BLOCKED:
+        postError(status, SUBSCRIBER_BLOCKED);
+        break;
+      case VVM3_SUBSCRIBER_UNKNOWN:
+        postError(status, SUBSCRIBER_UNKNOWN);
+        break;
+      default:
+        return false;
+    }
+    return true;
+  }
+
+  private static void postError(VoicemailStatus.Editor editor, @ErrorCode int errorCode) {
+    switch (errorCode) {
+      case VMG_DNS_FAILURE:
+      case SPG_DNS_FAILURE:
+      case VMG_NO_CELLULAR:
+      case SPG_NO_CELLULAR:
+      case VMG_TIMEOUT:
+      case SUBSCRIBER_BLOCKED:
+      case UNKNOWN_USER:
+      case UNKNOWN_DEVICE:
+      case INVALID_PASSWORD:
+      case MAILBOX_NOT_INITIALIZED:
+      case SERVICE_NOT_PROVISIONED:
+      case SERVICE_NOT_ACTIVATED:
+      case USER_BLOCKED:
+      case VMG_UNKNOWN_ERROR:
+      case SPG_URL_NOT_FOUND:
+      case VMG_INTERNAL_ERROR:
+      case VMG_DB_ERROR:
+      case VMG_COMMUNICATION_ERROR:
+      case PIN_NOT_SET:
+      case SUBSCRIBER_UNKNOWN:
+        editor.setConfigurationState(errorCode);
+        break;
+      case VMS_NO_CELLULAR:
+      case VMS_DNS_FAILURE:
+      case VMS_TIMEOUT:
+      case IMAP_GETQUOTA_ERROR:
+      case IMAP_SELECT_ERROR:
+      case IMAP_ERROR:
+        editor.setDataChannelState(errorCode);
+        break;
+      case STATUS_SMS_TIMEOUT:
+        editor.setNotificationChannelState(errorCode);
+        break;
+      default:
+        VvmLog.wtf(TAG, "unknown error code: " + errorCode);
+    }
+    editor.apply();
+  }
+
+  private static boolean isPinRandomized(Context context, PhoneAccountHandle phoneAccountHandle) {
+    if (phoneAccountHandle == null) {
+      // This should never happen.
+      VvmLog.e(TAG, "status editor has null phone account handle");
+      return false;
+    }
+    return VoicemailChangePinActivity.isDefaultOldPinSet(context, phoneAccountHandle);
+  }
+}
diff --git a/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java b/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java
new file mode 100644
index 0000000..f293a4c
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.protocol;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.net.Network;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+import com.android.voicemail.impl.settings.VoicemailChangePinActivity;
+import com.android.voicemail.impl.sms.OmtpMessageSender;
+import com.android.voicemail.impl.sms.StatusMessage;
+import com.android.voicemail.impl.sms.Vvm3MessageSender;
+import com.android.voicemail.impl.sync.VvmNetworkRequest;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.util.Locale;
+
+/**
+ * A flavor of OMTP protocol with a different provisioning process
+ *
+ * <p>Used by carriers such as Verizon Wireless
+ */
+@TargetApi(VERSION_CODES.O)
+public class Vvm3Protocol extends VisualVoicemailProtocol {
+
+  private static final String TAG = "Vvm3Protocol";
+
+  private static final String SMS_EVENT_UNRECOGNIZED = "UNRECOGNIZED";
+  private static final String SMS_EVENT_UNRECOGNIZED_CMD = "cmd";
+  private static final String SMS_EVENT_UNRECOGNIZED_STATUS = "STATUS";
+  private static final String DEFAULT_VMG_URL_KEY = "default_vmg_url";
+
+  private static final String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
+  private static final String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s";
+  private static final String IMAP_CLOSE_NUT = "CLOSE_NUT";
+
+  private static final String ISO639_Spanish = "es";
+
+  /**
+   * For VVM3, if the STATUS SMS returns {@link StatusMessage#getProvisioningStatus()} of {@link
+   * OmtpConstants#SUBSCRIBER_UNKNOWN} and {@link StatusMessage#getReturnCode()} of this value, the
+   * user can self-provision visual voicemail service. For other response codes, the user must
+   * contact customer support to resolve the issue.
+   */
+  private static final String VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE = "2";
+
+  // Default prompt level when using the telephone user interface.
+  // Standard prompt when the user call into the voicemail, and no prompts when someone else is
+  // leaving a voicemail.
+  private static final String VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS = "5";
+  private static final String VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS = "6";
+
+  private static final int DEFAULT_PIN_LENGTH = 6;
+
+  @Override
+  public void startActivation(
+      OmtpVvmCarrierConfigHelper config, @Nullable PendingIntent sentIntent) {
+    // VVM3 does not support activation SMS.
+    // Send a status request which will start the provisioning process if the user is not
+    // provisioned.
+    VvmLog.i(TAG, "Activating");
+    config.requestStatus(sentIntent);
+  }
+
+  @Override
+  public void startDeactivation(OmtpVvmCarrierConfigHelper config) {
+    // VVM3 does not support deactivation.
+    // do nothing.
+  }
+
+  @Override
+  public boolean supportsProvisioning() {
+    return true;
+  }
+
+  @Override
+  public void startProvisioning(
+      ActivationTask task,
+      PhoneAccountHandle phoneAccountHandle,
+      OmtpVvmCarrierConfigHelper config,
+      VoicemailStatus.Editor status,
+      StatusMessage message,
+      Bundle data) {
+    VvmLog.i(TAG, "start vvm3 provisioning");
+    if (OmtpConstants.SUBSCRIBER_UNKNOWN.equals(message.getProvisioningStatus())) {
+      VvmLog.i(TAG, "Provisioning status: Unknown");
+      if (VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE.equals(message.getReturnCode())) {
+        VvmLog.i(TAG, "Self provisioning available, subscribing");
+        new Vvm3Subscriber(task, phoneAccountHandle, config, status, data).subscribe();
+      } else {
+        config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_UNKNOWN);
+      }
+    } else if (OmtpConstants.SUBSCRIBER_NEW.equals(message.getProvisioningStatus())) {
+      VvmLog.i(TAG, "setting up new user");
+      // Save the IMAP credentials in preferences so they are persistent and can be retrieved.
+      VisualVoicemailPreferences prefs =
+          new VisualVoicemailPreferences(config.getContext(), phoneAccountHandle);
+      message.putStatus(prefs.edit()).apply();
+
+      startProvisionNewUser(task, phoneAccountHandle, config, status, message);
+    } else if (OmtpConstants.SUBSCRIBER_PROVISIONED.equals(message.getProvisioningStatus())) {
+      VvmLog.i(TAG, "User provisioned but not activated, disabling VVM");
+      VisualVoicemailSettingsUtil.setEnabled(config.getContext(), phoneAccountHandle, false);
+    } else if (OmtpConstants.SUBSCRIBER_BLOCKED.equals(message.getProvisioningStatus())) {
+      VvmLog.i(TAG, "User blocked");
+      config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_BLOCKED);
+    }
+  }
+
+  @Override
+  public OmtpMessageSender createMessageSender(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      short applicationPort,
+      String destinationNumber) {
+    return new Vvm3MessageSender(context, phoneAccountHandle, applicationPort, destinationNumber);
+  }
+
+  @Override
+  public void handleEvent(
+      Context context,
+      OmtpVvmCarrierConfigHelper config,
+      VoicemailStatus.Editor status,
+      OmtpEvents event) {
+    Vvm3EventHandler.handleEvent(context, config, status, event);
+  }
+
+  @Override
+  public String getCommand(String command) {
+    switch (command) {
+      case OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT:
+        return IMAP_CHANGE_TUI_PWD_FORMAT;
+      case OmtpConstants.IMAP_CLOSE_NUT:
+        return IMAP_CLOSE_NUT;
+      case OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT:
+        return IMAP_CHANGE_VM_LANG_FORMAT;
+      default:
+        return super.getCommand(command);
+    }
+  }
+
+  @Override
+  public Bundle translateStatusSmsBundle(
+      OmtpVvmCarrierConfigHelper config, String event, Bundle data) {
+    // UNRECOGNIZED?cmd=STATUS is the response of a STATUS request when the user is provisioned
+    // with iPhone visual voicemail without VoLTE. Translate it into an unprovisioned status
+    // so provisioning can be done.
+    if (!SMS_EVENT_UNRECOGNIZED.equals(event)) {
+      return null;
+    }
+    if (!SMS_EVENT_UNRECOGNIZED_STATUS.equals(data.getString(SMS_EVENT_UNRECOGNIZED_CMD))) {
+      return null;
+    }
+    Bundle bundle = new Bundle();
+    bundle.putString(OmtpConstants.PROVISIONING_STATUS, OmtpConstants.SUBSCRIBER_UNKNOWN);
+    bundle.putString(
+        OmtpConstants.RETURN_CODE, VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE);
+    String vmgUrl = config.getString(DEFAULT_VMG_URL_KEY);
+    if (TextUtils.isEmpty(vmgUrl)) {
+      VvmLog.e(TAG, "Unable to translate STATUS SMS: VMG URL is not set in config");
+      return null;
+    }
+    bundle.putString(Vvm3Subscriber.VMG_URL_KEY, vmgUrl);
+    VvmLog.i(TAG, "UNRECOGNIZED?cmd=STATUS translated into unprovisioned STATUS SMS");
+    return bundle;
+  }
+
+  private void startProvisionNewUser(
+      ActivationTask task,
+      PhoneAccountHandle phoneAccountHandle,
+      OmtpVvmCarrierConfigHelper config,
+      VoicemailStatus.Editor status,
+      StatusMessage message) {
+    try (NetworkWrapper wrapper =
+        VvmNetworkRequest.getNetwork(config, phoneAccountHandle, status)) {
+      Network network = wrapper.get();
+
+      VvmLog.i(TAG, "new user: network available");
+      try (ImapHelper helper =
+          new ImapHelper(config.getContext(), phoneAccountHandle, network, status)) {
+        // VVM3 has inconsistent error language code to OMTP. Just issue a raw command
+        // here.
+        // TODO(b/29082671): use LocaleList
+        if (Locale.getDefault().getLanguage().equals(new Locale(ISO639_Spanish).getLanguage())) {
+          // Spanish
+          helper.changeVoicemailTuiLanguage(VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS);
+        } else {
+          // English
+          helper.changeVoicemailTuiLanguage(VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS);
+        }
+        VvmLog.i(TAG, "new user: language set");
+
+        if (setPin(config.getContext(), phoneAccountHandle, helper, message)) {
+          // Only close new user tutorial if the PIN has been changed.
+          helper.closeNewUserTutorial();
+          VvmLog.i(TAG, "new user: NUT closed");
+
+          config.requestStatus(null);
+        }
+      } catch (InitializingException | MessagingException | IOException e) {
+        config.handleEvent(status, OmtpEvents.VVM3_NEW_USER_SETUP_FAILED);
+        task.fail();
+        VvmLog.e(TAG, e.toString());
+      }
+    } catch (RequestFailedException e) {
+      config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
+      task.fail();
+    }
+  }
+
+  private static boolean setPin(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      ImapHelper helper,
+      StatusMessage message)
+      throws IOException, MessagingException {
+    String defaultPin = getDefaultPin(message);
+    if (defaultPin == null) {
+      VvmLog.i(TAG, "cannot generate default PIN");
+      return false;
+    }
+
+    if (VoicemailChangePinActivity.isDefaultOldPinSet(context, phoneAccountHandle)) {
+      // The pin was already set
+      VvmLog.i(TAG, "PIN already set");
+      return true;
+    }
+    String newPin = generatePin(getMinimumPinLength(context, phoneAccountHandle));
+    if (helper.changePin(defaultPin, newPin) == OmtpConstants.CHANGE_PIN_SUCCESS) {
+      VoicemailChangePinActivity.setDefaultOldPIN(context, phoneAccountHandle, newPin);
+      helper.handleEvent(OmtpEvents.CONFIG_DEFAULT_PIN_REPLACED);
+    }
+    VvmLog.i(TAG, "new user: PIN set");
+    return true;
+  }
+
+  @Nullable
+  private static String getDefaultPin(StatusMessage message) {
+    // The IMAP username is [phone number]@example.com
+    String username = message.getImapUserName();
+    try {
+      String number = username.substring(0, username.indexOf('@'));
+      if (number.length() < 4) {
+        VvmLog.e(TAG, "unable to extract number from IMAP username");
+        return null;
+      }
+      return "1" + number.substring(number.length() - 4);
+    } catch (StringIndexOutOfBoundsException e) {
+      VvmLog.e(TAG, "unable to extract number from IMAP username");
+      return null;
+    }
+  }
+
+  private static int getMinimumPinLength(Context context, PhoneAccountHandle phoneAccountHandle) {
+    VisualVoicemailPreferences preferences =
+        new VisualVoicemailPreferences(context, phoneAccountHandle);
+    // The OMTP pin length format is {min}-{max}
+    String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-");
+    if (lengths.length == 2) {
+      try {
+        return Integer.parseInt(lengths[0]);
+      } catch (NumberFormatException e) {
+        return DEFAULT_PIN_LENGTH;
+      }
+    }
+    return DEFAULT_PIN_LENGTH;
+  }
+
+  private static String generatePin(int length) {
+    SecureRandom random = new SecureRandom();
+    return String.format(Locale.US, "%010d", Math.abs(random.nextLong())).substring(0, length);
+  }
+}
diff --git a/java/com/android/voicemail/impl/protocol/Vvm3Subscriber.java b/java/com/android/voicemail/impl/protocol/Vvm3Subscriber.java
new file mode 100644
index 0000000..c8a74c8
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/Vvm3Subscriber.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.protocol;
+
+import android.annotation.TargetApi;
+import android.net.Network;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.style.URLSpan;
+import android.util.ArrayMap;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.sync.VvmNetworkRequest;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
+import com.android.volley.AuthFailureError;
+import com.android.volley.Request;
+import com.android.volley.RequestQueue;
+import com.android.volley.toolbox.HurlStack;
+import com.android.volley.toolbox.RequestFuture;
+import com.android.volley.toolbox.StringRequest;
+import com.android.volley.toolbox.Volley;
+import java.io.IOException;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Class to subscribe to basic VVM3 visual voicemail, for example, Verizon. Subscription is required
+ * when the user is unprovisioned. This could happen when the user is on a legacy service, or
+ * switched over from devices that used other type of visual voicemail.
+ *
+ * <p>The STATUS SMS will come with a URL to the voicemail management gateway. From it we can find
+ * the self provisioning gateway URL that we can modify voicemail services.
+ *
+ * <p>A request to the self provisioning gateway to activate basic visual voicemail will return us
+ * with a web page. If the user hasn't subscribe to it yet it will contain a link to confirm the
+ * subscription. This link should be clicked through cellular network, and have cookies enabled.
+ *
+ * <p>After the process is completed, the carrier should send us another STATUS SMS with a new or
+ * ready user.
+ */
+@TargetApi(VERSION_CODES.O)
+public class Vvm3Subscriber {
+
+  private static final String TAG = "Vvm3Subscriber";
+
+  private static final String OPERATION_GET_SPG_URL = "retrieveSPGURL";
+  private static final String SPG_URL_TAG = "spgurl";
+  private static final String TRANSACTION_ID_TAG = "transactionid";
+  //language=XML
+  private static final String VMG_XML_REQUEST_FORMAT =
+      ""
+          + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+          + "<VMGVVMRequest>"
+          + "  <MessageHeader>"
+          + "    <transactionid>%1$s</transactionid>"
+          + "  </MessageHeader>"
+          + "  <MessageBody>"
+          + "    <mdn>%2$s</mdn>"
+          + "    <operation>%3$s</operation>"
+          + "    <source>Device</source>"
+          + "    <devicemodel>%4$s</devicemodel>"
+          + "  </MessageBody>"
+          + "</VMGVVMRequest>";
+
+  static final String VMG_URL_KEY = "vmg_url";
+
+  // Self provisioning POST key/values. VVM3 API 2.1.0 12.3
+  private static final String SPG_VZW_MDN_PARAM = "VZW_MDN";
+  private static final String SPG_VZW_SERVICE_PARAM = "VZW_SERVICE";
+  private static final String SPG_VZW_SERVICE_BASIC = "BVVM";
+  private static final String SPG_DEVICE_MODEL_PARAM = "DEVICE_MODEL";
+  // Value for all android device
+  private static final String SPG_DEVICE_MODEL_ANDROID = "DROID_4G";
+  private static final String SPG_APP_TOKEN_PARAM = "APP_TOKEN";
+  private static final String SPG_APP_TOKEN = "q8e3t5u2o1";
+  private static final String SPG_LANGUAGE_PARAM = "SPG_LANGUAGE_PARAM";
+  private static final String SPG_LANGUAGE_EN = "ENGLISH";
+
+  private static final String BASIC_SUBSCRIBE_LINK_TEXT = "Subscribe to Basic Visual Voice Mail";
+
+  private static final int REQUEST_TIMEOUT_SECONDS = 30;
+
+  private final ActivationTask mTask;
+  private final PhoneAccountHandle mHandle;
+  private final OmtpVvmCarrierConfigHelper mHelper;
+  private final VoicemailStatus.Editor mStatus;
+  private final Bundle mData;
+
+  private final String mNumber;
+
+  private RequestQueue mRequestQueue;
+
+  private static class ProvisioningException extends Exception {
+
+    public ProvisioningException(String message) {
+      super(message);
+    }
+  }
+
+  static {
+    // Set the default cookie handler to retain session data for the self provisioning gateway.
+    // Note; this is not ideal as it is application-wide, and can easily get clobbered.
+    // But it seems to be the preferred way to manage cookie for HttpURLConnection, and manually
+    // managing cookies will greatly increase complexity.
+    CookieManager cookieManager = new CookieManager();
+    CookieHandler.setDefault(cookieManager);
+  }
+
+  @WorkerThread
+  public Vvm3Subscriber(
+      ActivationTask task,
+      PhoneAccountHandle handle,
+      OmtpVvmCarrierConfigHelper helper,
+      VoicemailStatus.Editor status,
+      Bundle data) {
+    Assert.isNotMainThread();
+    mTask = task;
+    mHandle = handle;
+    mHelper = helper;
+    mStatus = status;
+    mData = data;
+
+    // Assuming getLine1Number() will work with VVM3. For unprovisioned users the IMAP username
+    // is not included in the status SMS, thus no other way to get the current phone number.
+    mNumber =
+        mHelper
+            .getContext()
+            .getSystemService(TelephonyManager.class)
+            .createForPhoneAccountHandle(mHandle)
+            .getLine1Number();
+  }
+
+  @WorkerThread
+  public void subscribe() {
+    Assert.isNotMainThread();
+    // Cellular data is required to subscribe.
+    // processSubscription() is called after network is available.
+    VvmLog.i(TAG, "Subscribing");
+
+    try (NetworkWrapper wrapper = VvmNetworkRequest.getNetwork(mHelper, mHandle, mStatus)) {
+      Network network = wrapper.get();
+      VvmLog.d(TAG, "provisioning: network available");
+      mRequestQueue =
+          Volley.newRequestQueue(mHelper.getContext(), new NetworkSpecifiedHurlStack(network));
+      processSubscription();
+    } catch (RequestFailedException e) {
+      mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
+      mTask.fail();
+    }
+  }
+
+  private void processSubscription() {
+    try {
+      String gatewayUrl = getSelfProvisioningGateway();
+      String selfProvisionResponse = getSelfProvisionResponse(gatewayUrl);
+      String subscribeLink = findSubscribeLink(selfProvisionResponse);
+      clickSubscribeLink(subscribeLink);
+    } catch (ProvisioningException e) {
+      VvmLog.e(TAG, e.toString());
+      mTask.fail();
+    }
+  }
+
+  /** Get the URL to perform self-provisioning from the voicemail management gateway. */
+  private String getSelfProvisioningGateway() throws ProvisioningException {
+    VvmLog.i(TAG, "retrieving SPG URL");
+    String response = vvm3XmlRequest(OPERATION_GET_SPG_URL);
+    return extractText(response, SPG_URL_TAG);
+  }
+
+  /**
+   * Sent a request to the self-provisioning gateway, which will return us with a webpage. The page
+   * might contain a "Subscribe to Basic Visual Voice Mail" link to complete the subscription. The
+   * cookie from this response and cellular data is required to click the link.
+   */
+  private String getSelfProvisionResponse(String url) throws ProvisioningException {
+    VvmLog.i(TAG, "Retrieving self provisioning response");
+
+    RequestFuture<String> future = RequestFuture.newFuture();
+
+    StringRequest stringRequest =
+        new StringRequest(Request.Method.POST, url, future, future) {
+          @Override
+          protected Map<String, String> getParams() {
+            Map<String, String> params = new ArrayMap<>();
+            params.put(SPG_VZW_MDN_PARAM, mNumber);
+            params.put(SPG_VZW_SERVICE_PARAM, SPG_VZW_SERVICE_BASIC);
+            params.put(SPG_DEVICE_MODEL_PARAM, SPG_DEVICE_MODEL_ANDROID);
+            params.put(SPG_APP_TOKEN_PARAM, SPG_APP_TOKEN);
+            // Language to display the subscription page. The page is never shown to the user
+            // so just use English.
+            params.put(SPG_LANGUAGE_PARAM, SPG_LANGUAGE_EN);
+            return params;
+          }
+        };
+
+    mRequestQueue.add(stringRequest);
+    try {
+      return future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+    } catch (InterruptedException | ExecutionException | TimeoutException e) {
+      mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
+      throw new ProvisioningException(e.toString());
+    }
+  }
+
+  private void clickSubscribeLink(String subscribeLink) throws ProvisioningException {
+    VvmLog.i(TAG, "Clicking subscribe link");
+    RequestFuture<String> future = RequestFuture.newFuture();
+
+    StringRequest stringRequest =
+        new StringRequest(Request.Method.POST, subscribeLink, future, future);
+    mRequestQueue.add(stringRequest);
+    try {
+      // A new STATUS SMS will be sent after this request.
+      future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+    } catch (TimeoutException | ExecutionException | InterruptedException e) {
+      mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
+      throw new ProvisioningException(e.toString());
+    }
+    // It could take very long for the STATUS SMS to return. Waiting for it is unreliable.
+    // Just leave the CONFIG STATUS as CONFIGURING and end the task. The user can always
+    // manually retry if it took too long.
+  }
+
+  private String vvm3XmlRequest(String operation) throws ProvisioningException {
+    VvmLog.d(TAG, "Sending vvm3XmlRequest for " + operation);
+    String voicemailManagementGateway = mData.getString(VMG_URL_KEY);
+    if (voicemailManagementGateway == null) {
+      VvmLog.e(TAG, "voicemailManagementGateway url unknown");
+      return null;
+    }
+    String transactionId = createTransactionId();
+    String body =
+        String.format(
+            Locale.US, VMG_XML_REQUEST_FORMAT, transactionId, mNumber, operation, Build.MODEL);
+
+    RequestFuture<String> future = RequestFuture.newFuture();
+    StringRequest stringRequest =
+        new StringRequest(Request.Method.POST, voicemailManagementGateway, future, future) {
+          @Override
+          public byte[] getBody() throws AuthFailureError {
+            return body.getBytes();
+          }
+        };
+    mRequestQueue.add(stringRequest);
+
+    try {
+      String response = future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+      if (!transactionId.equals(extractText(response, TRANSACTION_ID_TAG))) {
+        throw new ProvisioningException("transactionId mismatch");
+      }
+      return response;
+    } catch (InterruptedException | ExecutionException | TimeoutException e) {
+      mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
+      throw new ProvisioningException(e.toString());
+    }
+  }
+
+  private String findSubscribeLink(String response) throws ProvisioningException {
+    Spanned doc = Html.fromHtml(response, Html.FROM_HTML_MODE_LEGACY);
+    URLSpan[] spans = doc.getSpans(0, doc.length(), URLSpan.class);
+    StringBuilder fulltext = new StringBuilder();
+    for (URLSpan span : spans) {
+      String text = doc.subSequence(doc.getSpanStart(span), doc.getSpanEnd(span)).toString();
+      if (BASIC_SUBSCRIBE_LINK_TEXT.equals(text)) {
+        return span.getURL();
+      }
+      fulltext.append(text);
+    }
+    throw new ProvisioningException("Subscribe link not found: " + fulltext);
+  }
+
+  private String createTransactionId() {
+    return String.valueOf(Math.abs(new Random().nextLong()));
+  }
+
+  private String extractText(String xml, String tag) throws ProvisioningException {
+    Pattern pattern = Pattern.compile("<" + tag + ">(.*)<\\/" + tag + ">");
+    Matcher matcher = pattern.matcher(xml);
+    if (matcher.find()) {
+      return matcher.group(1);
+    }
+    throw new ProvisioningException("Tag " + tag + " not found in xml response");
+  }
+
+  private static class NetworkSpecifiedHurlStack extends HurlStack {
+
+    private final Network mNetwork;
+
+    public NetworkSpecifiedHurlStack(Network network) {
+      mNetwork = network;
+    }
+
+    @Override
+    protected HttpURLConnection createConnection(URL url) throws IOException {
+      return (HttpURLConnection) mNetwork.openConnection(url);
+    }
+  }
+}
diff --git a/java/com/android/voicemailomtp/res/layout/voicemail_change_pin.xml b/java/com/android/voicemail/impl/res/layout/voicemail_change_pin.xml
similarity index 98%
rename from java/com/android/voicemailomtp/res/layout/voicemail_change_pin.xml
rename to java/com/android/voicemail/impl/res/layout/voicemail_change_pin.xml
index b0db64b..50c9277 100644
--- a/java/com/android/voicemailomtp/res/layout/voicemail_change_pin.xml
+++ b/java/com/android/voicemail/impl/res/layout/voicemail_change_pin.xml
@@ -46,7 +46,7 @@
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:gravity="center"
-      android:lines="2" />
+      android:lines="2"/>
 
     <!-- error text ('PIN too short') -->
     <TextView
diff --git a/java/com/android/voicemailomtp/res/values/arrays.xml b/java/com/android/voicemail/impl/res/values/arrays.xml
similarity index 100%
rename from java/com/android/voicemailomtp/res/values/arrays.xml
rename to java/com/android/voicemail/impl/res/values/arrays.xml
diff --git a/java/com/android/voicemailomtp/res/values/attrs.xml b/java/com/android/voicemail/impl/res/values/attrs.xml
similarity index 91%
rename from java/com/android/voicemailomtp/res/values/attrs.xml
rename to java/com/android/voicemail/impl/res/values/attrs.xml
index d1c7329..a1195c7 100644
--- a/java/com/android/voicemailomtp/res/values/attrs.xml
+++ b/java/com/android/voicemail/impl/res/values/attrs.xml
@@ -16,5 +16,5 @@
 
 <resources>
 
-    <attr name="preferenceBackgroundColor" format="color" />
+  <attr name="preferenceBackgroundColor" format="color"/>
 </resources>
diff --git a/java/com/android/voicemailomtp/res/values/colors.xml b/java/com/android/voicemail/impl/res/values/colors.xml
similarity index 100%
rename from java/com/android/voicemailomtp/res/values/colors.xml
rename to java/com/android/voicemail/impl/res/values/colors.xml
diff --git a/java/com/android/voicemailomtp/res/values/config.xml b/java/com/android/voicemail/impl/res/values/config.xml
similarity index 100%
rename from java/com/android/voicemailomtp/res/values/config.xml
rename to java/com/android/voicemail/impl/res/values/config.xml
diff --git a/java/com/android/voicemailomtp/res/values/dimens.xml b/java/com/android/voicemail/impl/res/values/dimens.xml
similarity index 100%
rename from java/com/android/voicemailomtp/res/values/dimens.xml
rename to java/com/android/voicemail/impl/res/values/dimens.xml
diff --git a/java/com/android/voicemailomtp/res/values/ids.xml b/java/com/android/voicemail/impl/res/values/ids.xml
similarity index 100%
rename from java/com/android/voicemailomtp/res/values/ids.xml
rename to java/com/android/voicemail/impl/res/values/ids.xml
diff --git a/java/com/android/voicemail/impl/res/values/strings.xml b/java/com/android/voicemail/impl/res/values/strings.xml
new file mode 100644
index 0000000..6c3d552
--- /dev/null
+++ b/java/com/android/voicemail/impl/res/values/strings.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+  <!-- Title of the "Voicemail" settings screen, with a text label identifying which SIM the settings are for. -->
+  <string translatable="false" name="voicemail_settings_with_label">Voicemail (<xliff:g id="subscriptionlabel" example="Mock Carrier">%s</xliff:g>)</string>
+
+  <!-- Call settings screen, setting option name -->
+  <string translatable="false" name="voicemail_settings_title">Voicemail</string>
+
+  <!-- DO NOT TRANSLATE. Internal key for a voicemail notification preference. -->
+  <string translatable="false" name="voicemail_notification_ringtone_key">voicemail_notification_ringtone_key</string>
+  <!-- DO NOT TRANSLATE. Internal key for a voicemail notification preference. -->
+  <string translatable="false" name="voicemail_notification_vibrate_key">voicemail_notification_vibrate_key</string>
+
+  <!-- Title for the vibration settings for voicemail notifications [CHAR LIMIT=40] -->
+  <string name="voicemail_notification_vibrate_when_title">Vibrate</string>
+  <!-- Dialog title for the vibration settings for voice mail notifications [CHAR LIMIT=40]-->
+  <string name="voicemail_notification_vibarte_when_dialog_title">Vibrate</string>
+
+  <!-- Voicemail ringtone title. The user clicks on this preference to select
+         which sound to play when a voicemail notification is received.
+         [CHAR LIMIT=30] -->
+  <string name="voicemail_notification_ringtone_title">Sound</string>
+  <string translatable="false" name="voicemail_advanced_settings_key">voicemail_advanced_settings_key</string>
+
+  <!-- Title for advanced settings in the voicemail settings -->
+  <string name="voicemail_advanced_settings_title">Advanced Settings</string>
+
+  <!-- DO NOT TRANSLATE. Internal key for a visual voicemail preference. -->
+    <string translatable="false" name="voicemail_visual_voicemail_key">
+        voicemail_visual_voicemail_key
+    </string>
+  <!-- DO NOT TRANSLATE. Internal key for a visual voicemail archive preference. -->
+    <string translatable="false" name="voicemail_visual_voicemail_archive_key">
+        archive_is_enabled
+    </string>
+  <!-- DO NOT TRANSLATE. Internal key for a voicemail change pin preference. -->
+  <string translatable="false" name="voicemail_change_pin_key">voicemail_change_pin_key</string>
+
+  <!-- Visual voicemail on/off title [CHAR LIMIT=40] -->
+  <string translatable="false" name="voicemail_visual_voicemail_switch_title">Visual Voicemail</string>
+
+  <!-- Visual voicemail archive on/off title [CHAR LIMIT=40] -->
+  <string translatable="false" name="voicemail_visual_voicemail_auto_archive_switch_title">
+    Voicemail Auto Archive
+  </string>
+
+  <!-- Voicemail change PIN dialog title [CHAR LIMIT=40] -->
+  <string translatable="false" name="voicemail_set_pin_dialog_title">Set PIN</string>
+  <!-- Voicemail change PIN dialog title [CHAR LIMIT=40] -->
+  <string translatable="false" name="voicemail_change_pin_dialog_title">Change PIN</string>
+
+  <!-- Hint for the old PIN field in the change vociemail PIN dialog -->
+  <string translatable="false" name="vm_change_pin_old_pin">Old PIN</string>
+  <!-- Hint for the new PIN field in the change vociemail PIN dialog -->
+  <string translatable="false" name="vm_change_pin_new_pin">New PIN</string>
+
+  <!-- Message on the dialog when PIN changing is in progress -->
+  <string translatable="false" name="vm_change_pin_progress_message">Please wait.</string>
+  <!-- Error message for the voicemail PIN change if the PIN is too short -->
+  <string translatable="false" name="vm_change_pin_error_too_short">The new PIN is too short.</string>
+  <!-- Error message for the voicemail PIN change if the PIN is too long -->
+  <string translatable="false" name="vm_change_pin_error_too_long">The new PIN is too long.</string>
+  <!-- Error message for the voicemail PIN change if the PIN is too weak -->
+  <string translatable="false" name="vm_change_pin_error_too_weak">The new PIN is too weak. A strong password should not have continuous sequence or repeated digits.</string>
+  <!-- Error message for the voicemail PIN change if the old PIN entered doesn't match  -->
+  <string translatable="false" name="vm_change_pin_error_mismatch">The old PIN does not match.</string>
+  <!-- Error message for the voicemail PIN change if the new PIN contains invalid character -->
+  <string translatable="false" name="vm_change_pin_error_invalid">The new PIN contains invalid characters.</string>
+  <!-- Error message for the voicemail PIN change if operation has failed -->
+  <string translatable="false" name="vm_change_pin_error_system_error">Unable to change PIN</string>
+  <!-- Message to replace the transcription if a visual voicemail message is not supported-->
+  <string translatable="false" name="vvm_unsupported_message_format">Unsupported message type, call <xliff:g id="number" example="*86">%s</xliff:g> to listen.</string>
+
+  <!-- The title for the change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_title">Change Voicemail PIN</string>
+  <!-- The label for the continue button in change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_continue_label">Continue</string>
+  <!-- The label for the cancel button in change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_cancel_label">Cancel</string>
+  <!-- The label for the ok button in change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_ok_label">Ok</string>
+  <!-- The title for the enter old pin step in change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_enter_old_pin_header">Confirm your old PIN</string>
+  <!-- The hint for the enter old pin step in change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_enter_old_pin_hint">Enter your voicemail PIN to continue.</string>
+  <!-- The title for the enter new pin step in change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_enter_new_pin_header">Set a new PIN</string>
+  <!-- The hint for the enter new pin step in change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_enter_new_pin_hint">PIN must be <xliff:g id="min" example="4">%1$d</xliff:g>-<xliff:g id="max" example="7">%2$d</xliff:g> digits.</string>
+  <!-- The title for the confirm new pin step in change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_confirm_pin_header">Confirm your PIN</string>
+  <!-- The error message for th confirm new pin step in change voicemail PIN activity, if the pin doen't match the one previously entered -->
+  <string translatable="false" name="change_pin_confirm_pins_dont_match">PINs don\'t match</string>
+  <!-- The toast to show after the voicemail PIN has been successfully changed -->
+  <string translatable="false" name="change_pin_succeeded">Voicemail PIN updated</string>
+  <!-- The error message to show if the server reported an error while attempting to change the voicemail PIN -->
+  <string translatable="false" name="change_pin_system_error">Unable to set PIN</string>
+</resources>
diff --git a/java/com/android/voicemailomtp/res/values/styles.xml b/java/com/android/voicemail/impl/res/values/styles.xml
similarity index 100%
rename from java/com/android/voicemailomtp/res/values/styles.xml
rename to java/com/android/voicemail/impl/res/values/styles.xml
diff --git a/java/com/android/voicemail/impl/res/xml/voicemail_settings.xml b/java/com/android/voicemail/impl/res/xml/voicemail_settings.xml
new file mode 100644
index 0000000..2243733
--- /dev/null
+++ b/java/com/android/voicemail/impl/res/xml/voicemail_settings.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+  android:title="@string/voicemail_settings_title">
+
+  <com.android.voicemail.impl.settings.VoicemailRingtonePreference
+    android:key="@string/voicemail_notification_ringtone_key"
+    android:title="@string/voicemail_notification_ringtone_title"
+    android:persistent="false"
+    android:ringtoneType="notification" />
+
+  <CheckBoxPreference
+    android:key="@string/voicemail_notification_vibrate_key"
+    android:title="@string/voicemail_notification_vibrate_when_title"
+    android:persistent="true" />
+
+  <SwitchPreference
+    android:key="@string/voicemail_visual_voicemail_key"
+    android:title="@string/voicemail_visual_voicemail_switch_title"/>"
+
+  <SwitchPreference
+    android:key="@string/voicemail_visual_voicemail_archive_key"
+    android:dependency="@string/voicemail_visual_voicemail_key"
+    android:title="@string/voicemail_visual_voicemail_auto_archive_switch_title"/>"
+  <Preference
+    android:key="@string/voicemail_change_pin_key"
+    android:title="@string/voicemail_change_pin_dialog_title"/>
+
+  <PreferenceScreen
+    android:key="@string/voicemail_advanced_settings_key"
+    android:title="@string/voicemail_advanced_settings_title">
+    </PreferenceScreen>
+</PreferenceScreen>
diff --git a/java/com/android/voicemailomtp/res/xml/vvm_config.xml b/java/com/android/voicemail/impl/res/xml/vvm_config.xml
similarity index 89%
rename from java/com/android/voicemailomtp/res/xml/vvm_config.xml
rename to java/com/android/voicemail/impl/res/xml/vvm_config.xml
index 19c667e..230d40f 100644
--- a/java/com/android/voicemailomtp/res/xml/vvm_config.xml
+++ b/java/com/android/voicemail/impl/res/xml/vvm_config.xml
@@ -29,13 +29,17 @@
       <item value="20802"/>
     </string-array>
 
-    <int name="vvm_port_number_int" value="20481"/>
+    <int
+      name="vvm_port_number_int"
+      value="20481"/>
     <string name="vvm_destination_number_string">21101</string>
     <string-array name="carrier_vvm_package_name_string_array">
       <item value="com.orange.vvm"/>
     </string-array>
     <string name="vvm_type_string">vvm_type_omtp</string>
-    <boolean name="vvm_cellular_data_required_bool" value="true"/>
+    <boolean
+      name="vvm_cellular_data_required_bool"
+      value="true"/>
     <string-array name="vvm_disabled_capabilities_string_array">
       <!-- b/32365569 -->
       <item value="STARTTLS"/>
@@ -64,8 +68,12 @@
       <item value="310800"/>
     </string-array>
 
-    <int name="vvm_port_number_int" value="1808"/>
-    <int name="vvm_ssl_port_number_int" value="993"/>
+    <int
+      name="vvm_port_number_int"
+      value="1808"/>
+    <int
+      name="vvm_ssl_port_number_int"
+      value="993"/>
     <string name="vvm_destination_number_string">122</string>
     <string-array name="carrier_vvm_package_name_string_array">
       <item value="com.tmobile.vvm.application"/>
@@ -121,12 +129,18 @@
       <item value="311489"/>
     </string-array>
 
-    <int name="vvm_port_number_int" value="0"/>
+    <int
+      name="vvm_port_number_int"
+      value="0"/>
     <string name="vvm_destination_number_string">900080006200</string>
     <string name="vvm_type_string">vvm_type_vvm3</string>
     <string name="vvm_client_prefix_string">//VZWVVM</string>
-    <boolean name="vvm_cellular_data_required_bool" value="true"/>
-    <boolean name="vvm_legacy_mode_enabled_bool" value="true"/>
+    <boolean
+      name="vvm_cellular_data_required_bool"
+      value="true"/>
+    <boolean
+      name="vvm_legacy_mode_enabled_bool"
+      value="true"/>
     <!-- VVM3 specific value for the voicemail management gateway to use if the SMS didn't provide
          one -->
     <string name="default_vmg_url">https://mobile.vzw.com/VMGIMS/VMServices</string>
diff --git a/java/com/android/voicemail/impl/scheduling/BaseTask.java b/java/com/android/voicemail/impl/scheduling/BaseTask.java
new file mode 100644
index 0000000..4cc6dd5
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/BaseTask.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemClock;
+import android.support.annotation.CallSuper;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.NeededForTesting;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides common utilities for task implementations, such as execution time and managing {@link
+ * Policy}
+ */
+public abstract class BaseTask implements Task {
+
+  private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+
+  private Context mContext;
+
+  private int mId;
+  private PhoneAccountHandle mPhoneAccountHandle;
+
+  private boolean mHasStarted;
+  private volatile boolean mHasFailed;
+
+  @NonNull private final List<Policy> mPolicies = new ArrayList<>();
+
+  private long mExecutionTime;
+
+  private static Clock sClock = new Clock();
+
+  protected BaseTask(int id) {
+    mId = id;
+    mExecutionTime = getTimeMillis();
+  }
+
+  /**
+   * Modify the task ID to prevent arbitrary task from executing. Can only be called before {@link
+   * #onCreate(Context, Intent, int, int)} returns.
+   */
+  @MainThread
+  public void setId(int id) {
+    Assert.isMainThread();
+    mId = id;
+  }
+
+  @MainThread
+  public boolean hasStarted() {
+    Assert.isMainThread();
+    return mHasStarted;
+  }
+
+  @MainThread
+  public boolean hasFailed() {
+    Assert.isMainThread();
+    return mHasFailed;
+  }
+
+  public Context getContext() {
+    return mContext;
+  }
+
+  public PhoneAccountHandle getPhoneAccountHandle() {
+    return mPhoneAccountHandle;
+  }
+  /**
+   * Should be call in the constructor or {@link Policy#onCreate(BaseTask, Intent, int, int)} will
+   * be missed.
+   */
+  @MainThread
+  public BaseTask addPolicy(Policy policy) {
+    Assert.isMainThread();
+    mPolicies.add(policy);
+    return this;
+  }
+
+  /**
+   * Indicate the task has failed. {@link Policy#onFail()} will be triggered once the execution
+   * ends. This mechanism is used by policies for actions such as determining whether to schedule a
+   * retry. Must be call inside {@link #onExecuteInBackgroundThread()}
+   */
+  @WorkerThread
+  public void fail() {
+    Assert.isNotMainThread();
+    mHasFailed = true;
+  }
+
+  @MainThread
+  public void setExecutionTime(long timeMillis) {
+    Assert.isMainThread();
+    mExecutionTime = timeMillis;
+  }
+
+  public long getTimeMillis() {
+    return sClock.getTimeMillis();
+  }
+
+  /**
+   * Creates an intent that can be used to restart the current task. Derived class should build
+   * their intent upon this.
+   */
+  public Intent createRestartIntent() {
+    return createIntent(getContext(), this.getClass(), mPhoneAccountHandle);
+  }
+
+  /**
+   * Creates an intent that can be used to start the {@link TaskSchedulerService}. Derived class
+   * should build their intent upon this.
+   */
+  public static Intent createIntent(
+      Context context, Class<? extends BaseTask> task, PhoneAccountHandle phoneAccountHandle) {
+    Intent intent = TaskSchedulerService.createIntent(context, task);
+    intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+    return intent;
+  }
+
+  @Override
+  public TaskId getId() {
+    return new TaskId(mId, mPhoneAccountHandle);
+  }
+
+  @Override
+  @CallSuper
+  public void onCreate(Context context, Intent intent, int flags, int startId) {
+    mContext = context;
+    mPhoneAccountHandle = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+    for (Policy policy : mPolicies) {
+      policy.onCreate(this, intent, flags, startId);
+    }
+  }
+
+  @Override
+  public long getReadyInMilliSeconds() {
+    return mExecutionTime - getTimeMillis();
+  }
+
+  @Override
+  @CallSuper
+  public void onBeforeExecute() {
+    for (Policy policy : mPolicies) {
+      policy.onBeforeExecute();
+    }
+    mHasStarted = true;
+  }
+
+  @Override
+  @CallSuper
+  public void onCompleted() {
+    if (mHasFailed) {
+      for (Policy policy : mPolicies) {
+        policy.onFail();
+      }
+    }
+
+    for (Policy policy : mPolicies) {
+      policy.onCompleted();
+    }
+  }
+
+  @Override
+  public void onDuplicatedTaskAdded(Task task) {
+    for (Policy policy : mPolicies) {
+      policy.onDuplicatedTaskAdded();
+    }
+  }
+
+  @NeededForTesting
+  static class Clock {
+
+    public long getTimeMillis() {
+      return SystemClock.elapsedRealtime();
+    }
+  }
+
+  /** Used to replace the clock with an deterministic clock */
+  @NeededForTesting
+  static void setClockForTesting(Clock clock) {
+    sClock = clock;
+  }
+}
diff --git a/java/com/android/voicemail/impl/scheduling/BlockerTask.java b/java/com/android/voicemail/impl/scheduling/BlockerTask.java
new file mode 100644
index 0000000..353508d
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/BlockerTask.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Context;
+import android.content.Intent;
+import com.android.voicemail.impl.VvmLog;
+
+/** Task to block another task of the same ID from being queued for a certain amount of time. */
+public class BlockerTask extends BaseTask {
+
+  private static final String TAG = "BlockerTask";
+
+  public static final String EXTRA_TASK_ID = "extra_task_id";
+  public static final String EXTRA_BLOCK_FOR_MILLIS = "extra_block_for_millis";
+
+  public BlockerTask() {
+    super(TASK_INVALID);
+  }
+
+  @Override
+  public void onCreate(Context context, Intent intent, int flags, int startId) {
+    super.onCreate(context, intent, flags, startId);
+    setId(intent.getIntExtra(EXTRA_TASK_ID, TASK_INVALID));
+    setExecutionTime(getTimeMillis() + intent.getIntExtra(EXTRA_BLOCK_FOR_MILLIS, 0));
+  }
+
+  @Override
+  public void onExecuteInBackgroundThread() {
+    // Do nothing.
+  }
+
+  @Override
+  public void onDuplicatedTaskAdded(Task task) {
+    VvmLog.v(TAG, task.toString() + "blocked, " + getReadyInMilliSeconds() + "millis remaining");
+  }
+}
diff --git a/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java b/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java
new file mode 100644
index 0000000..8b2fe70
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Intent;
+import com.android.voicemail.impl.scheduling.Task.TaskId;
+
+/**
+ * If a task with this policy succeeds, a {@link BlockerTask} with the same {@link TaskId} of the
+ * task will be queued immediately, preventing the same task from running for a certain amount of
+ * time.
+ */
+public class MinimalIntervalPolicy implements Policy {
+
+  BaseTask mTask;
+  TaskId mId;
+  int mBlockForMillis;
+
+  public MinimalIntervalPolicy(int blockForMillis) {
+    mBlockForMillis = blockForMillis;
+  }
+
+  @Override
+  public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+    mTask = task;
+    mId = mTask.getId();
+  }
+
+  @Override
+  public void onBeforeExecute() {}
+
+  @Override
+  public void onCompleted() {
+    if (!mTask.hasFailed()) {
+      Intent intent =
+          mTask.createIntent(mTask.getContext(), BlockerTask.class, mId.phoneAccountHandle);
+      intent.putExtra(BlockerTask.EXTRA_TASK_ID, mId.id);
+      intent.putExtra(BlockerTask.EXTRA_BLOCK_FOR_MILLIS, mBlockForMillis);
+      mTask.getContext().startService(intent);
+    }
+  }
+
+  @Override
+  public void onFail() {}
+
+  @Override
+  public void onDuplicatedTaskAdded() {}
+}
diff --git a/java/com/android/voicemailomtp/scheduling/Policy.java b/java/com/android/voicemail/impl/scheduling/Policy.java
similarity index 78%
rename from java/com/android/voicemailomtp/scheduling/Policy.java
rename to java/com/android/voicemail/impl/scheduling/Policy.java
index 4a475d2..6077821 100644
--- a/java/com/android/voicemailomtp/scheduling/Policy.java
+++ b/java/com/android/voicemail/impl/scheduling/Policy.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.voicemailomtp.scheduling;
+package com.android.voicemail.impl.scheduling;
 
 import android.content.Intent;
 
@@ -24,13 +24,13 @@
  */
 public interface Policy {
 
-    void onCreate(BaseTask task, Intent intent, int flags, int startId);
+  void onCreate(BaseTask task, Intent intent, int flags, int startId);
 
-    void onBeforeExecute();
+  void onBeforeExecute();
 
-    void onCompleted();
+  void onCompleted();
 
-    void onFail();
+  void onFail();
 
-    void onDuplicatedTaskAdded();
+  void onDuplicatedTaskAdded();
 }
diff --git a/java/com/android/voicemail/impl/scheduling/PostponePolicy.java b/java/com/android/voicemail/impl/scheduling/PostponePolicy.java
new file mode 100644
index 0000000..e24df0c
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/PostponePolicy.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Intent;
+import com.android.voicemail.impl.VvmLog;
+
+/**
+ * A task with Postpone policy will not be executed immediately. It will wait for a while and if a
+ * duplicated task is queued during the duration, the task will be postponed further. The task will
+ * only be executed if no new task was added in postponeMillis. Useful to batch small tasks in quick
+ * succession together.
+ */
+public class PostponePolicy implements Policy {
+
+  private static final String TAG = "PostponePolicy";
+
+  private final int mPostponeMillis;
+  private BaseTask mTask;
+
+  public PostponePolicy(int postponeMillis) {
+    mPostponeMillis = postponeMillis;
+  }
+
+  @Override
+  public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+    mTask = task;
+    mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis);
+  }
+
+  @Override
+  public void onBeforeExecute() {
+    // Do nothing
+  }
+
+  @Override
+  public void onCompleted() {
+    // Do nothing
+  }
+
+  @Override
+  public void onFail() {
+    // Do nothing
+  }
+
+  @Override
+  public void onDuplicatedTaskAdded() {
+    if (mTask.hasStarted()) {
+      return;
+    }
+    VvmLog.d(TAG, "postponing " + mTask);
+    mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis);
+  }
+}
diff --git a/java/com/android/voicemail/impl/scheduling/RetryPolicy.java b/java/com/android/voicemail/impl/scheduling/RetryPolicy.java
new file mode 100644
index 0000000..a8e4a3d
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/RetryPolicy.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+
+/**
+ * A task with this policy will automatically re-queue itself if {@link BaseTask#fail()} has been
+ * called during {@link BaseTask#onExecuteInBackgroundThread()}. A task will be retried at most
+ * <code>retryLimit</code> times and with a <code>retryDelayMillis</code> interval in between.
+ */
+public class RetryPolicy implements Policy {
+
+  private static final String TAG = "RetryPolicy";
+  private static final String EXTRA_RETRY_COUNT = "extra_retry_count";
+
+  private final int mRetryLimit;
+  private final int mRetryDelayMillis;
+
+  private BaseTask mTask;
+
+  private int mRetryCount;
+  private boolean mFailed;
+
+  private VoicemailStatus.DeferredEditor mVoicemailStatusEditor;
+
+  public RetryPolicy(int retryLimit, int retryDelayMillis) {
+    mRetryLimit = retryLimit;
+    mRetryDelayMillis = retryDelayMillis;
+  }
+
+  private boolean hasMoreRetries() {
+    return mRetryCount < mRetryLimit;
+  }
+
+  /**
+   * Error status should only be set if retries has exhausted or the task is successful. Status
+   * writes to this editor will be deferred until the task has ended, and will only be committed if
+   * the task is successful or there are no retries left.
+   */
+  public VoicemailStatus.Editor getVoicemailStatusEditor() {
+    return mVoicemailStatusEditor;
+  }
+
+  @Override
+  public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+    mTask = task;
+    mRetryCount = intent.getIntExtra(EXTRA_RETRY_COUNT, 0);
+    if (mRetryCount > 0) {
+      VvmLog.d(
+          TAG,
+          "retry #" + mRetryCount + " for " + mTask + " queued, executing in " + mRetryDelayMillis);
+      mTask.setExecutionTime(mTask.getTimeMillis() + mRetryDelayMillis);
+    }
+    PhoneAccountHandle phoneAccountHandle = task.getPhoneAccountHandle();
+    if (phoneAccountHandle == null) {
+      VvmLog.e(TAG, "null phone account for phoneAccountHandle " + task.getPhoneAccountHandle());
+      // This should never happen, but continue on if it does. The status write will be
+      // discarded.
+    }
+    mVoicemailStatusEditor = VoicemailStatus.deferredEdit(task.getContext(), phoneAccountHandle);
+  }
+
+  @Override
+  public void onBeforeExecute() {}
+
+  @Override
+  public void onCompleted() {
+    if (!mFailed || !hasMoreRetries()) {
+      if (!mFailed) {
+        VvmLog.d(TAG, mTask.toString() + " completed successfully");
+      }
+      if (!hasMoreRetries()) {
+        VvmLog.d(TAG, "Retry limit for " + mTask + " reached");
+      }
+      VvmLog.i(TAG, "committing deferred status: " + mVoicemailStatusEditor.getValues());
+      mVoicemailStatusEditor.deferredApply();
+      return;
+    }
+    VvmLog.i(TAG, "discarding deferred status: " + mVoicemailStatusEditor.getValues());
+    Intent intent = mTask.createRestartIntent();
+    intent.putExtra(EXTRA_RETRY_COUNT, mRetryCount + 1);
+
+    mTask.getContext().startService(intent);
+  }
+
+  @Override
+  public void onFail() {
+    mFailed = true;
+  }
+
+  @Override
+  public void onDuplicatedTaskAdded() {}
+}
diff --git a/java/com/android/voicemail/impl/scheduling/Task.java b/java/com/android/voicemail/impl/scheduling/Task.java
new file mode 100644
index 0000000..2d08f5b
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/Task.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.MainThread;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import java.util.Objects;
+
+/**
+ * A task for {@link TaskSchedulerService} to execute. Since the task is sent through a intent to
+ * the scheduler, The task must be constructable with the intent. Specifically, It must have a
+ * constructor with zero arguments, and have all relevant data packed inside the intent. Use {@link
+ * TaskSchedulerService#createIntent(Context, Class)} to create a intent that will construct the
+ * Task.
+ *
+ * <p>Only {@link #onExecuteInBackgroundThread()} is run on the worker thread.
+ */
+public interface Task {
+
+  /**
+   * TaskId to indicate it has not be set. If a task does not provide a default TaskId it should be
+   * set before {@link Task#onCreate(Context, Intent, int, int) returns}
+   */
+  int TASK_INVALID = -1;
+
+  /**
+   * TaskId to indicate it should always be queued regardless of duplicates. {@link
+   * Task#onDuplicatedTaskAdded(Task)} will never be called on tasks with this TaskId.
+   */
+  int TASK_ALLOW_DUPLICATES = -2;
+
+  int TASK_UPLOAD = 1;
+  int TASK_SYNC = 2;
+  int TASK_ACTIVATION = 3;
+
+  /**
+   * Used to differentiate between types of tasks. If a task with the same TaskId is already in the
+   * queue the new task will be rejected.
+   */
+  class TaskId {
+
+    /** Indicates the operation type of the task. */
+    public final int id;
+    /**
+     * Same operation for a different phoneAccountHandle is allowed. phoneAccountHandle is used to
+     * differentiate phone accounts in multi-SIM scenario. For example, each SIM can queue a sync
+     * task for their own.
+     */
+    public final PhoneAccountHandle phoneAccountHandle;
+
+    public TaskId(int id, PhoneAccountHandle phoneAccountHandle) {
+      this.id = id;
+      this.phoneAccountHandle = phoneAccountHandle;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+      if (!(object instanceof TaskId)) {
+        return false;
+      }
+      TaskId other = (TaskId) object;
+      return id == other.id && phoneAccountHandle.equals(other.phoneAccountHandle);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(id, phoneAccountHandle);
+    }
+  }
+
+  TaskId getId();
+
+  @MainThread
+  void onCreate(Context context, Intent intent, int flags, int startId);
+
+  /**
+   * @return number of milliSeconds the scheduler should wait before running this task. A value less
+   *     than {@link TaskSchedulerService#READY_TOLERANCE_MILLISECONDS} will be considered ready. If
+   *     no tasks are ready, the scheduler will sleep for this amount of time before doing another
+   *     check (it will still wake if a new task is added). The first task in the queue that is
+   *     ready will be executed.
+   */
+  @MainThread
+  long getReadyInMilliSeconds();
+
+  /**
+   * Called on the main thread when the scheduler is about to send the task into the worker thread,
+   * calling {@link #onExecuteInBackgroundThread()}
+   */
+  @MainThread
+  void onBeforeExecute();
+
+  /** The actual payload of the task, executed on the worker thread. */
+  @WorkerThread
+  void onExecuteInBackgroundThread();
+
+  /**
+   * Called on the main thread when {@link #onExecuteInBackgroundThread()} has finished or thrown an
+   * uncaught exception. The task is already removed from the queue at this point, and a same task
+   * can be queued again.
+   */
+  @MainThread
+  void onCompleted();
+
+  /**
+   * Another task with the same TaskId has been added. Necessary data can be retrieved from the
+   * other task, and after this returns the task will be discarded.
+   */
+  @MainThread
+  void onDuplicatedTaskAdded(Task task);
+}
diff --git a/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java b/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java
new file mode 100644
index 0000000..81bd36f
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.NeededForTesting;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.scheduling.Task.TaskId;
+import java.util.ArrayDeque;
+import java.util.Queue;
+
+/**
+ * A service to queue and run {@link Task} on a worker thread. Only one task will be ran at a time,
+ * and same task cannot exist in the queue at the same time. The service will be started when a
+ * intent is received, and stopped when there are no more tasks in the queue.
+ */
+public class TaskSchedulerService extends Service {
+
+  private static final String TAG = "VvmTaskScheduler";
+
+  private static final String ACTION_WAKEUP = "action_wakeup";
+
+  private static final int READY_TOLERANCE_MILLISECONDS = 100;
+
+  /**
+   * Threshold to determine whether to do a short or long sleep when a task is scheduled in the
+   * future.
+   *
+   * <p>A short sleep will continue to held the wake lock and use {@link
+   * Handler#postDelayed(Runnable, long)} to wait for the next task.
+   *
+   * <p>A long sleep will release the wake lock and set a {@link AlarmManager} alarm. The alarm is
+   * exact and will wake up the device. Note: as this service is run in the telephony process it
+   * does not seem to be restricted by doze or sleep, it will fire exactly at the moment. The
+   * unbundled version should take doze into account.
+   */
+  private static final int SHORT_SLEEP_THRESHOLD_MILLISECONDS = 60_000;
+  /**
+   * When there are no more tasks to be run the service should be stopped. But when all tasks has
+   * finished there might still be more tasks in the message queue waiting to be processed,
+   * especially the ones submitted in {@link Task#onCompleted()}. Wait for a while before stopping
+   * the service to make sure there are no pending messages.
+   */
+  private static final int STOP_DELAY_MILLISECONDS = 5_000;
+
+  private static final String EXTRA_CLASS_NAME = "extra_class_name";
+
+  private static final String WAKE_LOCK_TAG = "TaskSchedulerService_wakelock";
+
+  // The thread to run tasks on
+  private volatile WorkerThreadHandler mWorkerThreadHandler;
+
+  private Context mContext = this;
+  /**
+   * Used by tests to turn task handling into a single threaded process by calling {@link
+   * Handler#handleMessage(Message)} directly
+   */
+  private MessageSender mMessageSender = new MessageSender();
+
+  private MainThreadHandler mMainThreadHandler;
+
+  private WakeLock mWakeLock;
+
+  /** Main thread only, access through {@link #getTasks()} */
+  private final Queue<Task> mTasks = new ArrayDeque<>();
+
+  private boolean mWorkerThreadIsBusy = false;
+
+  private final Runnable mStopServiceWithDelay =
+      new Runnable() {
+        @Override
+        public void run() {
+          VvmLog.d(TAG, "Stopping service");
+          stopSelf();
+        }
+      };
+  /** Should attempt to run the next task when a task has finished or been added. */
+  private boolean mTaskAutoRunDisabledForTesting = false;
+
+  @VisibleForTesting
+  final class WorkerThreadHandler extends Handler {
+
+    public WorkerThreadHandler(Looper looper) {
+      super(looper);
+    }
+
+    @Override
+    @WorkerThread
+    public void handleMessage(Message msg) {
+      Assert.isNotMainThread();
+      Task task = (Task) msg.obj;
+      try {
+        VvmLog.v(TAG, "executing task " + task);
+        task.onExecuteInBackgroundThread();
+      } catch (Throwable throwable) {
+        VvmLog.e(TAG, "Exception while executing task " + task + ":", throwable);
+      }
+
+      Message schedulerMessage = mMainThreadHandler.obtainMessage();
+      schedulerMessage.obj = task;
+      mMessageSender.send(schedulerMessage);
+    }
+  }
+
+  @VisibleForTesting
+  final class MainThreadHandler extends Handler {
+
+    public MainThreadHandler(Looper looper) {
+      super(looper);
+    }
+
+    @Override
+    @MainThread
+    public void handleMessage(Message msg) {
+      Assert.isMainThread();
+      Task task = (Task) msg.obj;
+      getTasks().remove(task);
+      task.onCompleted();
+      mWorkerThreadIsBusy = false;
+      maybeRunNextTask();
+    }
+  }
+
+  @Override
+  @MainThread
+  public void onCreate() {
+    super.onCreate();
+    mWakeLock =
+        getSystemService(PowerManager.class)
+            .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
+    mWakeLock.setReferenceCounted(false);
+    HandlerThread thread = new HandlerThread("VvmTaskSchedulerService");
+    thread.start();
+
+    mWorkerThreadHandler = new WorkerThreadHandler(thread.getLooper());
+    mMainThreadHandler = new MainThreadHandler(Looper.getMainLooper());
+  }
+
+  @Override
+  public void onDestroy() {
+    mWorkerThreadHandler.getLooper().quit();
+    mWakeLock.release();
+  }
+
+  @Override
+  @MainThread
+  public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
+    Assert.isMainThread();
+    // maybeRunNextTask() will release the wakelock either by entering a long sleep or stopping
+    // the service.
+    mWakeLock.acquire();
+    if (ACTION_WAKEUP.equals(intent.getAction())) {
+      VvmLog.d(TAG, "woke up by AlarmManager");
+    } else {
+      Task task = createTask(intent, flags, startId);
+      if (task == null) {
+        VvmLog.e(TAG, "cannot create task form intent");
+      } else {
+        addTask(task);
+      }
+    }
+    maybeRunNextTask();
+    // STICKY means the service will be automatically restarted will the last intent if it is
+    // killed.
+    return START_NOT_STICKY;
+  }
+
+  @MainThread
+  @VisibleForTesting
+  void addTask(Task task) {
+    Assert.isMainThread();
+    if (task.getId().id == Task.TASK_INVALID) {
+      throw new AssertionError("Task id was not set to a valid value before adding.");
+    }
+    if (task.getId().id != Task.TASK_ALLOW_DUPLICATES) {
+      Task oldTask = getTask(task.getId());
+      if (oldTask != null) {
+        oldTask.onDuplicatedTaskAdded(task);
+        return;
+      }
+    }
+    mMainThreadHandler.removeCallbacks(mStopServiceWithDelay);
+    getTasks().add(task);
+    maybeRunNextTask();
+  }
+
+  @MainThread
+  @Nullable
+  private Task getTask(TaskId taskId) {
+    Assert.isMainThread();
+    for (Task task : getTasks()) {
+      if (task.getId().equals(taskId)) {
+        return task;
+      }
+    }
+    return null;
+  }
+
+  @MainThread
+  private Queue<Task> getTasks() {
+    Assert.isMainThread();
+    return mTasks;
+  }
+
+  /** Create an intent that will queue the <code>task</code> */
+  public static Intent createIntent(Context context, Class<? extends Task> task) {
+    Intent intent = new Intent(context, TaskSchedulerService.class);
+    intent.putExtra(EXTRA_CLASS_NAME, task.getName());
+    return intent;
+  }
+
+  @VisibleForTesting
+  @MainThread
+  @Nullable
+  Task createTask(@Nullable Intent intent, int flags, int startId) {
+    Assert.isMainThread();
+    if (intent == null) {
+      return null;
+    }
+    String className = intent.getStringExtra(EXTRA_CLASS_NAME);
+    VvmLog.d(TAG, "create task:" + className);
+    if (className == null) {
+      throw new IllegalArgumentException("EXTRA_CLASS_NAME expected");
+    }
+    try {
+      Task task = (Task) Class.forName(className).newInstance();
+      task.onCreate(mContext, intent, flags, startId);
+      return task;
+    } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+  @MainThread
+  private void maybeRunNextTask() {
+    Assert.isMainThread();
+    if (mWorkerThreadIsBusy) {
+      return;
+    }
+    if (mTaskAutoRunDisabledForTesting) {
+      // If mTaskAutoRunDisabledForTesting is true, runNextTask() must be explicitly called
+      // to run the next task.
+      return;
+    }
+
+    runNextTask();
+  }
+
+  @VisibleForTesting
+  @MainThread
+  void runNextTask() {
+    Assert.isMainThread();
+    // The current alarm is no longer valid, a new one will be set up if required.
+    getSystemService(AlarmManager.class).cancel(getWakeupIntent());
+    if (getTasks().isEmpty()) {
+      prepareStop();
+      return;
+    }
+    Long minimalWaitTime = null;
+    for (Task task : getTasks()) {
+      long waitTime = task.getReadyInMilliSeconds();
+      if (waitTime < READY_TOLERANCE_MILLISECONDS) {
+        task.onBeforeExecute();
+        Message message = mWorkerThreadHandler.obtainMessage();
+        message.obj = task;
+        mWorkerThreadIsBusy = true;
+        mMessageSender.send(message);
+        return;
+      } else {
+        if (minimalWaitTime == null || waitTime < minimalWaitTime) {
+          minimalWaitTime = waitTime;
+        }
+      }
+    }
+    VvmLog.d(TAG, "minimal wait time:" + minimalWaitTime);
+    if (!mTaskAutoRunDisabledForTesting && minimalWaitTime != null) {
+      // No tasks are currently ready. Sleep until the next one should be.
+      // If a new task is added during the sleep the service will wake immediately.
+      sleep(minimalWaitTime);
+    }
+  }
+
+  private void sleep(long timeMillis) {
+    if (timeMillis < SHORT_SLEEP_THRESHOLD_MILLISECONDS) {
+      mMainThreadHandler.postDelayed(
+          new Runnable() {
+            @Override
+            public void run() {
+              maybeRunNextTask();
+            }
+          },
+          timeMillis);
+      return;
+    }
+
+    // Tasks does not have a strict timing requirement, use AlarmManager.set() so the OS could
+    // optimize the battery usage. As this service currently run in the telephony process the
+    // OS give it privileges to behave the same as setExact(), but set() is the targeted
+    // behavior once this is unbundled.
+    getSystemService(AlarmManager.class)
+        .set(
+            AlarmManager.ELAPSED_REALTIME_WAKEUP,
+            SystemClock.elapsedRealtime() + timeMillis,
+            getWakeupIntent());
+    mWakeLock.release();
+    VvmLog.d(TAG, "Long sleep for " + timeMillis + " millis");
+  }
+
+  private PendingIntent getWakeupIntent() {
+    Intent intent = new Intent(ACTION_WAKEUP, null, this, getClass());
+    return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+  }
+
+  private void prepareStop() {
+    VvmLog.d(
+        TAG,
+        "No more tasks, stopping service if no task are added in "
+            + STOP_DELAY_MILLISECONDS
+            + " millis");
+    mMainThreadHandler.postDelayed(mStopServiceWithDelay, STOP_DELAY_MILLISECONDS);
+  }
+
+  static class MessageSender {
+
+    public void send(Message message) {
+      message.sendToTarget();
+    }
+  }
+
+  @NeededForTesting
+  void setContextForTest(Context context) {
+    mContext = context;
+  }
+
+  @NeededForTesting
+  void setTaskAutoRunDisabledForTest(boolean value) {
+    mTaskAutoRunDisabledForTesting = value;
+  }
+
+  @NeededForTesting
+  void setMessageSenderForTest(MessageSender sender) {
+    mMessageSender = sender;
+  }
+
+  @NeededForTesting
+  void clearTasksForTest() {
+    mTasks.clear();
+  }
+
+  @Override
+  @Nullable
+  public IBinder onBind(Intent intent) {
+    return new LocalBinder();
+  }
+
+  @NeededForTesting
+  class LocalBinder extends Binder {
+
+    @NeededForTesting
+    public TaskSchedulerService getService() {
+      return TaskSchedulerService.this;
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/settings/VisualVoicemailSettingsUtil.java b/java/com/android/voicemail/impl/settings/VisualVoicemailSettingsUtil.java
new file mode 100644
index 0000000..7e4a6a7
--- /dev/null
+++ b/java/com/android/voicemail/impl/settings/VisualVoicemailSettingsUtil.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.settings;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.R;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+
+/** Save whether or not a particular account is enabled in shared to be retrieved later. */
+public class VisualVoicemailSettingsUtil {
+
+  private static final String IS_ENABLED_KEY = "is_enabled";
+  // Flag name used for configuration
+  public static final String ALLOW_VOICEMAIL_ARCHIVE = "allow_voicemail_archive";
+
+  public static void setEnabled(
+      Context context, PhoneAccountHandle phoneAccount, boolean isEnabled) {
+    new VisualVoicemailPreferences(context, phoneAccount)
+        .edit()
+        .putBoolean(IS_ENABLED_KEY, isEnabled)
+        .apply();
+    OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(context, phoneAccount);
+    if (isEnabled) {
+      config.startActivation();
+    } else {
+      VvmAccountManager.removeAccount(context, phoneAccount);
+      config.startDeactivation();
+    }
+  }
+
+  public static void setArchiveEnabled(
+      Context context, PhoneAccountHandle phoneAccount, boolean isEnabled) {
+    new VisualVoicemailPreferences(context, phoneAccount)
+        .edit()
+        .putBoolean(context.getString(R.string.voicemail_visual_voicemail_archive_key), isEnabled)
+        .apply();
+  }
+
+  public static boolean isEnabled(Context context, PhoneAccountHandle phoneAccount) {
+    if (phoneAccount == null) {
+      return false;
+    }
+
+    VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount);
+    if (prefs.contains(IS_ENABLED_KEY)) {
+      // isEnableByDefault is a bit expensive, so don't use it as default value of
+      // getBoolean(). The "false" here should never be actually used.
+      return prefs.getBoolean(IS_ENABLED_KEY, false);
+    }
+    return new OmtpVvmCarrierConfigHelper(context, phoneAccount).isEnabledByDefault();
+  }
+
+  public static boolean isArchiveEnabled(Context context, PhoneAccountHandle phoneAccount) {
+    Assert.isNotNull(phoneAccount);
+
+    VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount);
+    return prefs.getBoolean(
+        context.getString(R.string.voicemail_visual_voicemail_archive_key), false);
+  }
+
+  /**
+   * Whether the client enabled status is explicitly set by user or by default(Whether carrier VVM
+   * app is installed). This is used to determine whether to disable the client when the carrier VVM
+   * app is installed. If the carrier VVM app is installed the client should give priority to it if
+   * the settings are not touched.
+   */
+  public static boolean isEnabledUserSet(Context context, PhoneAccountHandle phoneAccount) {
+    if (phoneAccount == null) {
+      return false;
+    }
+    VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount);
+    return prefs.contains(IS_ENABLED_KEY);
+  }
+}
diff --git a/java/com/android/voicemail/impl/settings/VoicemailChangePinActivity.java b/java/com/android/voicemail/impl/settings/VoicemailChangePinActivity.java
new file mode 100644
index 0000000..f288a5b
--- /dev/null
+++ b/java/com/android/voicemail/impl/settings/VoicemailChangePinActivity.java
@@ -0,0 +1,624 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.settings;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.net.Network;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.InputFilter.LengthFilter;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+import android.widget.Toast;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpConstants.ChangePinResult;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.R;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.sync.VvmNetworkRequestCallback;
+
+/**
+ * Dialog to change the voicemail PIN. The TUI (Telephony User Interface) PIN is used when accessing
+ * traditional voicemail through phone call. The intent to launch this activity must contain {@link
+ * #EXTRA_PHONE_ACCOUNT_HANDLE}
+ */
+@TargetApi(VERSION_CODES.O)
+public class VoicemailChangePinActivity extends Activity
+    implements OnClickListener, OnEditorActionListener, TextWatcher {
+
+  private static final String TAG = "VmChangePinActivity";
+
+  public static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+
+  private static final String KEY_DEFAULT_OLD_PIN = "default_old_pin";
+
+  private static final int MESSAGE_HANDLE_RESULT = 1;
+
+  private PhoneAccountHandle mPhoneAccountHandle;
+  private OmtpVvmCarrierConfigHelper mConfig;
+
+  private int mPinMinLength;
+  private int mPinMaxLength;
+
+  private State mUiState = State.Initial;
+  private String mOldPin;
+  private String mFirstPin;
+
+  private ProgressDialog mProgressDialog;
+
+  private TextView mHeaderText;
+  private TextView mHintText;
+  private TextView mErrorText;
+  private EditText mPinEntry;
+  private Button mCancelButton;
+  private Button mNextButton;
+
+  private Handler mHandler =
+      new Handler() {
+        @Override
+        public void handleMessage(Message message) {
+          if (message.what == MESSAGE_HANDLE_RESULT) {
+            mUiState.handleResult(VoicemailChangePinActivity.this, message.arg1);
+          }
+        }
+      };
+
+  private enum State {
+    /**
+     * Empty state to handle initial state transition. Will immediately switch into {@link
+     * #VerifyOldPin} if a default PIN has been set by the OMTP client, or {@link #EnterOldPin} if
+     * not.
+     */
+    Initial,
+    /**
+     * Prompt the user to enter old PIN. The PIN will be verified with the server before proceeding
+     * to {@link #EnterNewPin}.
+     */
+    EnterOldPin {
+      @Override
+      public void onEnter(VoicemailChangePinActivity activity) {
+        activity.setHeader(R.string.change_pin_enter_old_pin_header);
+        activity.mHintText.setText(R.string.change_pin_enter_old_pin_hint);
+        activity.mNextButton.setText(R.string.change_pin_continue_label);
+        activity.mErrorText.setText(null);
+      }
+
+      @Override
+      public void onInputChanged(VoicemailChangePinActivity activity) {
+        activity.setNextEnabled(activity.getCurrentPasswordInput().length() > 0);
+      }
+
+      @Override
+      public void handleNext(VoicemailChangePinActivity activity) {
+        activity.mOldPin = activity.getCurrentPasswordInput();
+        activity.verifyOldPin();
+      }
+
+      @Override
+      public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) {
+        if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
+          activity.updateState(State.EnterNewPin);
+        } else {
+          CharSequence message = activity.getChangePinResultMessage(result);
+          activity.showError(message);
+          activity.mPinEntry.setText("");
+        }
+      }
+    },
+    /**
+     * The default old PIN is found. Show a blank screen while verifying with the server to make
+     * sure the PIN is still valid. If the PIN is still valid, proceed to {@link #EnterNewPin}. If
+     * not, the user probably changed the PIN through other means, proceed to {@link #EnterOldPin}.
+     * If any other issue caused the verifying to fail, show an error and exit.
+     */
+    VerifyOldPin {
+      @Override
+      public void onEnter(VoicemailChangePinActivity activity) {
+        activity.findViewById(android.R.id.content).setVisibility(View.INVISIBLE);
+        activity.verifyOldPin();
+      }
+
+      @Override
+      public void handleResult(
+          final VoicemailChangePinActivity activity, @ChangePinResult int result) {
+        if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
+          activity.updateState(State.EnterNewPin);
+        } else if (result == OmtpConstants.CHANGE_PIN_SYSTEM_ERROR) {
+          activity
+              .getWindow()
+              .setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
+          activity.showError(
+              activity.getString(R.string.change_pin_system_error),
+              new OnDismissListener() {
+                @Override
+                public void onDismiss(DialogInterface dialog) {
+                  activity.finish();
+                }
+              });
+        } else {
+          VvmLog.e(TAG, "invalid default old PIN: " + activity.getChangePinResultMessage(result));
+          // If the default old PIN is rejected by the server, the PIN is probably changed
+          // through other means, or the generated pin is invalid
+          // Wipe the default old PIN so the old PIN input box will be shown to the user
+          // on the next time.
+          setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null);
+          activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET);
+          activity.updateState(State.EnterOldPin);
+        }
+      }
+
+      @Override
+      public void onLeave(VoicemailChangePinActivity activity) {
+        activity.findViewById(android.R.id.content).setVisibility(View.VISIBLE);
+      }
+    },
+    /**
+     * Let the user enter the new PIN and validate the format. Only length is enforced, PIN strength
+     * check relies on the server. After a valid PIN is entered, proceed to {@link #ConfirmNewPin}
+     */
+    EnterNewPin {
+      @Override
+      public void onEnter(VoicemailChangePinActivity activity) {
+        activity.mHeaderText.setText(R.string.change_pin_enter_new_pin_header);
+        activity.mNextButton.setText(R.string.change_pin_continue_label);
+        activity.mHintText.setText(
+            activity.getString(
+                R.string.change_pin_enter_new_pin_hint,
+                activity.mPinMinLength,
+                activity.mPinMaxLength));
+      }
+
+      @Override
+      public void onInputChanged(VoicemailChangePinActivity activity) {
+        String password = activity.getCurrentPasswordInput();
+        if (password.length() == 0) {
+          activity.setNextEnabled(false);
+          return;
+        }
+        CharSequence error = activity.validatePassword(password);
+        if (error != null) {
+          activity.mErrorText.setText(error);
+          activity.setNextEnabled(false);
+        } else {
+          activity.mErrorText.setText(null);
+          activity.setNextEnabled(true);
+        }
+      }
+
+      @Override
+      public void handleNext(VoicemailChangePinActivity activity) {
+        CharSequence errorMsg;
+        errorMsg = activity.validatePassword(activity.getCurrentPasswordInput());
+        if (errorMsg != null) {
+          activity.showError(errorMsg);
+          return;
+        }
+        activity.mFirstPin = activity.getCurrentPasswordInput();
+        activity.updateState(State.ConfirmNewPin);
+      }
+    },
+    /**
+     * Let the user type in the same PIN again to avoid typos. If the PIN matches then perform a PIN
+     * change to the server. Finish the activity if succeeded. Return to {@link #EnterOldPin} if the
+     * old PIN is rejected, {@link #EnterNewPin} for other failure.
+     */
+    ConfirmNewPin {
+      @Override
+      public void onEnter(VoicemailChangePinActivity activity) {
+        activity.mHeaderText.setText(R.string.change_pin_confirm_pin_header);
+        activity.mHintText.setText(null);
+        activity.mNextButton.setText(R.string.change_pin_ok_label);
+      }
+
+      @Override
+      public void onInputChanged(VoicemailChangePinActivity activity) {
+        if (activity.getCurrentPasswordInput().length() == 0) {
+          activity.setNextEnabled(false);
+          return;
+        }
+        if (activity.getCurrentPasswordInput().equals(activity.mFirstPin)) {
+          activity.setNextEnabled(true);
+          activity.mErrorText.setText(null);
+        } else {
+          activity.setNextEnabled(false);
+          activity.mErrorText.setText(R.string.change_pin_confirm_pins_dont_match);
+        }
+      }
+
+      @Override
+      public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) {
+        if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
+          // If the PIN change succeeded we no longer know what the old (current) PIN is.
+          // Wipe the default old PIN so the old PIN input box will be shown to the user
+          // on the next time.
+          setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null);
+          activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET);
+
+          activity.finish();
+
+          Toast.makeText(
+                  activity, activity.getString(R.string.change_pin_succeeded), Toast.LENGTH_SHORT)
+              .show();
+        } else {
+          CharSequence message = activity.getChangePinResultMessage(result);
+          VvmLog.i(TAG, "Change PIN failed: " + message);
+          activity.showError(message);
+          if (result == OmtpConstants.CHANGE_PIN_MISMATCH) {
+            // Somehow the PIN has changed, prompt to enter the old PIN again.
+            activity.updateState(State.EnterOldPin);
+          } else {
+            // The new PIN failed to fulfil other restrictions imposed by the server.
+            activity.updateState(State.EnterNewPin);
+          }
+        }
+      }
+
+      @Override
+      public void handleNext(VoicemailChangePinActivity activity) {
+        activity.processPinChange(activity.mOldPin, activity.mFirstPin);
+      }
+    };
+
+    /** The activity has switched from another state to this one. */
+    public void onEnter(VoicemailChangePinActivity activity) {
+      // Do nothing
+    }
+
+    /**
+     * The user has typed something into the PIN input field. Also called after {@link
+     * #onEnter(VoicemailChangePinActivity)}
+     */
+    public void onInputChanged(VoicemailChangePinActivity activity) {
+      // Do nothing
+    }
+
+    /** The asynchronous call to change the PIN on the server has returned. */
+    public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) {
+      // Do nothing
+    }
+
+    /** The user has pressed the "next" button. */
+    public void handleNext(VoicemailChangePinActivity activity) {
+      // Do nothing
+    }
+
+    /** The activity has switched from this state to another one. */
+    public void onLeave(VoicemailChangePinActivity activity) {
+      // Do nothing
+    }
+  }
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+
+    mPhoneAccountHandle = getIntent().getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+    mConfig = new OmtpVvmCarrierConfigHelper(this, mPhoneAccountHandle);
+    setContentView(R.layout.voicemail_change_pin);
+    setTitle(R.string.change_pin_title);
+
+    readPinLength();
+
+    View view = findViewById(android.R.id.content);
+
+    mCancelButton = (Button) view.findViewById(R.id.cancel_button);
+    mCancelButton.setOnClickListener(this);
+    mNextButton = (Button) view.findViewById(R.id.next_button);
+    mNextButton.setOnClickListener(this);
+
+    mPinEntry = (EditText) view.findViewById(R.id.pin_entry);
+    mPinEntry.setOnEditorActionListener(this);
+    mPinEntry.addTextChangedListener(this);
+    if (mPinMaxLength != 0) {
+      mPinEntry.setFilters(new InputFilter[] {new LengthFilter(mPinMaxLength)});
+    }
+
+    mHeaderText = (TextView) view.findViewById(R.id.headerText);
+    mHintText = (TextView) view.findViewById(R.id.hintText);
+    mErrorText = (TextView) view.findViewById(R.id.errorText);
+
+    if (isDefaultOldPinSet(this, mPhoneAccountHandle)) {
+      mOldPin = getDefaultOldPin(this, mPhoneAccountHandle);
+      updateState(State.VerifyOldPin);
+    } else {
+      updateState(State.EnterOldPin);
+    }
+  }
+
+  private void handleOmtpEvent(OmtpEvents event) {
+    mConfig.handleEvent(getVoicemailStatusEditor(), event);
+  }
+
+  private VoicemailStatus.Editor getVoicemailStatusEditor() {
+    // This activity does not have any automatic retry mechanism, errors should be written right
+    // away.
+    return VoicemailStatus.edit(this, mPhoneAccountHandle);
+  }
+
+  /** Extracts the pin length requirement sent by the server with a STATUS SMS. */
+  private void readPinLength() {
+    VisualVoicemailPreferences preferences =
+        new VisualVoicemailPreferences(this, mPhoneAccountHandle);
+    // The OMTP pin length format is {min}-{max}
+    String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-");
+    if (lengths.length == 2) {
+      try {
+        mPinMinLength = Integer.parseInt(lengths[0]);
+        mPinMaxLength = Integer.parseInt(lengths[1]);
+      } catch (NumberFormatException e) {
+        mPinMinLength = 0;
+        mPinMaxLength = 0;
+      }
+    } else {
+      mPinMinLength = 0;
+      mPinMaxLength = 0;
+    }
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+    updateState(mUiState);
+  }
+
+  public void handleNext() {
+    if (mPinEntry.length() == 0) {
+      return;
+    }
+    mUiState.handleNext(this);
+  }
+
+  @Override
+  public void onClick(View v) {
+    if (v.getId() == R.id.next_button) {
+      handleNext();
+    } else if (v.getId() == R.id.cancel_button) {
+      finish();
+    }
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    if (item.getItemId() == android.R.id.home) {
+      onBackPressed();
+      return true;
+    }
+    return super.onOptionsItemSelected(item);
+  }
+
+  @Override
+  public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+    if (!mNextButton.isEnabled()) {
+      return true;
+    }
+    // Check if this was the result of hitting the enter or "done" key
+    if (actionId == EditorInfo.IME_NULL
+        || actionId == EditorInfo.IME_ACTION_DONE
+        || actionId == EditorInfo.IME_ACTION_NEXT) {
+      handleNext();
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public void afterTextChanged(Editable s) {
+    mUiState.onInputChanged(this);
+  }
+
+  @Override
+  public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+    // Do nothing
+  }
+
+  @Override
+  public void onTextChanged(CharSequence s, int start, int before, int count) {
+    // Do nothing
+  }
+
+  /**
+   * After replacing the default PIN with a random PIN, call this to store the random PIN. The
+   * stored PIN will be automatically entered when the user attempts to change the PIN.
+   */
+  public static void setDefaultOldPIN(
+      Context context, PhoneAccountHandle phoneAccountHandle, String pin) {
+    new VisualVoicemailPreferences(context, phoneAccountHandle)
+        .edit()
+        .putString(KEY_DEFAULT_OLD_PIN, pin)
+        .apply();
+  }
+
+  public static boolean isDefaultOldPinSet(Context context, PhoneAccountHandle phoneAccountHandle) {
+    return getDefaultOldPin(context, phoneAccountHandle) != null;
+  }
+
+  private static String getDefaultOldPin(Context context, PhoneAccountHandle phoneAccountHandle) {
+    return new VisualVoicemailPreferences(context, phoneAccountHandle)
+        .getString(KEY_DEFAULT_OLD_PIN);
+  }
+
+  private String getCurrentPasswordInput() {
+    return mPinEntry.getText().toString();
+  }
+
+  private void updateState(State state) {
+    State previousState = mUiState;
+    mUiState = state;
+    if (previousState != state) {
+      previousState.onLeave(this);
+      mPinEntry.setText("");
+      mUiState.onEnter(this);
+    }
+    mUiState.onInputChanged(this);
+  }
+
+  /**
+   * Validates PIN and returns a message to display if PIN fails test.
+   *
+   * @param password the raw password the user typed in
+   * @return error message to show to user or null if password is OK
+   */
+  private CharSequence validatePassword(String password) {
+    if (mPinMinLength == 0 && mPinMaxLength == 0) {
+      // Invalid length requirement is sent by the server, just accept anything and let the
+      // server decide.
+      return null;
+    }
+
+    if (password.length() < mPinMinLength) {
+      return getString(R.string.vm_change_pin_error_too_short);
+    }
+    return null;
+  }
+
+  private void setHeader(int text) {
+    mHeaderText.setText(text);
+    mPinEntry.setContentDescription(mHeaderText.getText());
+  }
+
+  /**
+   * Get the corresponding message for the {@link ChangePinResult}.<code>result</code> must not
+   * {@link OmtpConstants#CHANGE_PIN_SUCCESS}
+   */
+  private CharSequence getChangePinResultMessage(@ChangePinResult int result) {
+    switch (result) {
+      case OmtpConstants.CHANGE_PIN_TOO_SHORT:
+        return getString(R.string.vm_change_pin_error_too_short);
+      case OmtpConstants.CHANGE_PIN_TOO_LONG:
+        return getString(R.string.vm_change_pin_error_too_long);
+      case OmtpConstants.CHANGE_PIN_TOO_WEAK:
+        return getString(R.string.vm_change_pin_error_too_weak);
+      case OmtpConstants.CHANGE_PIN_INVALID_CHARACTER:
+        return getString(R.string.vm_change_pin_error_invalid);
+      case OmtpConstants.CHANGE_PIN_MISMATCH:
+        return getString(R.string.vm_change_pin_error_mismatch);
+      case OmtpConstants.CHANGE_PIN_SYSTEM_ERROR:
+        return getString(R.string.vm_change_pin_error_system_error);
+      default:
+        VvmLog.wtf(TAG, "Unexpected ChangePinResult " + result);
+        return null;
+    }
+  }
+
+  private void verifyOldPin() {
+    processPinChange(mOldPin, mOldPin);
+  }
+
+  private void setNextEnabled(boolean enabled) {
+    mNextButton.setEnabled(enabled);
+  }
+
+  private void showError(CharSequence message) {
+    showError(message, null);
+  }
+
+  private void showError(CharSequence message, @Nullable OnDismissListener callback) {
+    new AlertDialog.Builder(this)
+        .setMessage(message)
+        .setPositiveButton(android.R.string.ok, null)
+        .setOnDismissListener(callback)
+        .show();
+  }
+
+  /** Asynchronous call to change the PIN on the server. */
+  private void processPinChange(String oldPin, String newPin) {
+    mProgressDialog = new ProgressDialog(this);
+    mProgressDialog.setCancelable(false);
+    mProgressDialog.setMessage(getString(R.string.vm_change_pin_progress_message));
+    mProgressDialog.show();
+
+    ChangePinNetworkRequestCallback callback = new ChangePinNetworkRequestCallback(oldPin, newPin);
+    callback.requestNetwork();
+  }
+
+  private class ChangePinNetworkRequestCallback extends VvmNetworkRequestCallback {
+
+    private final String mOldPin;
+    private final String mNewPin;
+
+    public ChangePinNetworkRequestCallback(String oldPin, String newPin) {
+      super(
+          mConfig, mPhoneAccountHandle, VoicemailChangePinActivity.this.getVoicemailStatusEditor());
+      mOldPin = oldPin;
+      mNewPin = newPin;
+    }
+
+    @Override
+    public void onAvailable(Network network) {
+      super.onAvailable(network);
+      try (ImapHelper helper =
+          new ImapHelper(
+              VoicemailChangePinActivity.this,
+              mPhoneAccountHandle,
+              network,
+              getVoicemailStatusEditor())) {
+
+        @ChangePinResult int result = helper.changePin(mOldPin, mNewPin);
+        sendResult(result);
+      } catch (InitializingException | MessagingException e) {
+        VvmLog.e(TAG, "ChangePinNetworkRequestCallback: onAvailable: ", e);
+        sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR);
+      }
+    }
+
+    @Override
+    public void onFailed(String reason) {
+      super.onFailed(reason);
+      sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR);
+    }
+
+    private void sendResult(@ChangePinResult int result) {
+      VvmLog.i(TAG, "Change PIN result: " + result);
+      if (mProgressDialog.isShowing()
+          && !VoicemailChangePinActivity.this.isDestroyed()
+          && !VoicemailChangePinActivity.this.isFinishing()) {
+        mProgressDialog.dismiss();
+      } else {
+        VvmLog.i(TAG, "Dialog not visible, not dismissing");
+      }
+      mHandler.obtainMessage(MESSAGE_HANDLE_RESULT, result, 0).sendToTarget();
+      releaseNetwork();
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/settings/VoicemailRingtonePreference.java b/java/com/android/voicemail/impl/settings/VoicemailRingtonePreference.java
new file mode 100644
index 0000000..22c729c
--- /dev/null
+++ b/java/com/android/voicemail/impl/settings/VoicemailRingtonePreference.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.settings;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.RingtonePreference;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.util.AttributeSet;
+import com.android.dialer.common.Assert;
+import com.android.dialer.util.SettingsUtil;
+
+/**
+ * Looks up the voicemail ringtone's name asynchronously and updates the preference's summary when
+ * it is created or updated.
+ */
+@TargetApi(VERSION_CODES.O)
+public class VoicemailRingtonePreference extends RingtonePreference {
+
+  /** Callback when the ringtone name has been fetched. */
+  public interface VoicemailRingtoneNameChangeListener {
+    void onVoicemailRingtoneNameChanged(CharSequence name);
+  }
+
+  private static final int MSG_UPDATE_VOICEMAIL_RINGTONE_SUMMARY = 1;
+
+  private PhoneAccountHandle phoneAccountHandle;
+  private final TelephonyManager telephonyManager;
+
+  private VoicemailRingtoneNameChangeListener mVoicemailRingtoneNameChangeListener;
+  private Runnable mVoicemailRingtoneLookupRunnable;
+  private final Handler mVoicemailRingtoneLookupComplete =
+      new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+          switch (msg.what) {
+            case MSG_UPDATE_VOICEMAIL_RINGTONE_SUMMARY:
+              if (mVoicemailRingtoneNameChangeListener != null) {
+                mVoicemailRingtoneNameChangeListener.onVoicemailRingtoneNameChanged(
+                    (CharSequence) msg.obj);
+              }
+              setSummary((CharSequence) msg.obj);
+              break;
+            default:
+              Assert.fail();
+          }
+        }
+      };
+
+  public VoicemailRingtonePreference(Context context, AttributeSet attrs) {
+    super(context, attrs);
+    telephonyManager = context.getSystemService(TelephonyManager.class);
+  }
+
+  public void init(PhoneAccountHandle phoneAccountHandle, CharSequence oldRingtoneName) {
+    this.phoneAccountHandle = phoneAccountHandle;
+    setSummary(oldRingtoneName);
+    mVoicemailRingtoneLookupRunnable =
+        new Runnable() {
+          @Override
+          public void run() {
+            SettingsUtil.getRingtoneName(
+                getContext(),
+                mVoicemailRingtoneLookupComplete,
+                telephonyManager.getVoicemailRingtoneUri(phoneAccountHandle),
+                MSG_UPDATE_VOICEMAIL_RINGTONE_SUMMARY);
+          }
+        };
+
+    updateRingtoneName();
+  }
+
+  public void setVoicemailRingtoneNameChangeListener(VoicemailRingtoneNameChangeListener l) {
+    mVoicemailRingtoneNameChangeListener = l;
+  }
+
+  @Override
+  protected Uri onRestoreRingtone() {
+    return telephonyManager.getVoicemailRingtoneUri(phoneAccountHandle);
+  }
+
+  @Override
+  protected void onSaveRingtone(Uri ringtoneUri) {
+    telephonyManager.setVoicemailRingtoneUri(phoneAccountHandle, ringtoneUri);
+    updateRingtoneName();
+  }
+
+  private void updateRingtoneName() {
+    new Thread(mVoicemailRingtoneLookupRunnable).start();
+  }
+}
diff --git a/java/com/android/voicemail/impl/settings/VoicemailSettingsFragment.java b/java/com/android/voicemail/impl/settings/VoicemailSettingsFragment.java
new file mode 100644
index 0000000..a5b94a7
--- /dev/null
+++ b/java/com/android/voicemail/impl/settings/VoicemailSettingsFragment.java
@@ -0,0 +1,202 @@
+/**
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.settings;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.preference.SwitchPreference;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.R;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+
+/** Fragment for voicemail settings. */
+@TargetApi(VERSION_CODES.O)
+public class VoicemailSettingsFragment extends PreferenceFragment
+    implements Preference.OnPreferenceChangeListener,
+        VoicemailRingtonePreference.VoicemailRingtoneNameChangeListener {
+
+  private static final String TAG = "VmSettingsActivity";
+
+  private PhoneAccountHandle phoneAccountHandle;
+  private OmtpVvmCarrierConfigHelper omtpVvmCarrierConfigHelper;
+
+  private VoicemailRingtonePreference voicemailRingtonePreference;
+  private CheckBoxPreference voicemailVibration;
+  private SwitchPreference voicemailVisualVoicemail;
+  private SwitchPreference autoArchiveSwitchPreference;
+  private Preference voicemailChangePinPreference;
+  private PreferenceScreen advancedSettings;
+
+  // The ringtone name is retrieved with an async call. Cache the old name so there will be no jank
+  // during transition.
+  private CharSequence oldRingtoneName = "";
+
+  @Override
+  public void onCreate(Bundle icicle) {
+    super.onCreate(icicle);
+
+    phoneAccountHandle =
+        getContext()
+            .getSystemService(TelecomManager.class)
+            .getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
+
+    omtpVvmCarrierConfigHelper = new OmtpVvmCarrierConfigHelper(getContext(), phoneAccountHandle);
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+
+    PreferenceScreen preferenceScreen = getPreferenceScreen();
+    if (preferenceScreen != null) {
+      preferenceScreen.removeAll();
+    }
+
+    addPreferencesFromResource(R.xml.voicemail_settings);
+
+    PreferenceScreen prefSet = getPreferenceScreen();
+
+    voicemailRingtonePreference =
+        (VoicemailRingtonePreference)
+            findPreference(getString(R.string.voicemail_notification_ringtone_key));
+    voicemailRingtonePreference.setVoicemailRingtoneNameChangeListener(this);
+    voicemailRingtonePreference.init(phoneAccountHandle, oldRingtoneName);
+
+    voicemailVibration =
+        (CheckBoxPreference) findPreference(getString(R.string.voicemail_notification_vibrate_key));
+    voicemailVibration.setOnPreferenceChangeListener(this);
+    voicemailVibration.setChecked(
+        getContext()
+            .getSystemService(TelephonyManager.class)
+            .isVoicemailVibrationEnabled(phoneAccountHandle));
+
+    voicemailVisualVoicemail =
+        (SwitchPreference) findPreference(getString(R.string.voicemail_visual_voicemail_key));
+
+    autoArchiveSwitchPreference =
+        (SwitchPreference)
+            findPreference(getString(R.string.voicemail_visual_voicemail_archive_key));
+    autoArchiveSwitchPreference.setOnPreferenceChangeListener(this);
+    autoArchiveSwitchPreference.setChecked(
+        VisualVoicemailSettingsUtil.isArchiveEnabled(getContext(), phoneAccountHandle));
+
+    if (!ConfigProviderBindings.get(getContext())
+        .getBoolean(VisualVoicemailSettingsUtil.ALLOW_VOICEMAIL_ARCHIVE, true)) {
+      getPreferenceScreen().removePreference(autoArchiveSwitchPreference);
+    }
+
+    voicemailChangePinPreference = findPreference(getString(R.string.voicemail_change_pin_key));
+    Intent changePinIntent = new Intent(new Intent(getContext(), VoicemailChangePinActivity.class));
+    changePinIntent.putExtra(
+        VoicemailChangePinActivity.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+
+    voicemailChangePinPreference.setIntent(changePinIntent);
+    if (VoicemailChangePinActivity.isDefaultOldPinSet(getContext(), phoneAccountHandle)) {
+      voicemailChangePinPreference.setTitle(R.string.voicemail_set_pin_dialog_title);
+    } else {
+      voicemailChangePinPreference.setTitle(R.string.voicemail_change_pin_dialog_title);
+    }
+
+    if (omtpVvmCarrierConfigHelper.isValid()) {
+      voicemailVisualVoicemail.setOnPreferenceChangeListener(this);
+      voicemailVisualVoicemail.setChecked(
+          VisualVoicemailSettingsUtil.isEnabled(getContext(), phoneAccountHandle));
+      if (!isVisualVoicemailActivated()) {
+        prefSet.removePreference(voicemailChangePinPreference);
+      }
+    } else {
+      prefSet.removePreference(voicemailVisualVoicemail);
+      prefSet.removePreference(voicemailChangePinPreference);
+    }
+
+    advancedSettings =
+        (PreferenceScreen) findPreference(getString(R.string.voicemail_advanced_settings_key));
+    Intent advancedSettingsIntent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
+    advancedSettingsIntent.putExtra(TelephonyManager.EXTRA_HIDE_PUBLIC_SETTINGS, true);
+    advancedSettings.setIntent(advancedSettingsIntent);
+  }
+
+  @Override
+  public void onPause() {
+    super.onPause();
+  }
+
+  /**
+   * Implemented to support onPreferenceChangeListener to look for preference changes.
+   *
+   * @param preference is the preference to be changed
+   * @param objValue should be the value of the selection, NOT its localized display value.
+   */
+  @Override
+  public boolean onPreferenceChange(Preference preference, Object objValue) {
+    VvmLog.d(TAG, "onPreferenceChange: \"" + preference + "\" changed to \"" + objValue + "\"");
+    if (preference.getKey().equals(voicemailVisualVoicemail.getKey())) {
+      boolean isEnabled = (boolean) objValue;
+      VisualVoicemailSettingsUtil.setEnabled(getContext(), phoneAccountHandle, isEnabled);
+      PreferenceScreen prefSet = getPreferenceScreen();
+      if (isVisualVoicemailActivated()) {
+        prefSet.addPreference(voicemailChangePinPreference);
+      } else {
+        prefSet.removePreference(voicemailChangePinPreference);
+      }
+    } else if (preference.getKey().equals(autoArchiveSwitchPreference.getKey())) {
+      logArchiveToggle((boolean) objValue);
+      VisualVoicemailSettingsUtil.setArchiveEnabled(
+          getContext(), phoneAccountHandle, (boolean) objValue);
+    } else if (preference.getKey().equals(voicemailVibration.getKey())) {
+      getContext()
+          .getSystemService(TelephonyManager.class)
+          .setVoicemailVibrationEnabled(phoneAccountHandle, (boolean) objValue);
+    }
+
+    // Always let the preference setting proceed.
+    return true;
+  }
+
+  private void logArchiveToggle(boolean userTurnedOn) {
+    if (userTurnedOn) {
+      Logger.get(getContext())
+          .logImpression(DialerImpression.Type.VVM_USER_TURNED_ARCHIVE_ON_FROM_SETTINGS);
+    } else {
+      Logger.get(getContext())
+          .logImpression(DialerImpression.Type.VVM_USER_TURNED_ARCHIVE_OFF_FROM_SETTINGS);
+    }
+  }
+
+  @Override
+  public void onVoicemailRingtoneNameChanged(CharSequence name) {
+    oldRingtoneName = name;
+  }
+
+  private boolean isVisualVoicemailActivated() {
+    if (!VisualVoicemailSettingsUtil.isEnabled(getContext(), phoneAccountHandle)) {
+      return false;
+    }
+    return VvmAccountManager.isAccountActivated(getContext(), phoneAccountHandle);
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/LegacyModeSmsHandler.java b/java/com/android/voicemail/impl/sms/LegacyModeSmsHandler.java
new file mode 100644
index 0000000..1d1a639
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/LegacyModeSmsHandler.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.sms;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.VisualVoicemailSms;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.TelephonyManagerStub;
+import com.android.voicemail.impl.VvmLog;
+
+/**
+ * Class ot handle voicemail SMS under legacy mode
+ *
+ * @see OmtpVvmCarrierConfigHelper#isLegacyModeEnabled()
+ */
+public class LegacyModeSmsHandler {
+
+  private static final String TAG = "LegacyModeSmsHandler";
+
+  public static void handle(Context context, VisualVoicemailSms sms) {
+    VvmLog.v(TAG, "processing VVM SMS on legacy mode");
+    String eventType = sms.getPrefix();
+    Bundle data = sms.getFields();
+    PhoneAccountHandle handle = sms.getPhoneAccountHandle();
+
+    if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
+      SyncMessage message = new SyncMessage(data);
+      VvmLog.v(
+          TAG, "Received SYNC sms for " + handle + " with event " + message.getSyncTriggerEvent());
+
+      switch (message.getSyncTriggerEvent()) {
+        case OmtpConstants.NEW_MESSAGE:
+        case OmtpConstants.MAILBOX_UPDATE:
+          // The user has called into the voicemail and the new message count could
+          // change.
+          // For some carriers new message count could be set to 0 even if there are still
+          // unread messages, to clear the message waiting indicator.
+          VvmLog.v(TAG, "updating MWI");
+
+          // Setting voicemail message count to non-zero will show the telephony voicemail
+          // notification, and zero will clear it.
+          TelephonyManagerStub.showVoicemailNotification(message.getNewMessageCount());
+          break;
+        default:
+          break;
+      }
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/OmtpCvvmMessageSender.java b/java/com/android/voicemail/impl/sms/OmtpCvvmMessageSender.java
new file mode 100644
index 0000000..5fc5e70
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/OmtpCvvmMessageSender.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sms;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.OmtpConstants;
+
+/** An implementation of the OmtpMessageSender for T-Mobile. */
+public class OmtpCvvmMessageSender extends OmtpMessageSender {
+  public OmtpCvvmMessageSender(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      short applicationPort,
+      String destinationNumber) {
+    super(context, phoneAccountHandle, applicationPort, destinationNumber);
+  }
+
+  @Override
+  public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
+    sendCvvmMessage(OmtpConstants.ACTIVATE_REQUEST, sentIntent);
+  }
+
+  @Override
+  public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
+    sendCvvmMessage(OmtpConstants.DEACTIVATE_REQUEST, sentIntent);
+  }
+
+  @Override
+  public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
+    sendCvvmMessage(OmtpConstants.STATUS_REQUEST, sentIntent);
+  }
+
+  private void sendCvvmMessage(String request, PendingIntent sentIntent) {
+    StringBuilder sb = new StringBuilder().append(request);
+    sb.append(OmtpConstants.SMS_PREFIX_SEPARATOR);
+    appendField(sb, "dt", "15");
+    sendSms(sb.toString(), sentIntent);
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/OmtpMessageReceiver.java b/java/com/android/voicemail/impl/sms/OmtpMessageReceiver.java
new file mode 100644
index 0000000..ef0bf10
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/OmtpMessageReceiver.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sms;
+
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.UserManager;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.VisualVoicemailSms;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpService;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.Voicemail;
+import com.android.voicemail.impl.Voicemail.Builder;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.protocol.VisualVoicemailProtocol;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+import com.android.voicemail.impl.sync.OmtpVvmSyncService;
+import com.android.voicemail.impl.sync.SyncOneTask;
+import com.android.voicemail.impl.sync.SyncTask;
+import com.android.voicemail.impl.sync.VoicemailsQueryHelper;
+import com.android.voicemail.impl.utils.VoicemailDatabaseUtil;
+
+/** Receive SMS messages and send for processing by the OMTP visual voicemail source. */
+@TargetApi(VERSION_CODES.O)
+public class OmtpMessageReceiver extends BroadcastReceiver {
+
+  private static final String TAG = "OmtpMessageReceiver";
+
+  private Context mContext;
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    mContext = context;
+    VisualVoicemailSms sms = intent.getExtras().getParcelable(OmtpService.EXTRA_VOICEMAIL_SMS);
+    PhoneAccountHandle phone = sms.getPhoneAccountHandle();
+
+    if (phone == null) {
+      // This should never happen
+      VvmLog.i(TAG, "Received message for null phone account");
+      return;
+    }
+
+    if (!context.getSystemService(UserManager.class).isUserUnlocked()) {
+      VvmLog.i(TAG, "Received message on locked device");
+      // LegacyModeSmsHandler can handle new message notifications without storage access
+      LegacyModeSmsHandler.handle(context, sms);
+      // A full sync will happen after the device is unlocked, so nothing else need to be
+      // done.
+      return;
+    }
+
+    OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(mContext, phone);
+    if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phone)) {
+      if (helper.isLegacyModeEnabled()) {
+        LegacyModeSmsHandler.handle(context, sms);
+      } else {
+        VvmLog.i(TAG, "Received vvm message for disabled vvm source.");
+      }
+      return;
+    }
+
+    String eventType = sms.getPrefix();
+    Bundle data = sms.getFields();
+
+    if (eventType == null || data == null) {
+      VvmLog.e(TAG, "Unparsable VVM SMS received, ignoring");
+      return;
+    }
+
+    if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
+      SyncMessage message = new SyncMessage(data);
+
+      VvmLog.v(
+          TAG, "Received SYNC sms for " + phone + " with event " + message.getSyncTriggerEvent());
+      processSync(phone, message);
+    } else if (eventType.equals(OmtpConstants.STATUS_SMS_PREFIX)) {
+      VvmLog.v(TAG, "Received Status sms for " + phone);
+      // If the STATUS SMS is initiated by ActivationTask the TaskSchedulerService will reject
+      // the follow request. Providing the data will also prevent ActivationTask from
+      // requesting another STATUS SMS. The following task will only run if the carrier
+      // spontaneous send a STATUS SMS, in that case, the VVM service should be reactivated.
+      ActivationTask.start(context, phone, data);
+    } else {
+      VvmLog.w(TAG, "Unknown prefix: " + eventType);
+      VisualVoicemailProtocol protocol = helper.getProtocol();
+      if (protocol == null) {
+        return;
+      }
+      Bundle statusData = helper.getProtocol().translateStatusSmsBundle(helper, eventType, data);
+      if (statusData != null) {
+        VvmLog.i(TAG, "Protocol recognized the SMS as STATUS, activating");
+        ActivationTask.start(context, phone, data);
+      }
+    }
+  }
+
+  /**
+   * A sync message has two purposes: to signal a new voicemail message, and to indicate the
+   * voicemails on the server have changed remotely (usually through the TUI). Save the new message
+   * to the voicemail provider if it is the former case and perform a full sync in the latter case.
+   *
+   * @param message The sync message to extract data from.
+   */
+  private void processSync(PhoneAccountHandle phone, SyncMessage message) {
+    switch (message.getSyncTriggerEvent()) {
+      case OmtpConstants.NEW_MESSAGE:
+        if (!OmtpConstants.VOICE.equals(message.getContentType())) {
+          VvmLog.i(
+              TAG,
+              "Non-voice message of type '" + message.getContentType() + "' received, ignoring");
+          return;
+        }
+
+        Builder builder =
+            Voicemail.createForInsertion(message.getTimestampMillis(), message.getSender())
+                .setPhoneAccount(phone)
+                .setSourceData(message.getId())
+                .setDuration(message.getLength())
+                .setSourcePackage(mContext.getPackageName());
+        Voicemail voicemail = builder.build();
+
+        VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
+        if (queryHelper.isVoicemailUnique(voicemail)) {
+          Uri uri = VoicemailDatabaseUtil.insert(mContext, voicemail);
+          voicemail = builder.setId(ContentUris.parseId(uri)).setUri(uri).build();
+          SyncOneTask.start(mContext, phone, voicemail);
+        }
+        break;
+      case OmtpConstants.MAILBOX_UPDATE:
+        SyncTask.start(mContext, phone, OmtpVvmSyncService.SYNC_DOWNLOAD_ONLY);
+        break;
+      case OmtpConstants.GREETINGS_UPDATE:
+        // Not implemented in V1
+        break;
+      default:
+        VvmLog.e(TAG, "Unrecognized sync trigger event: " + message.getSyncTriggerEvent());
+        break;
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/OmtpMessageSender.java b/java/com/android/voicemail/impl/sms/OmtpMessageSender.java
new file mode 100644
index 0000000..6c9333f
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/OmtpMessageSender.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sms;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SmsManager;
+import android.telephony.VisualVoicemailService;
+import com.android.voicemail.impl.OmtpConstants;
+
+/**
+ * Send client originated OMTP messages to the OMTP server.
+ *
+ * <p>Uses {@link PendingIntent} instead of a call back to notify when the message is sent. This is
+ * primarily to keep the implementation simple and reuse what the underlying {@link SmsManager}
+ * interface provides.
+ *
+ * <p>Provides simple APIs to send different types of mobile originated OMTP SMS to the VVM server.
+ */
+public abstract class OmtpMessageSender {
+  protected static final String TAG = "OmtpMessageSender";
+  protected final Context mContext;
+  protected final PhoneAccountHandle mPhoneAccountHandle;
+  protected final short mApplicationPort;
+  protected final String mDestinationNumber;
+
+  public OmtpMessageSender(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      short applicationPort,
+      String destinationNumber) {
+    mContext = context;
+    mPhoneAccountHandle = phoneAccountHandle;
+    mApplicationPort = applicationPort;
+    mDestinationNumber = destinationNumber;
+  }
+
+  /**
+   * Sends a request to the VVM server to activate VVM for the current subscriber.
+   *
+   * @param sentIntent If not NULL this PendingIntent is broadcast when the message is successfully
+   *     sent, or failed.
+   */
+  public void requestVvmActivation(@Nullable PendingIntent sentIntent) {}
+
+  /**
+   * Sends a request to the VVM server to deactivate VVM for the current subscriber.
+   *
+   * @param sentIntent If not NULL this PendingIntent is broadcast when the message is successfully
+   *     sent, or failed.
+   */
+  public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {}
+
+  /**
+   * Send a request to the VVM server to get account status of the current subscriber.
+   *
+   * @param sentIntent If not NULL this PendingIntent is broadcast when the message is successfully
+   *     sent, or failed.
+   */
+  public void requestVvmStatus(@Nullable PendingIntent sentIntent) {}
+
+  protected void sendSms(String text, PendingIntent sentIntent) {
+    VisualVoicemailService.sendVisualVoicemailSms(
+        mContext, mPhoneAccountHandle, mDestinationNumber, mApplicationPort, text, sentIntent);
+  }
+
+  protected void appendField(StringBuilder sb, String field, Object value) {
+    sb.append(field).append(OmtpConstants.SMS_KEY_VALUE_SEPARATOR).append(value);
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/OmtpStandardMessageSender.java b/java/com/android/voicemail/impl/sms/OmtpStandardMessageSender.java
new file mode 100644
index 0000000..7974699
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/OmtpStandardMessageSender.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sms;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import com.android.voicemail.impl.OmtpConstants;
+
+/** A implementation of the OmtpMessageSender using the standard OMTP sms protocol. */
+public class OmtpStandardMessageSender extends OmtpMessageSender {
+  private final String mClientType;
+  private final String mProtocolVersion;
+  private final String mClientPrefix;
+
+  /**
+   * Creates a new instance of OmtpStandardMessageSender.
+   *
+   * @param applicationPort If set to a value > 0 then a binary sms is sent to this port number.
+   *     Otherwise, a standard text SMS is sent.
+   * @param destinationNumber Destination number to be used.
+   * @param clientType The "ct" field to be set in the MO message. This is the value used by the VVM
+   *     server to identify the client. Certain VVM servers require a specific agreed value for this
+   *     field.
+   * @param protocolVersion OMTP protocol version.
+   * @param clientPrefix The client prefix requested to be used by the server in its MT messages.
+   */
+  public OmtpStandardMessageSender(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      short applicationPort,
+      String destinationNumber,
+      String clientType,
+      String protocolVersion,
+      String clientPrefix) {
+    super(context, phoneAccountHandle, applicationPort, destinationNumber);
+    mClientType = clientType;
+    mProtocolVersion = protocolVersion;
+    mClientPrefix = clientPrefix;
+  }
+
+  // Activate message:
+  // V1.1: Activate:pv=<value>;ct=<value>
+  // V1.2: Activate:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
+  // V1.3: Activate:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
+  @Override
+  public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
+    StringBuilder sb = new StringBuilder().append(OmtpConstants.ACTIVATE_REQUEST);
+
+    appendProtocolVersionAndClientType(sb);
+    if (TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_2)
+        || TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_3)) {
+      appendApplicationPort(sb);
+      appendClientPrefix(sb);
+    }
+
+    sendSms(sb.toString(), sentIntent);
+  }
+
+  // Deactivate message:
+  // V1.1: Deactivate:pv=<value>;ct=<string>
+  // V1.2: Deactivate:pv=<value>;ct=<string>
+  // V1.3: Deactivate:pv=<value>;ct=<string>
+  @Override
+  public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
+    StringBuilder sb = new StringBuilder().append(OmtpConstants.DEACTIVATE_REQUEST);
+    appendProtocolVersionAndClientType(sb);
+
+    sendSms(sb.toString(), sentIntent);
+  }
+
+  // Status message:
+  // V1.1: STATUS
+  // V1.2: STATUS
+  // V1.3: STATUS:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
+  @Override
+  public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
+    StringBuilder sb = new StringBuilder().append(OmtpConstants.STATUS_REQUEST);
+
+    if (TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_3)) {
+      appendProtocolVersionAndClientType(sb);
+      appendApplicationPort(sb);
+      appendClientPrefix(sb);
+    }
+
+    sendSms(sb.toString(), sentIntent);
+  }
+
+  private void appendProtocolVersionAndClientType(StringBuilder sb) {
+    sb.append(OmtpConstants.SMS_PREFIX_SEPARATOR);
+    appendField(sb, OmtpConstants.PROTOCOL_VERSION, mProtocolVersion);
+    sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
+    appendField(sb, OmtpConstants.CLIENT_TYPE, mClientType);
+  }
+
+  private void appendApplicationPort(StringBuilder sb) {
+    sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
+    appendField(sb, OmtpConstants.APPLICATION_PORT, mApplicationPort);
+  }
+
+  private void appendClientPrefix(StringBuilder sb) {
+    sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
+    sb.append(mClientPrefix);
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/StatusMessage.java b/java/com/android/voicemail/impl/sms/StatusMessage.java
new file mode 100644
index 0000000..a5766a6
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/StatusMessage.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sms;
+
+import android.os.Bundle;
+import com.android.voicemail.impl.NeededForTesting;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.VvmLog;
+
+/**
+ * Structured data representation of OMTP STATUS message.
+ *
+ * <p>The getters will return null if the field was not set in the message body or it could not be
+ * parsed.
+ */
+public class StatusMessage {
+  // NOTE: Following Status SMS fields are not yet parsed, as they do not seem
+  // to be useful for initial omtp source implementation.
+  // lang, g_len, vs_len, pw_len, pm, gm, vtc, vt
+
+  private final String mProvisioningStatus;
+  private final String mStatusReturnCode;
+  private final String mSubscriptionUrl;
+  private final String mServerAddress;
+  private final String mTuiAccessNumber;
+  private final String mClientSmsDestinationNumber;
+  private final String mImapPort;
+  private final String mImapUserName;
+  private final String mImapPassword;
+  private final String mSmtpPort;
+  private final String mSmtpUserName;
+  private final String mSmtpPassword;
+  private final String mTuiPasswordLength;
+
+  @Override
+  public String toString() {
+    return "StatusMessage [mProvisioningStatus="
+        + mProvisioningStatus
+        + ", mStatusReturnCode="
+        + mStatusReturnCode
+        + ", mSubscriptionUrl="
+        + mSubscriptionUrl
+        + ", mServerAddress="
+        + mServerAddress
+        + ", mTuiAccessNumber="
+        + mTuiAccessNumber
+        + ", mClientSmsDestinationNumber="
+        + mClientSmsDestinationNumber
+        + ", mImapPort="
+        + mImapPort
+        + ", mImapUserName="
+        + mImapUserName
+        + ", mImapPassword="
+        + VvmLog.pii(mImapPassword)
+        + ", mSmtpPort="
+        + mSmtpPort
+        + ", mSmtpUserName="
+        + mSmtpUserName
+        + ", mSmtpPassword="
+        + VvmLog.pii(mSmtpPassword)
+        + ", mTuiPasswordLength="
+        + mTuiPasswordLength
+        + "]";
+  }
+
+  public StatusMessage(Bundle wrappedData) {
+    mProvisioningStatus = unquote(getString(wrappedData, OmtpConstants.PROVISIONING_STATUS));
+    mStatusReturnCode = getString(wrappedData, OmtpConstants.RETURN_CODE);
+    mSubscriptionUrl = getString(wrappedData, OmtpConstants.SUBSCRIPTION_URL);
+    mServerAddress = getString(wrappedData, OmtpConstants.SERVER_ADDRESS);
+    mTuiAccessNumber = getString(wrappedData, OmtpConstants.TUI_ACCESS_NUMBER);
+    mClientSmsDestinationNumber =
+        getString(wrappedData, OmtpConstants.CLIENT_SMS_DESTINATION_NUMBER);
+    mImapPort = getString(wrappedData, OmtpConstants.IMAP_PORT);
+    mImapUserName = getString(wrappedData, OmtpConstants.IMAP_USER_NAME);
+    mImapPassword = getString(wrappedData, OmtpConstants.IMAP_PASSWORD);
+    mSmtpPort = getString(wrappedData, OmtpConstants.SMTP_PORT);
+    mSmtpUserName = getString(wrappedData, OmtpConstants.SMTP_USER_NAME);
+    mSmtpPassword = getString(wrappedData, OmtpConstants.SMTP_PASSWORD);
+    mTuiPasswordLength = getString(wrappedData, OmtpConstants.TUI_PASSWORD_LENGTH);
+  }
+
+  private static String unquote(String string) {
+    if (string.length() < 2) {
+      return string;
+    }
+    if (string.startsWith("\"") && string.endsWith("\"")) {
+      return string.substring(1, string.length() - 1);
+    }
+    return string;
+  }
+
+  /** @return the subscriber's VVM provisioning status. */
+  public String getProvisioningStatus() {
+    return mProvisioningStatus;
+  }
+
+  /** @return the return-code of the status SMS. */
+  public String getReturnCode() {
+    return mStatusReturnCode;
+  }
+
+  /**
+   * @return the URL of the voicemail server. This is the URL to send the users to for subscribing
+   *     to the visual voicemail service.
+   */
+  @NeededForTesting
+  public String getSubscriptionUrl() {
+    return mSubscriptionUrl;
+  }
+
+  /**
+   * @return the voicemail server address. Either server IP address or fully qualified domain name.
+   */
+  public String getServerAddress() {
+    return mServerAddress;
+  }
+
+  /**
+   * @return the Telephony User Interface number to call to access voicemails directly from the IVR.
+   */
+  @NeededForTesting
+  public String getTuiAccessNumber() {
+    return mTuiAccessNumber;
+  }
+
+  /** @return the number to which client originated SMSes should be sent to. */
+  @NeededForTesting
+  public String getClientSmsDestinationNumber() {
+    return mClientSmsDestinationNumber;
+  }
+
+  /** @return the IMAP server port to talk to. */
+  public String getImapPort() {
+    return mImapPort;
+  }
+
+  /** @return the IMAP user name to be used for authentication. */
+  public String getImapUserName() {
+    return mImapUserName;
+  }
+
+  /** @return the IMAP password to be used for authentication. */
+  public String getImapPassword() {
+    return mImapPassword;
+  }
+
+  /** @return the SMTP server port to talk to. */
+  @NeededForTesting
+  public String getSmtpPort() {
+    return mSmtpPort;
+  }
+
+  /** @return the SMTP user name to be used for SMTP authentication. */
+  @NeededForTesting
+  public String getSmtpUserName() {
+    return mSmtpUserName;
+  }
+
+  /** @return the SMTP password to be used for SMTP authentication. */
+  @NeededForTesting
+  public String getSmtpPassword() {
+    return mSmtpPassword;
+  }
+
+  public String getTuiPasswordLength() {
+    return mTuiPasswordLength;
+  }
+
+  private static String getString(Bundle bundle, String key) {
+    String value = bundle.getString(key);
+    if (value == null) {
+      return "";
+    }
+    return value;
+  }
+
+  /** Saves a StatusMessage to the {@link VisualVoicemailPreferences}. Not all fields are saved. */
+  public VisualVoicemailPreferences.Editor putStatus(VisualVoicemailPreferences.Editor editor) {
+    return editor
+        .putString(OmtpConstants.IMAP_PORT, getImapPort())
+        .putString(OmtpConstants.SERVER_ADDRESS, getServerAddress())
+        .putString(OmtpConstants.IMAP_USER_NAME, getImapUserName())
+        .putString(OmtpConstants.IMAP_PASSWORD, getImapPassword())
+        .putString(OmtpConstants.TUI_PASSWORD_LENGTH, getTuiPasswordLength());
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/StatusSmsFetcher.java b/java/com/android/voicemail/impl/sms/StatusSmsFetcher.java
new file mode 100644
index 0000000..d178628
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/StatusSmsFetcher.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.sms;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SmsManager;
+import android.telephony.VisualVoicemailSms;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpService;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.protocol.VisualVoicemailProtocol;
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/** Intercepts a incoming STATUS SMS with a blocking call. */
+@SuppressWarnings("AndroidApiChecker") /* CompletableFuture is java8*/
+@TargetApi(VERSION_CODES.O)
+public class StatusSmsFetcher extends BroadcastReceiver implements Closeable {
+
+  private static final String TAG = "VvmStatusSmsFetcher";
+
+  private static final long STATUS_SMS_TIMEOUT_MILLIS = 60_000;
+
+  private static final String ACTION_REQUEST_SENT_INTENT =
+      "com.android.voicemailomtp.sms.REQUEST_SENT";
+  private static final int ACTION_REQUEST_SENT_REQUEST_CODE = 0;
+
+  private CompletableFuture<Bundle> mFuture = new CompletableFuture<>();
+
+  private final Context mContext;
+  private final PhoneAccountHandle mPhoneAccountHandle;
+
+  public StatusSmsFetcher(Context context, PhoneAccountHandle phoneAccountHandle) {
+    mContext = context;
+    mPhoneAccountHandle = phoneAccountHandle;
+    IntentFilter filter = new IntentFilter(ACTION_REQUEST_SENT_INTENT);
+    filter.addAction(OmtpService.ACTION_SMS_RECEIVED);
+    context.registerReceiver(this, filter);
+  }
+
+  @Override
+  public void close() throws IOException {
+    mContext.unregisterReceiver(this);
+  }
+
+  @WorkerThread
+  @Nullable
+  public Bundle get()
+      throws InterruptedException, ExecutionException, TimeoutException, CancellationException {
+    Assert.isNotMainThread();
+    return mFuture.get(STATUS_SMS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+  }
+
+  public PendingIntent getSentIntent() {
+    Intent intent = new Intent(ACTION_REQUEST_SENT_INTENT);
+    intent.setPackage(mContext.getPackageName());
+    // Because the receiver is registered dynamically, implicit intent must be used.
+    // There should only be a single status SMS request at a time.
+    return PendingIntent.getBroadcast(
+        mContext, ACTION_REQUEST_SENT_REQUEST_CODE, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+  }
+
+  @Override
+  @MainThread
+  public void onReceive(Context context, Intent intent) {
+    Assert.isMainThread();
+    if (ACTION_REQUEST_SENT_INTENT.equals(intent.getAction())) {
+      int resultCode = getResultCode();
+
+      if (resultCode == Activity.RESULT_OK) {
+        VvmLog.d(TAG, "Request SMS successfully sent");
+        return;
+      }
+
+      VvmLog.e(TAG, "Request SMS send failed: " + sentSmsResultToString(resultCode));
+      mFuture.cancel(true);
+      return;
+    }
+
+    VisualVoicemailSms sms = intent.getExtras().getParcelable(OmtpService.EXTRA_VOICEMAIL_SMS);
+
+    if (!mPhoneAccountHandle.equals(sms.getPhoneAccountHandle())) {
+      return;
+    }
+    String eventType = sms.getPrefix();
+
+    if (eventType.equals(OmtpConstants.STATUS_SMS_PREFIX)) {
+      mFuture.complete(sms.getFields());
+      return;
+    }
+
+    if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
+      return;
+    }
+
+    VvmLog.i(
+        TAG,
+        "VVM SMS with event " + eventType + " received, attempting to translate to STATUS SMS");
+    OmtpVvmCarrierConfigHelper helper =
+        new OmtpVvmCarrierConfigHelper(context, mPhoneAccountHandle);
+    VisualVoicemailProtocol protocol = helper.getProtocol();
+    if (protocol == null) {
+      return;
+    }
+    Bundle translatedBundle = protocol.translateStatusSmsBundle(helper, eventType, sms.getFields());
+
+    if (translatedBundle != null) {
+      VvmLog.i(TAG, "Translated to STATUS SMS");
+      mFuture.complete(translatedBundle);
+    }
+  }
+
+  private static String sentSmsResultToString(int resultCode) {
+    switch (resultCode) {
+      case Activity.RESULT_OK:
+        return "OK";
+      case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
+        return "RESULT_ERROR_GENERIC_FAILURE";
+      case SmsManager.RESULT_ERROR_NO_SERVICE:
+        return "RESULT_ERROR_GENERIC_FAILURE";
+      case SmsManager.RESULT_ERROR_NULL_PDU:
+        return "RESULT_ERROR_GENERIC_FAILURE";
+      case SmsManager.RESULT_ERROR_RADIO_OFF:
+        return "RESULT_ERROR_GENERIC_FAILURE";
+      default:
+        return "UNKNOWN CODE: " + resultCode;
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/SyncMessage.java b/java/com/android/voicemail/impl/sms/SyncMessage.java
new file mode 100644
index 0000000..3cfa1a7
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/SyncMessage.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sms;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import com.android.voicemail.impl.NeededForTesting;
+import com.android.voicemail.impl.OmtpConstants;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
+/**
+ * Structured data representation of an OMTP SYNC message.
+ *
+ * <p>Getters will return null if the field was not set in the message body or it could not be
+ * parsed.
+ */
+public class SyncMessage {
+  // Sync event that triggered this message.
+  private final String mSyncTriggerEvent;
+  // Total number of new messages on the server.
+  private final int mNewMessageCount;
+  // UID of the new message.
+  private final String mMessageId;
+  // Length of the message.
+  private final int mMessageLength;
+  // Content type (voice, video, fax...) of the new message.
+  private final String mContentType;
+  // Sender of the new message.
+  private final String mSender;
+  // Timestamp (in millis) of the new message.
+  private final long mMsgTimeMillis;
+
+  @Override
+  public String toString() {
+    return "SyncMessage [mSyncTriggerEvent="
+        + mSyncTriggerEvent
+        + ", mNewMessageCount="
+        + mNewMessageCount
+        + ", mMessageId="
+        + mMessageId
+        + ", mMessageLength="
+        + mMessageLength
+        + ", mContentType="
+        + mContentType
+        + ", mSender="
+        + mSender
+        + ", mMsgTimeMillis="
+        + mMsgTimeMillis
+        + "]";
+  }
+
+  public SyncMessage(Bundle wrappedData) {
+    mSyncTriggerEvent = getString(wrappedData, OmtpConstants.SYNC_TRIGGER_EVENT);
+    mMessageId = getString(wrappedData, OmtpConstants.MESSAGE_UID);
+    mMessageLength = getInt(wrappedData, OmtpConstants.MESSAGE_LENGTH);
+    mContentType = getString(wrappedData, OmtpConstants.CONTENT_TYPE);
+    mSender = getString(wrappedData, OmtpConstants.SENDER);
+    mNewMessageCount = getInt(wrappedData, OmtpConstants.NUM_MESSAGE_COUNT);
+    mMsgTimeMillis = parseTime(wrappedData.getString(OmtpConstants.TIME));
+  }
+
+  private static long parseTime(@Nullable String value) {
+    if (value == null) {
+      return 0L;
+    }
+    try {
+      return new SimpleDateFormat(OmtpConstants.DATE_TIME_FORMAT, Locale.US).parse(value).getTime();
+    } catch (ParseException e) {
+      return 0L;
+    }
+  }
+  /**
+   * @return the event that triggered the sync message. This is a mandatory field and must always be
+   *     set.
+   */
+  public String getSyncTriggerEvent() {
+    return mSyncTriggerEvent;
+  }
+
+  /** @return the number of new messages stored on the voicemail server. */
+  @NeededForTesting
+  public int getNewMessageCount() {
+    return mNewMessageCount;
+  }
+
+  /**
+   * @return the message ID of the new message.
+   *     <p>Expected to be set only for {@link OmtpConstants#NEW_MESSAGE}
+   */
+  public String getId() {
+    return mMessageId;
+  }
+
+  /**
+   * @return the content type of the new message.
+   *     <p>Expected to be set only for {@link OmtpConstants#NEW_MESSAGE}
+   */
+  @NeededForTesting
+  public String getContentType() {
+    return mContentType;
+  }
+
+  /**
+   * @return the message length of the new message.
+   *     <p>Expected to be set only for {@link OmtpConstants#NEW_MESSAGE}
+   */
+  public int getLength() {
+    return mMessageLength;
+  }
+
+  /**
+   * @return the sender's phone number of the new message specified as MSISDN.
+   *     <p>Expected to be set only for {@link OmtpConstants#NEW_MESSAGE}
+   */
+  public String getSender() {
+    return mSender;
+  }
+
+  /**
+   * @return the timestamp as milliseconds for the new message.
+   *     <p>Expected to be set only for {@link OmtpConstants#NEW_MESSAGE}
+   */
+  public long getTimestampMillis() {
+    return mMsgTimeMillis;
+  }
+
+  private static int getInt(Bundle wrappedData, String key) {
+    String value = wrappedData.getString(key);
+    if (value == null) {
+      return 0;
+    }
+    try {
+      return Integer.parseInt(value);
+    } catch (NumberFormatException e) {
+      return 0;
+    }
+  }
+
+  private static String getString(Bundle wrappedData, String key) {
+    String value = wrappedData.getString(key);
+    if (value == null) {
+      return "";
+    }
+    return value;
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/Vvm3MessageSender.java b/java/com/android/voicemail/impl/sms/Vvm3MessageSender.java
new file mode 100644
index 0000000..1f17692
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/Vvm3MessageSender.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sms;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+
+public class Vvm3MessageSender extends OmtpMessageSender {
+
+  /**
+   * Creates a new instance of Vvm3MessageSender.
+   *
+   * @param applicationPort If set to a value > 0 then a binary sms is sent to this port number.
+   *     Otherwise, a standard text SMS is sent.
+   */
+  public Vvm3MessageSender(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      short applicationPort,
+      String destinationNumber) {
+    super(context, phoneAccountHandle, applicationPort, destinationNumber);
+  }
+
+  @Override
+  public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
+    // Activation not supported for VVM3, send a status request instead.
+    requestVvmStatus(sentIntent);
+  }
+
+  @Override
+  public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
+    // Deactivation not supported for VVM3, do nothing
+  }
+
+  @Override
+  public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
+    // Status message:
+    // STATUS
+    StringBuilder sb = new StringBuilder().append("STATUS");
+    sendSms(sb.toString(), sentIntent);
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/OmtpVvmSyncReceiver.java b/java/com/android/voicemail/impl/sync/OmtpVvmSyncReceiver.java
new file mode 100644
index 0000000..5a2fe14
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/OmtpVvmSyncReceiver.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.sync;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.VoicemailContract;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+import java.util.List;
+
+public class OmtpVvmSyncReceiver extends BroadcastReceiver {
+
+  private static final String TAG = "OmtpVvmSyncReceiver";
+
+  @Override
+  public void onReceive(final Context context, Intent intent) {
+    if (VoicemailContract.ACTION_SYNC_VOICEMAIL.equals(intent.getAction())) {
+      VvmLog.v(TAG, "Sync intent received");
+
+      List<PhoneAccountHandle> accounts =
+          context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts();
+      for (PhoneAccountHandle phoneAccount : accounts) {
+        if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccount)) {
+          continue;
+        }
+        if (!VvmAccountManager.isAccountActivated(context, phoneAccount)) {
+          VvmLog.i(TAG, "Unactivated account " + phoneAccount + " found, activating");
+          ActivationTask.start(context, phoneAccount, null);
+        } else {
+          SyncTask.start(context, phoneAccount, OmtpVvmSyncService.SYNC_FULL_SYNC);
+        }
+      }
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java b/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java
new file mode 100644
index 0000000..c255019
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sync;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.Network;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.support.v4.os.BuildCompat;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.Voicemail;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.fetch.VoicemailFetchedCallback;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
+import com.android.voicemail.impl.scheduling.BaseTask;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
+import com.android.voicemail.impl.utils.VoicemailDatabaseUtil;
+import java.util.List;
+import java.util.Map;
+
+/** Sync OMTP visual voicemail. */
+@TargetApi(VERSION_CODES.O)
+public class OmtpVvmSyncService {
+
+  private static final String TAG = OmtpVvmSyncService.class.getSimpleName();
+
+  /** Signifies a sync with both uploading to the server and downloading from the server. */
+  public static final String SYNC_FULL_SYNC = "full_sync";
+  /** Only upload to the server. */
+  public static final String SYNC_UPLOAD_ONLY = "upload_only";
+  /** Only download from the server. */
+  public static final String SYNC_DOWNLOAD_ONLY = "download_only";
+  /** Only download single voicemail transcription. */
+  public static final String SYNC_DOWNLOAD_ONE_TRANSCRIPTION = "download_one_transcription";
+  /** Threshold for whether we should archive and delete voicemails from the remote VM server. */
+  private static final float AUTO_DELETE_ARCHIVE_VM_THRESHOLD = 0.75f;
+
+  private final Context mContext;
+
+  private VoicemailsQueryHelper mQueryHelper;
+
+  public OmtpVvmSyncService(Context context) {
+    mContext = context;
+    mQueryHelper = new VoicemailsQueryHelper(mContext);
+  }
+
+  public void sync(
+      BaseTask task,
+      String action,
+      PhoneAccountHandle phoneAccount,
+      Voicemail voicemail,
+      VoicemailStatus.Editor status) {
+    Assert.isTrue(phoneAccount != null);
+    VvmLog.v(TAG, "Sync requested: " + action + " - for account: " + phoneAccount);
+    setupAndSendRequest(task, phoneAccount, voicemail, action, status);
+  }
+
+  private void setupAndSendRequest(
+      BaseTask task,
+      PhoneAccountHandle phoneAccount,
+      Voicemail voicemail,
+      String action,
+      VoicemailStatus.Editor status) {
+    if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phoneAccount)) {
+      VvmLog.v(TAG, "Sync requested for disabled account");
+      return;
+    }
+    if (!VvmAccountManager.isAccountActivated(mContext, phoneAccount)) {
+      ActivationTask.start(mContext, phoneAccount, null);
+      return;
+    }
+
+    OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(mContext, phoneAccount);
+    // DATA_IMAP_OPERATION_STARTED posting should not be deferred. This event clears all data
+    // channel errors, which should happen when the task starts, not when it ends. It is the
+    // "Sync in progress..." status.
+    config.handleEvent(
+        VoicemailStatus.edit(mContext, phoneAccount), OmtpEvents.DATA_IMAP_OPERATION_STARTED);
+    try (NetworkWrapper network = VvmNetworkRequest.getNetwork(config, phoneAccount, status)) {
+      if (network == null) {
+        VvmLog.e(TAG, "unable to acquire network");
+        task.fail();
+        return;
+      }
+      doSync(task, network.get(), phoneAccount, voicemail, action, status);
+    } catch (RequestFailedException e) {
+      config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
+      task.fail();
+    }
+  }
+
+  private void doSync(
+      BaseTask task,
+      Network network,
+      PhoneAccountHandle phoneAccount,
+      Voicemail voicemail,
+      String action,
+      VoicemailStatus.Editor status) {
+    try (ImapHelper imapHelper = new ImapHelper(mContext, phoneAccount, network, status)) {
+      boolean success;
+      if (voicemail == null) {
+        success = syncAll(action, imapHelper, phoneAccount);
+      } else {
+        success = syncOne(imapHelper, voicemail, phoneAccount);
+      }
+      if (success) {
+        // TODO: b/30569269 failure should interrupt all subsequent task via exceptions
+        imapHelper.updateQuota();
+        autoDeleteAndArchiveVM(imapHelper, phoneAccount);
+        imapHelper.handleEvent(OmtpEvents.DATA_IMAP_OPERATION_COMPLETED);
+      } else {
+        task.fail();
+      }
+    } catch (InitializingException e) {
+      VvmLog.w(TAG, "Can't retrieve Imap credentials.", e);
+      return;
+    }
+  }
+
+  /**
+   * If the VM quota exceeds {@value AUTO_DELETE_ARCHIVE_VM_THRESHOLD}, we should archive the VMs
+   * and delete them from the server to ensure new VMs can be received.
+   */
+  private void autoDeleteAndArchiveVM(
+      ImapHelper imapHelper, PhoneAccountHandle phoneAccountHandle) {
+
+    if (ConfigProviderBindings.get(mContext)
+            .getBoolean(VisualVoicemailSettingsUtil.ALLOW_VOICEMAIL_ARCHIVE, true)
+        && isArchiveEnabled(mContext, phoneAccountHandle)) {
+      if ((float) imapHelper.getOccuupiedQuota() / (float) imapHelper.getTotalQuota()
+          > AUTO_DELETE_ARCHIVE_VM_THRESHOLD) {
+        deleteAndArchiveVM(imapHelper);
+        imapHelper.updateQuota();
+        Logger.get(mContext)
+            .logImpression(DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETED_VM_FROM_SERVER);
+      } else {
+        VvmLog.i(TAG, "no need to archive and auto delete VM, quota below threshold");
+      }
+    } else {
+      VvmLog.i(TAG, "autoDeleteAndArchiveVM is turned off");
+      Logger.get(mContext).logImpression(DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETE_TURNED_OFF);
+    }
+  }
+
+  private static boolean isArchiveEnabled(Context context, PhoneAccountHandle phoneAccountHandle) {
+    return VisualVoicemailSettingsUtil.isArchiveEnabled(context, phoneAccountHandle)
+        && VisualVoicemailSettingsUtil.isEnabled(context, phoneAccountHandle);
+  }
+
+  private void deleteAndArchiveVM(ImapHelper imapHelper) {
+    // Archive column should only be used for 0 and above
+    Assert.isTrue(BuildCompat.isAtLeastO());
+    // The number of voicemails that exceed our threshold and should be deleted from the server
+    int numVoicemails =
+        imapHelper.getOccuupiedQuota()
+            - ((int) AUTO_DELETE_ARCHIVE_VM_THRESHOLD * imapHelper.getTotalQuota());
+    List<Voicemail> oldestVoicemails = mQueryHelper.oldestVoicemailsOnServer(numVoicemails);
+    if (!oldestVoicemails.isEmpty()) {
+      mQueryHelper.markArchivedInDatabase(oldestVoicemails);
+      imapHelper.markMessagesAsDeleted(oldestVoicemails);
+      VvmLog.i(
+          TAG,
+          String.format(
+              "successfully archived and deleted %d voicemails", oldestVoicemails.size()));
+    } else {
+      VvmLog.w(TAG, "remote voicemail server is empty");
+    }
+  }
+
+  private boolean syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account) {
+    boolean uploadSuccess = true;
+    boolean downloadSuccess = true;
+
+    if (SYNC_FULL_SYNC.equals(action) || SYNC_UPLOAD_ONLY.equals(action)) {
+      uploadSuccess = upload(imapHelper);
+    }
+    if (SYNC_FULL_SYNC.equals(action) || SYNC_DOWNLOAD_ONLY.equals(action)) {
+      downloadSuccess = download(imapHelper, account);
+    }
+
+    VvmLog.v(
+        TAG,
+        "upload succeeded: ["
+            + String.valueOf(uploadSuccess)
+            + "] download succeeded: ["
+            + String.valueOf(downloadSuccess)
+            + "]");
+
+    return uploadSuccess && downloadSuccess;
+  }
+
+  private boolean syncOne(ImapHelper imapHelper, Voicemail voicemail, PhoneAccountHandle account) {
+    if (shouldPerformPrefetch(account, imapHelper)) {
+      VoicemailFetchedCallback callback =
+          new VoicemailFetchedCallback(mContext, voicemail.getUri(), account);
+      imapHelper.fetchVoicemailPayload(callback, voicemail.getSourceData());
+    }
+
+    return imapHelper.fetchTranscription(
+        new TranscriptionFetchedCallback(mContext, voicemail), voicemail.getSourceData());
+  }
+
+  private boolean upload(ImapHelper imapHelper) {
+    List<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails();
+    List<Voicemail> deletedVoicemails = mQueryHelper.getDeletedVoicemails();
+
+    boolean success = true;
+
+    if (deletedVoicemails.size() > 0) {
+      if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) {
+        // We want to delete selectively instead of all the voicemails for this provider
+        // in case the state changed since the IMAP query was completed.
+        mQueryHelper.deleteFromDatabase(deletedVoicemails);
+      } else {
+        success = false;
+      }
+    }
+
+    if (readVoicemails.size() > 0) {
+      if (imapHelper.markMessagesAsRead(readVoicemails)) {
+        mQueryHelper.markCleanInDatabase(readVoicemails);
+      } else {
+        success = false;
+      }
+    }
+
+    return success;
+  }
+
+  private boolean download(ImapHelper imapHelper, PhoneAccountHandle account) {
+    List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails();
+    List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails();
+
+    if (localVoicemails == null || serverVoicemails == null) {
+      // Null value means the query failed.
+      return false;
+    }
+
+    Map<String, Voicemail> remoteMap = buildMap(serverVoicemails);
+
+    // Go through all the local voicemails and check if they are on the server.
+    // They may be read or deleted on the server but not locally. Perform the
+    // appropriate local operation if the status differs from the server. Remove
+    // the messages that exist both locally and on the server to know which server
+    // messages to insert locally.
+    // Voicemails that were removed automatically from the server, are marked as
+    // archived and are stored locally. We do not delete them, as they were removed from the server
+    // by design (to make space).
+    for (int i = 0; i < localVoicemails.size(); i++) {
+      Voicemail localVoicemail = localVoicemails.get(i);
+      Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData());
+
+      // Do not delete voicemails that are archived marked as archived.
+      if (remoteVoicemail == null) {
+        mQueryHelper.deleteNonArchivedFromDatabase(localVoicemail);
+      } else {
+        if (remoteVoicemail.isRead() != localVoicemail.isRead()) {
+          mQueryHelper.markReadInDatabase(localVoicemail);
+        }
+
+        if (!TextUtils.isEmpty(remoteVoicemail.getTranscription())
+            && TextUtils.isEmpty(localVoicemail.getTranscription())) {
+          mQueryHelper.updateWithTranscription(localVoicemail, remoteVoicemail.getTranscription());
+        }
+      }
+    }
+
+    // The leftover messages are messages that exist on the server but not locally.
+    boolean prefetchEnabled = shouldPerformPrefetch(account, imapHelper);
+    for (Voicemail remoteVoicemail : remoteMap.values()) {
+      Uri uri = VoicemailDatabaseUtil.insert(mContext, remoteVoicemail);
+      if (prefetchEnabled) {
+        VoicemailFetchedCallback fetchedCallback =
+            new VoicemailFetchedCallback(mContext, uri, account);
+        imapHelper.fetchVoicemailPayload(fetchedCallback, remoteVoicemail.getSourceData());
+      }
+    }
+
+    return true;
+  }
+
+  private boolean shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper) {
+    OmtpVvmCarrierConfigHelper carrierConfigHelper =
+        new OmtpVvmCarrierConfigHelper(mContext, account);
+    return carrierConfigHelper.isPrefetchEnabled() && !imapHelper.isRoaming();
+  }
+
+  /** Builds a map from provider data to message for the given collection of voicemails. */
+  private Map<String, Voicemail> buildMap(List<Voicemail> messages) {
+    Map<String, Voicemail> map = new ArrayMap<String, Voicemail>();
+    for (Voicemail message : messages) {
+      map.put(message.getSourceData(), message);
+    }
+    return map;
+  }
+
+  /** Callback for {@link ImapHelper#fetchTranscription(TranscriptionFetchedCallback, String)} */
+  public static class TranscriptionFetchedCallback {
+
+    private Context mContext;
+    private Voicemail mVoicemail;
+
+    public TranscriptionFetchedCallback(Context context, Voicemail voicemail) {
+      mContext = context;
+      mVoicemail = voicemail;
+    }
+
+    public void setVoicemailTranscription(String transcription) {
+      VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
+      queryHelper.updateWithTranscription(mVoicemail, transcription);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/SyncOneTask.java b/java/com/android/voicemail/impl/sync/SyncOneTask.java
new file mode 100644
index 0000000..f970150
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/SyncOneTask.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.sync;
+
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.Voicemail;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.scheduling.BaseTask;
+import com.android.voicemail.impl.scheduling.RetryPolicy;
+
+/**
+ * Task to download a single voicemail from the server. This task is initiated by a SMS notifying
+ * the new voicemail arrival, and ignores the duplicated tasks constraint.
+ */
+public class SyncOneTask extends BaseTask {
+
+  private static final int RETRY_TIMES = 2;
+  private static final int RETRY_INTERVAL_MILLIS = 5_000;
+
+  private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+  private static final String EXTRA_SYNC_TYPE = "extra_sync_type";
+  private static final String EXTRA_VOICEMAIL = "extra_voicemail";
+
+  private PhoneAccountHandle mPhone;
+  private String mSyncType;
+  private Voicemail mVoicemail;
+
+  public static void start(Context context, PhoneAccountHandle phone, Voicemail voicemail) {
+    Intent intent = BaseTask.createIntent(context, SyncOneTask.class, phone);
+    intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phone);
+    intent.putExtra(EXTRA_SYNC_TYPE, OmtpVvmSyncService.SYNC_DOWNLOAD_ONE_TRANSCRIPTION);
+    intent.putExtra(EXTRA_VOICEMAIL, voicemail);
+    context.startService(intent);
+  }
+
+  public SyncOneTask() {
+    super(TASK_ALLOW_DUPLICATES);
+    addPolicy(new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS));
+  }
+
+  public void onCreate(Context context, Intent intent, int flags, int startId) {
+    super.onCreate(context, intent, flags, startId);
+    mPhone = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+    mSyncType = intent.getStringExtra(EXTRA_SYNC_TYPE);
+    mVoicemail = intent.getParcelableExtra(EXTRA_VOICEMAIL);
+  }
+
+  @Override
+  public void onExecuteInBackgroundThread() {
+    OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
+    service.sync(this, mSyncType, mPhone, mVoicemail, VoicemailStatus.edit(getContext(), mPhone));
+  }
+
+  @Override
+  public Intent createRestartIntent() {
+    Intent intent = super.createRestartIntent();
+    intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, mPhone);
+    intent.putExtra(EXTRA_SYNC_TYPE, mSyncType);
+    intent.putExtra(EXTRA_VOICEMAIL, mVoicemail);
+    return intent;
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/SyncTask.java b/java/com/android/voicemail/impl/sync/SyncTask.java
new file mode 100644
index 0000000..71c9841
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/SyncTask.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.sync;
+
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.scheduling.BaseTask;
+import com.android.voicemail.impl.scheduling.MinimalIntervalPolicy;
+import com.android.voicemail.impl.scheduling.RetryPolicy;
+
+/** System initiated sync request. */
+public class SyncTask extends BaseTask {
+
+  // Try sync for a total of 5 times, should take around 5 minutes before finally giving up.
+  private static final int RETRY_TIMES = 4;
+  private static final int RETRY_INTERVAL_MILLIS = 5_000;
+  private static final int MINIMAL_INTERVAL_MILLIS = 60_000;
+
+  private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+  private static final String EXTRA_SYNC_TYPE = "extra_sync_type";
+
+  private final RetryPolicy mRetryPolicy;
+
+  private PhoneAccountHandle mPhone;
+  private String mSyncType;
+
+  public static void start(Context context, PhoneAccountHandle phone, String syncType) {
+    Intent intent = BaseTask.createIntent(context, SyncTask.class, phone);
+    intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phone);
+    intent.putExtra(EXTRA_SYNC_TYPE, syncType);
+    context.startService(intent);
+  }
+
+  public SyncTask() {
+    super(TASK_SYNC);
+    mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS);
+    addPolicy(mRetryPolicy);
+    addPolicy(new MinimalIntervalPolicy(MINIMAL_INTERVAL_MILLIS));
+  }
+
+  public void onCreate(Context context, Intent intent, int flags, int startId) {
+    super.onCreate(context, intent, flags, startId);
+    mPhone = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+    mSyncType = intent.getStringExtra(EXTRA_SYNC_TYPE);
+  }
+
+  @Override
+  public void onExecuteInBackgroundThread() {
+    OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
+    service.sync(this, mSyncType, mPhone, null, mRetryPolicy.getVoicemailStatusEditor());
+  }
+
+  @Override
+  public Intent createRestartIntent() {
+    Intent intent = super.createRestartIntent();
+    intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, mPhone);
+    intent.putExtra(EXTRA_SYNC_TYPE, mSyncType);
+    return intent;
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/UploadTask.java b/java/com/android/voicemail/impl/sync/UploadTask.java
new file mode 100644
index 0000000..7d1a797
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/UploadTask.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.sync;
+
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.scheduling.BaseTask;
+import com.android.voicemail.impl.scheduling.PostponePolicy;
+
+/**
+ * Upload task triggered by database changes. Will wait until the database has been stable for
+ * {@link #POSTPONE_MILLIS} to execute.
+ */
+public class UploadTask extends BaseTask {
+
+  private static final String TAG = "VvmUploadTask";
+
+  private static final int POSTPONE_MILLIS = 5_000;
+
+  public UploadTask() {
+    super(TASK_UPLOAD);
+    addPolicy(new PostponePolicy(POSTPONE_MILLIS));
+  }
+
+  public static void start(Context context, PhoneAccountHandle phoneAccountHandle) {
+    Intent intent = BaseTask.createIntent(context, UploadTask.class, phoneAccountHandle);
+    context.startService(intent);
+  }
+
+  @Override
+  public void onCreate(Context context, Intent intent, int flags, int startId) {
+    super.onCreate(context, intent, flags, startId);
+  }
+
+  @Override
+  public void onExecuteInBackgroundThread() {
+    OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
+
+    PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
+    if (phoneAccountHandle == null) {
+      // This should never happen
+      VvmLog.e(TAG, "null phone account for phoneAccountHandle " + getPhoneAccountHandle());
+      return;
+    }
+    service.sync(
+        this,
+        OmtpVvmSyncService.SYNC_UPLOAD_ONLY,
+        phoneAccountHandle,
+        null,
+        VoicemailStatus.edit(getContext(), phoneAccountHandle));
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/VoicemailProviderChangeReceiver.java b/java/com/android/voicemail/impl/sync/VoicemailProviderChangeReceiver.java
new file mode 100644
index 0000000..eaca3c4
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VoicemailProviderChangeReceiver.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sync;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.VoicemailContract;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+
+/** Receives changes to the voicemail provider so they can be sent to the voicemail server. */
+public class VoicemailProviderChangeReceiver extends BroadcastReceiver {
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    boolean isSelfChanged = intent.getBooleanExtra(VoicemailContract.EXTRA_SELF_CHANGE, false);
+    if (!isSelfChanged) {
+      for (PhoneAccountHandle phoneAccount : VvmAccountManager.getActiveAccounts(context)) {
+        if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccount)) {
+          continue;
+        }
+        UploadTask.start(context, phoneAccount);
+      }
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/VoicemailStatusQueryHelper.java b/java/com/android/voicemail/impl/sync/VoicemailStatusQueryHelper.java
new file mode 100644
index 0000000..4ef19da
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VoicemailStatusQueryHelper.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sync;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Status;
+import android.telecom.PhoneAccountHandle;
+
+/** Construct queries to interact with the voicemail status table. */
+public class VoicemailStatusQueryHelper {
+
+  static final String[] PROJECTION =
+      new String[] {
+        Status._ID, // 0
+        Status.CONFIGURATION_STATE, // 1
+        Status.NOTIFICATION_CHANNEL_STATE, // 2
+        Status.SOURCE_PACKAGE // 3
+      };
+
+  public static final int _ID = 0;
+  public static final int CONFIGURATION_STATE = 1;
+  public static final int NOTIFICATION_CHANNEL_STATE = 2;
+  public static final int SOURCE_PACKAGE = 3;
+
+  private Context mContext;
+  private ContentResolver mContentResolver;
+  private Uri mSourceUri;
+
+  public VoicemailStatusQueryHelper(Context context) {
+    mContext = context;
+    mContentResolver = context.getContentResolver();
+    mSourceUri = VoicemailContract.Status.buildSourceUri(mContext.getPackageName());
+  }
+
+  /**
+   * Check if the configuration state for the voicemail source is "ok", meaning that the source is
+   * set up.
+   *
+   * @param phoneAccount The phone account for the voicemail source to check.
+   * @return {@code true} if the voicemail source is configured, {@code} false otherwise, including
+   *     if the voicemail source is not registered in the table.
+   */
+  public boolean isVoicemailSourceConfigured(PhoneAccountHandle phoneAccount) {
+    return isFieldEqualTo(phoneAccount, CONFIGURATION_STATE, Status.CONFIGURATION_STATE_OK);
+  }
+
+  /**
+   * Check if the notifications channel of a voicemail source is active. That is, when a new
+   * voicemail is available, if the server able to notify the device.
+   *
+   * @return {@code true} if notifications channel is active, {@code false} otherwise.
+   */
+  public boolean isNotificationsChannelActive(PhoneAccountHandle phoneAccount) {
+    return isFieldEqualTo(
+        phoneAccount, NOTIFICATION_CHANNEL_STATE, Status.NOTIFICATION_CHANNEL_STATE_OK);
+  }
+
+  /**
+   * Check if a field for an entry in the status table is equal to a specific value.
+   *
+   * @param phoneAccount The phone account of the voicemail source to query for.
+   * @param columnIndex The column index of the field in the returned query.
+   * @param value The value to compare against.
+   * @return {@code true} if the stored value is equal to the provided value. {@code false}
+   *     otherwise.
+   */
+  private boolean isFieldEqualTo(PhoneAccountHandle phoneAccount, int columnIndex, int value) {
+    Cursor cursor = null;
+    if (phoneAccount != null) {
+      String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString();
+      String phoneAccountId = phoneAccount.getId();
+      if (phoneAccountComponentName == null || phoneAccountId == null) {
+        return false;
+      }
+      try {
+        String whereClause =
+            Status.PHONE_ACCOUNT_COMPONENT_NAME
+                + "=? AND "
+                + Status.PHONE_ACCOUNT_ID
+                + "=? AND "
+                + Status.SOURCE_PACKAGE
+                + "=?";
+        String[] whereArgs = {phoneAccountComponentName, phoneAccountId, mContext.getPackageName()};
+        cursor = mContentResolver.query(mSourceUri, PROJECTION, whereClause, whereArgs, null);
+        if (cursor != null && cursor.moveToFirst()) {
+          return cursor.getInt(columnIndex) == value;
+        }
+      } finally {
+        if (cursor != null) {
+          cursor.close();
+        }
+      }
+    }
+    return false;
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java b/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java
new file mode 100644
index 0000000..d129406
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sync;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.telecom.PhoneAccountHandle;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.impl.Voicemail;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Construct queries to interact with the voicemails table. */
+public class VoicemailsQueryHelper {
+  static final String[] PROJECTION =
+      new String[] {
+        Voicemails._ID, // 0
+        Voicemails.SOURCE_DATA, // 1
+        Voicemails.IS_READ, // 2
+        Voicemails.DELETED, // 3
+        Voicemails.TRANSCRIPTION // 4
+      };
+
+  public static final int _ID = 0;
+  public static final int SOURCE_DATA = 1;
+  public static final int IS_READ = 2;
+  public static final int DELETED = 3;
+  public static final int TRANSCRIPTION = 4;
+
+  static final String READ_SELECTION =
+      Voicemails.DIRTY + "=1 AND " + Voicemails.DELETED + "!=1 AND " + Voicemails.IS_READ + "=1";
+  static final String DELETED_SELECTION = Voicemails.DELETED + "=1";
+  static final String ARCHIVED_SELECTION = Voicemails.ARCHIVED + "=0";
+
+  private Context mContext;
+  private ContentResolver mContentResolver;
+  private Uri mSourceUri;
+
+  public VoicemailsQueryHelper(Context context) {
+    mContext = context;
+    mContentResolver = context.getContentResolver();
+    mSourceUri = VoicemailContract.Voicemails.buildSourceUri(mContext.getPackageName());
+  }
+
+  /**
+   * Get all the local read voicemails that have not been synced to the server.
+   *
+   * @return A list of read voicemails.
+   */
+  public List<Voicemail> getReadVoicemails() {
+    return getLocalVoicemails(READ_SELECTION);
+  }
+
+  /**
+   * Get all the locally deleted voicemails that have not been synced to the server.
+   *
+   * @return A list of deleted voicemails.
+   */
+  public List<Voicemail> getDeletedVoicemails() {
+    return getLocalVoicemails(DELETED_SELECTION);
+  }
+
+  /**
+   * Get all voicemails locally stored.
+   *
+   * @return A list of all locally stored voicemails.
+   */
+  public List<Voicemail> getAllVoicemails() {
+    return getLocalVoicemails(null);
+  }
+
+  /**
+   * Utility method to make queries to the voicemail database.
+   *
+   * @param selection A filter declaring which rows to return. {@code null} returns all rows.
+   * @return A list of voicemails according to the selection statement.
+   */
+  private List<Voicemail> getLocalVoicemails(String selection) {
+    Cursor cursor = mContentResolver.query(mSourceUri, PROJECTION, selection, null, null);
+    if (cursor == null) {
+      return null;
+    }
+    try {
+      List<Voicemail> voicemails = new ArrayList<Voicemail>();
+      while (cursor.moveToNext()) {
+        final long id = cursor.getLong(_ID);
+        final String sourceData = cursor.getString(SOURCE_DATA);
+        final boolean isRead = cursor.getInt(IS_READ) == 1;
+        final String transcription = cursor.getString(TRANSCRIPTION);
+        Voicemail voicemail =
+            Voicemail.createForUpdate(id, sourceData)
+                .setIsRead(isRead)
+                .setTranscription(transcription)
+                .build();
+        voicemails.add(voicemail);
+      }
+      return voicemails;
+    } finally {
+      cursor.close();
+    }
+  }
+
+  /**
+   * Deletes a list of voicemails from the voicemail content provider.
+   *
+   * @param voicemails The list of voicemails to delete
+   * @return The number of voicemails deleted
+   */
+  public int deleteFromDatabase(List<Voicemail> voicemails) {
+    int count = voicemails.size();
+    if (count == 0) {
+      return 0;
+    }
+
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < count; i++) {
+      if (i > 0) {
+        sb.append(",");
+      }
+      sb.append(voicemails.get(i).getId());
+    }
+
+    String selectionStatement = String.format(Voicemails._ID + " IN (%s)", sb.toString());
+    return mContentResolver.delete(Voicemails.CONTENT_URI, selectionStatement, null);
+  }
+
+  /** Utility method to delete a single voicemail that is not archived. */
+  public void deleteNonArchivedFromDatabase(Voicemail voicemail) {
+    mContentResolver.delete(
+        Voicemails.CONTENT_URI,
+        Voicemails._ID + "=? AND " + Voicemails.ARCHIVED + "= 0",
+        new String[] {Long.toString(voicemail.getId())});
+  }
+
+  public int markReadInDatabase(List<Voicemail> voicemails) {
+    int count = voicemails.size();
+    for (int i = 0; i < count; i++) {
+      markReadInDatabase(voicemails.get(i));
+    }
+    return count;
+  }
+
+  /** Utility method to mark single message as read. */
+  public void markReadInDatabase(Voicemail voicemail) {
+    Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
+    ContentValues contentValues = new ContentValues();
+    contentValues.put(Voicemails.IS_READ, "1");
+    mContentResolver.update(uri, contentValues, null, null);
+  }
+
+  /**
+   * Sends an update command to the voicemail content provider for a list of voicemails. From the
+   * view of the provider, since the updater is the owner of the entry, a blank "update" means that
+   * the voicemail source is indicating that the server has up-to-date information on the voicemail.
+   * This flips the "dirty" bit to "0".
+   *
+   * @param voicemails The list of voicemails to update
+   * @return The number of voicemails updated
+   */
+  public int markCleanInDatabase(List<Voicemail> voicemails) {
+    int count = voicemails.size();
+    for (int i = 0; i < count; i++) {
+      markCleanInDatabase(voicemails.get(i));
+    }
+    return count;
+  }
+
+  /** Utility method to mark single message as clean. */
+  public void markCleanInDatabase(Voicemail voicemail) {
+    Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
+    ContentValues contentValues = new ContentValues();
+    mContentResolver.update(uri, contentValues, null, null);
+  }
+
+  /** Utility method to add a transcription to the voicemail. */
+  public void updateWithTranscription(Voicemail voicemail, String transcription) {
+    Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
+    ContentValues contentValues = new ContentValues();
+    contentValues.put(Voicemails.TRANSCRIPTION, transcription);
+    mContentResolver.update(uri, contentValues, null, null);
+  }
+
+  /**
+   * Voicemail is unique if the tuple of (phone account component name, phone account id, source
+   * data) is unique. If the phone account is missing, we also consider this unique since it's
+   * simply an "unknown" account.
+   *
+   * @param voicemail The voicemail to check if it is unique.
+   * @return {@code true} if the voicemail is unique, {@code false} otherwise.
+   */
+  public boolean isVoicemailUnique(Voicemail voicemail) {
+    Cursor cursor = null;
+    PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount();
+    if (phoneAccount != null) {
+      String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString();
+      String phoneAccountId = phoneAccount.getId();
+      String sourceData = voicemail.getSourceData();
+      if (phoneAccountComponentName == null || phoneAccountId == null || sourceData == null) {
+        return true;
+      }
+      try {
+        String whereClause =
+            Voicemails.PHONE_ACCOUNT_COMPONENT_NAME
+                + "=? AND "
+                + Voicemails.PHONE_ACCOUNT_ID
+                + "=? AND "
+                + Voicemails.SOURCE_DATA
+                + "=?";
+        String[] whereArgs = {phoneAccountComponentName, phoneAccountId, sourceData};
+        cursor = mContentResolver.query(mSourceUri, PROJECTION, whereClause, whereArgs, null);
+        if (cursor.getCount() == 0) {
+          return true;
+        } else {
+          return false;
+        }
+      } finally {
+        if (cursor != null) {
+          cursor.close();
+        }
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Marks voicemails in the local database as archived. This indicates that the voicemails from the
+   * server were removed automatically to make space for new voicemails, and are stored locally on
+   * the users devices, without a corresponding server copy.
+   */
+  public void markArchivedInDatabase(List<Voicemail> voicemails) {
+    for (Voicemail voicemail : voicemails) {
+      markArchiveInDatabase(voicemail);
+    }
+  }
+
+  /** Utility method to mark single voicemail as archived. */
+  public void markArchiveInDatabase(Voicemail voicemail) {
+    Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
+    ContentValues contentValues = new ContentValues();
+    contentValues.put(Voicemails.ARCHIVED, "1");
+    mContentResolver.update(uri, contentValues, null, null);
+  }
+
+  /** Find the oldest voicemails that are on the device, and also on the server. */
+  @TargetApi(VERSION_CODES.M) // used for try with resources
+  public List<Voicemail> oldestVoicemailsOnServer(int numVoicemails) {
+    if (numVoicemails <= 0) {
+      Assert.fail("Query for remote voicemails cannot be <= 0");
+    }
+
+    String sortAndLimit = "date ASC limit " + numVoicemails;
+
+    try (Cursor cursor =
+        mContentResolver.query(mSourceUri, null, ARCHIVED_SELECTION, null, sortAndLimit)) {
+
+      Assert.isNotNull(cursor);
+
+      List<Voicemail> voicemails = new ArrayList<>();
+      while (cursor.moveToNext()) {
+        final String sourceData = cursor.getString(SOURCE_DATA);
+        Voicemail voicemail = Voicemail.createForUpdate(cursor.getLong(_ID), sourceData).build();
+        voicemails.add(voicemail);
+      }
+
+      if (voicemails.size() != numVoicemails) {
+        Assert.fail(
+            String.format(
+                "voicemail count (%d) doesn't matched expected (%d)",
+                voicemails.size(), numVoicemails));
+      }
+      return voicemails;
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/VvmAccountManager.java b/java/com/android/voicemail/impl/sync/VvmAccountManager.java
new file mode 100644
index 0000000..05f6494
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VvmAccountManager.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sync;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.sms.StatusMessage;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tracks the activation state of a visual voicemail phone account. An account is considered
+ * activated if it has valid connection information from the {@link StatusMessage} stored on the
+ * device. Once activation/provisioning is completed, {@link #addAccount(Context,
+ * PhoneAccountHandle, StatusMessage)} should be called to store the connection information. When an
+ * account is removed or if the connection information is deemed invalid, {@link
+ * #removeAccount(Context, PhoneAccountHandle)} should be called to clear the connection information
+ * and allow reactivation.
+ */
+public class VvmAccountManager {
+  public static final String TAG = "VvmAccountManager";
+
+  private static final String IS_ACCOUNT_ACTIVATED = "is_account_activated";
+
+  public static void addAccount(
+      Context context, PhoneAccountHandle phoneAccountHandle, StatusMessage statusMessage) {
+    VisualVoicemailPreferences preferences =
+        new VisualVoicemailPreferences(context, phoneAccountHandle);
+    statusMessage.putStatus(preferences.edit()).putBoolean(IS_ACCOUNT_ACTIVATED, true).apply();
+  }
+
+  public static void removeAccount(Context context, PhoneAccountHandle phoneAccount) {
+    VoicemailStatus.disable(context, phoneAccount);
+    VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(context, phoneAccount);
+    preferences
+        .edit()
+        .putBoolean(IS_ACCOUNT_ACTIVATED, false)
+        .putString(OmtpConstants.IMAP_USER_NAME, null)
+        .putString(OmtpConstants.IMAP_PASSWORD, null)
+        .apply();
+  }
+
+  public static boolean isAccountActivated(Context context, PhoneAccountHandle phoneAccount) {
+    Assert.isNotNull(phoneAccount);
+    VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(context, phoneAccount);
+    return preferences.getBoolean(IS_ACCOUNT_ACTIVATED, false);
+  }
+
+  @NonNull
+  public static List<PhoneAccountHandle> getActiveAccounts(Context context) {
+    List<PhoneAccountHandle> results = new ArrayList<>();
+    for (PhoneAccountHandle phoneAccountHandle :
+        context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) {
+      if (isAccountActivated(context, phoneAccountHandle)) {
+        results.add(phoneAccountHandle);
+      }
+    }
+    return results;
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/VvmNetworkRequest.java b/java/com/android/voicemail/impl/sync/VvmNetworkRequest.java
new file mode 100644
index 0000000..189dc8f
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VvmNetworkRequest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.sync;
+
+import android.annotation.TargetApi;
+import android.net.Network;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import java.io.Closeable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+/**
+ * Class to retrieve a {@link Network} synchronously. {@link #getNetwork(OmtpVvmCarrierConfigHelper,
+ * PhoneAccountHandle)} will block until a suitable network is retrieved or it has failed.
+ */
+@SuppressWarnings("AndroidApiChecker") /* CompletableFuture is java8*/
+@TargetApi(VERSION_CODES.O)
+public class VvmNetworkRequest {
+
+  private static final String TAG = "VvmNetworkRequest";
+
+  /**
+   * A wrapper around a Network returned by a {@link VvmNetworkRequestCallback}, which should be
+   * closed once not needed anymore.
+   */
+  public static class NetworkWrapper implements Closeable {
+
+    private final Network mNetwork;
+    private final VvmNetworkRequestCallback mCallback;
+
+    private NetworkWrapper(Network network, VvmNetworkRequestCallback callback) {
+      mNetwork = network;
+      mCallback = callback;
+    }
+
+    public Network get() {
+      return mNetwork;
+    }
+
+    @Override
+    public void close() {
+      mCallback.releaseNetwork();
+    }
+  }
+
+  public static class RequestFailedException extends Exception {
+
+    private RequestFailedException(Throwable cause) {
+      super(cause);
+    }
+  }
+
+  @NonNull
+  public static NetworkWrapper getNetwork(
+      OmtpVvmCarrierConfigHelper config, PhoneAccountHandle handle, VoicemailStatus.Editor status)
+      throws RequestFailedException {
+    FutureNetworkRequestCallback callback =
+        new FutureNetworkRequestCallback(config, handle, status);
+    callback.requestNetwork();
+    try {
+      return callback.getFuture().get();
+    } catch (InterruptedException | ExecutionException e) {
+      callback.releaseNetwork();
+      VvmLog.e(TAG, "can't get future network", e);
+      throw new RequestFailedException(e);
+    }
+  }
+
+  private static class FutureNetworkRequestCallback extends VvmNetworkRequestCallback {
+
+    /**
+     * {@link CompletableFuture#get()} will block until {@link CompletableFuture# complete(Object) }
+     * has been called on the other thread.
+     */
+    private final CompletableFuture<NetworkWrapper> mFuture = new CompletableFuture<>();
+
+    public FutureNetworkRequestCallback(
+        OmtpVvmCarrierConfigHelper config,
+        PhoneAccountHandle phoneAccount,
+        VoicemailStatus.Editor status) {
+      super(config, phoneAccount, status);
+    }
+
+    public Future<NetworkWrapper> getFuture() {
+      return mFuture;
+    }
+
+    @Override
+    public void onAvailable(Network network) {
+      super.onAvailable(network);
+      mFuture.complete(new NetworkWrapper(network, this));
+    }
+
+    @Override
+    public void onFailed(String reason) {
+      super.onFailed(reason);
+      mFuture.complete(null);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java b/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java
new file mode 100644
index 0000000..067eff8
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemail.impl.sync;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build.VERSION_CODES;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.CallSuper;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+
+/**
+ * Base class for network request call backs for visual voicemail syncing with the Imap server. This
+ * handles retries and network requests.
+ */
+@TargetApi(VERSION_CODES.O)
+public abstract class VvmNetworkRequestCallback extends ConnectivityManager.NetworkCallback {
+
+  private static final String TAG = "VvmNetworkRequest";
+
+  // Timeout used to call ConnectivityManager.requestNetwork
+  private static final int NETWORK_REQUEST_TIMEOUT_MILLIS = 60 * 1000;
+
+  public static final String NETWORK_REQUEST_FAILED_TIMEOUT = "timeout";
+  public static final String NETWORK_REQUEST_FAILED_LOST = "lost";
+
+  protected Context mContext;
+  protected PhoneAccountHandle mPhoneAccount;
+  protected NetworkRequest mNetworkRequest;
+  private ConnectivityManager mConnectivityManager;
+  private final OmtpVvmCarrierConfigHelper mCarrierConfigHelper;
+  private final VoicemailStatus.Editor mStatus;
+  private boolean mRequestSent = false;
+  private boolean mResultReceived = false;
+
+  public VvmNetworkRequestCallback(
+      Context context, PhoneAccountHandle phoneAccount, VoicemailStatus.Editor status) {
+    mContext = context;
+    mPhoneAccount = phoneAccount;
+    mStatus = status;
+    mCarrierConfigHelper = new OmtpVvmCarrierConfigHelper(context, mPhoneAccount);
+    mNetworkRequest = createNetworkRequest();
+  }
+
+  public VvmNetworkRequestCallback(
+      OmtpVvmCarrierConfigHelper config,
+      PhoneAccountHandle phoneAccount,
+      VoicemailStatus.Editor status) {
+    mContext = config.getContext();
+    mPhoneAccount = phoneAccount;
+    mStatus = status;
+    mCarrierConfigHelper = config;
+    mNetworkRequest = createNetworkRequest();
+  }
+
+  public VoicemailStatus.Editor getVoicemailStatusEditor() {
+    return mStatus;
+  }
+
+  /**
+   * @return NetworkRequest for a proper transport type. Use only cellular network if the carrier
+   *     requires it. Otherwise use whatever available.
+   */
+  private NetworkRequest createNetworkRequest() {
+
+    NetworkRequest.Builder builder =
+        new NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+
+    TelephonyManager telephonyManager =
+        mContext
+            .getSystemService(TelephonyManager.class)
+            .createForPhoneAccountHandle(mPhoneAccount);
+    // At this point mPhoneAccount should always be valid and telephonyManager will never be null
+    Assert.isNotNull(telephonyManager);
+    if (mCarrierConfigHelper.isCellularDataRequired()) {
+      VvmLog.d(TAG, "Transport type: CELLULAR");
+      builder
+          .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+          .setNetworkSpecifier(telephonyManager.getNetworkSpecifier());
+    } else {
+      VvmLog.d(TAG, "Transport type: ANY");
+    }
+    return builder.build();
+  }
+
+  public NetworkRequest getNetworkRequest() {
+    return mNetworkRequest;
+  }
+
+  @Override
+  @CallSuper
+  public void onLost(Network network) {
+    VvmLog.d(TAG, "onLost");
+    mResultReceived = true;
+    onFailed(NETWORK_REQUEST_FAILED_LOST);
+  }
+
+  @Override
+  @CallSuper
+  public void onAvailable(Network network) {
+    super.onAvailable(network);
+    mResultReceived = true;
+  }
+
+  @CallSuper
+  public void onUnavailable() {
+    // TODO: b/32637799 this is hidden, do we really need this?
+    mResultReceived = true;
+    onFailed(NETWORK_REQUEST_FAILED_TIMEOUT);
+  }
+
+  public void requestNetwork() {
+    if (mRequestSent == true) {
+      VvmLog.e(TAG, "requestNetwork() called twice");
+      return;
+    }
+    mRequestSent = true;
+    getConnectivityManager().requestNetwork(getNetworkRequest(), this);
+    /**
+     * Somehow requestNetwork() with timeout doesn't work, and it's a hidden method. Implement our
+     * own timeout mechanism instead.
+     */
+    Handler handler = new Handler(Looper.getMainLooper());
+    handler.postDelayed(
+        new Runnable() {
+          @Override
+          public void run() {
+            if (mResultReceived == false) {
+              onFailed(NETWORK_REQUEST_FAILED_TIMEOUT);
+            }
+          }
+        },
+        NETWORK_REQUEST_TIMEOUT_MILLIS);
+  }
+
+  public void releaseNetwork() {
+    VvmLog.d(TAG, "releaseNetwork");
+    getConnectivityManager().unregisterNetworkCallback(this);
+  }
+
+  public ConnectivityManager getConnectivityManager() {
+    if (mConnectivityManager == null) {
+      mConnectivityManager =
+          (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+    }
+    return mConnectivityManager;
+  }
+
+  @CallSuper
+  public void onFailed(String reason) {
+    VvmLog.d(TAG, "onFailed: " + reason);
+    if (mCarrierConfigHelper.isCellularDataRequired()) {
+      mCarrierConfigHelper.handleEvent(mStatus, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
+    } else {
+      mCarrierConfigHelper.handleEvent(mStatus, OmtpEvents.DATA_NO_CONNECTION);
+    }
+    releaseNetwork();
+  }
+}
diff --git a/java/com/android/voicemail/impl/utils/IndentingPrintWriter.java b/java/com/android/voicemail/impl/utils/IndentingPrintWriter.java
new file mode 100644
index 0000000..bbc1d66
--- /dev/null
+++ b/java/com/android/voicemail/impl/utils/IndentingPrintWriter.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.utils;
+
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.util.Arrays;
+
+/**
+ * Lightweight wrapper around {@link PrintWriter} that automatically indents newlines based on
+ * internal state. It also automatically wraps long lines based on given line length.
+ *
+ * <p>Delays writing indent until first actual write on a newline, enabling indent modification
+ * after newline.
+ */
+public class IndentingPrintWriter extends PrintWriter {
+
+  private final String mSingleIndent;
+  private final int mWrapLength;
+
+  /** Mutable version of current indent */
+  private StringBuilder mIndentBuilder = new StringBuilder();
+  /** Cache of current {@link #mIndentBuilder} value */
+  private char[] mCurrentIndent;
+  /** Length of current line being built, excluding any indent */
+  private int mCurrentLength;
+
+  /**
+   * Flag indicating if we're currently sitting on an empty line, and that next write should be
+   * prefixed with the current indent.
+   */
+  private boolean mEmptyLine = true;
+
+  private char[] mSingleChar = new char[1];
+
+  public IndentingPrintWriter(Writer writer, String singleIndent) {
+    this(writer, singleIndent, -1);
+  }
+
+  public IndentingPrintWriter(Writer writer, String singleIndent, int wrapLength) {
+    super(writer);
+    mSingleIndent = singleIndent;
+    mWrapLength = wrapLength;
+  }
+
+  public void increaseIndent() {
+    mIndentBuilder.append(mSingleIndent);
+    mCurrentIndent = null;
+  }
+
+  public void decreaseIndent() {
+    mIndentBuilder.delete(0, mSingleIndent.length());
+    mCurrentIndent = null;
+  }
+
+  public void printPair(String key, Object value) {
+    print(key + "=" + String.valueOf(value) + " ");
+  }
+
+  public void printPair(String key, Object[] value) {
+    print(key + "=" + Arrays.toString(value) + " ");
+  }
+
+  public void printHexPair(String key, int value) {
+    print(key + "=0x" + Integer.toHexString(value) + " ");
+  }
+
+  @Override
+  public void println() {
+    write('\n');
+  }
+
+  @Override
+  public void write(int c) {
+    mSingleChar[0] = (char) c;
+    write(mSingleChar, 0, 1);
+  }
+
+  @Override
+  public void write(String s, int off, int len) {
+    final char[] buf = new char[len];
+    s.getChars(off, len - off, buf, 0);
+    write(buf, 0, len);
+  }
+
+  @Override
+  public void write(char[] buf, int offset, int count) {
+    final int indentLength = mIndentBuilder.length();
+    final int bufferEnd = offset + count;
+    int lineStart = offset;
+    int lineEnd = offset;
+
+    // March through incoming buffer looking for newlines
+    while (lineEnd < bufferEnd) {
+      char ch = buf[lineEnd++];
+      mCurrentLength++;
+      if (ch == '\n') {
+        maybeWriteIndent();
+        super.write(buf, lineStart, lineEnd - lineStart);
+        lineStart = lineEnd;
+        mEmptyLine = true;
+        mCurrentLength = 0;
+      }
+
+      // Wrap if we've pushed beyond line length
+      if (mWrapLength > 0 && mCurrentLength >= mWrapLength - indentLength) {
+        if (!mEmptyLine) {
+          // Give ourselves a fresh line to work with
+          super.write('\n');
+          mEmptyLine = true;
+          mCurrentLength = lineEnd - lineStart;
+        } else {
+          // We need more than a dedicated line, slice it hard
+          maybeWriteIndent();
+          super.write(buf, lineStart, lineEnd - lineStart);
+          super.write('\n');
+          mEmptyLine = true;
+          lineStart = lineEnd;
+          mCurrentLength = 0;
+        }
+      }
+    }
+
+    if (lineStart != lineEnd) {
+      maybeWriteIndent();
+      super.write(buf, lineStart, lineEnd - lineStart);
+    }
+  }
+
+  private void maybeWriteIndent() {
+    if (mEmptyLine) {
+      mEmptyLine = false;
+      if (mIndentBuilder.length() != 0) {
+        if (mCurrentIndent == null) {
+          mCurrentIndent = mIndentBuilder.toString().toCharArray();
+        }
+        super.write(mCurrentIndent, 0, mCurrentIndent.length);
+      }
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/utils/VoicemailDatabaseUtil.java b/java/com/android/voicemail/impl/utils/VoicemailDatabaseUtil.java
new file mode 100644
index 0000000..711d6a8
--- /dev/null
+++ b/java/com/android/voicemail/impl/utils/VoicemailDatabaseUtil.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.utils;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.VoicemailContract.Voicemails;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.Voicemail;
+import java.util.List;
+
+public class VoicemailDatabaseUtil {
+
+  /**
+   * Inserts a new voicemail into the voicemail content provider.
+   *
+   * @param context The context of the app doing the inserting
+   * @param voicemail Data to be inserted
+   * @return {@link Uri} of the newly inserted {@link Voicemail}
+   * @hide
+   */
+  public static Uri insert(Context context, Voicemail voicemail) {
+    ContentResolver contentResolver = context.getContentResolver();
+    ContentValues contentValues = getContentValues(voicemail);
+    return contentResolver.insert(
+        Voicemails.buildSourceUri(context.getPackageName()), contentValues);
+  }
+
+  /**
+   * Inserts a list of voicemails into the voicemail content provider.
+   *
+   * @param context The context of the app doing the inserting
+   * @param voicemails Data to be inserted
+   * @return the number of voicemails inserted
+   * @hide
+   */
+  public static int insert(Context context, List<Voicemail> voicemails) {
+    for (Voicemail voicemail : voicemails) {
+      insert(context, voicemail);
+    }
+    return voicemails.size();
+  }
+
+  /** Maps structured {@link Voicemail} to {@link ContentValues} in content provider. */
+  private static ContentValues getContentValues(Voicemail voicemail) {
+    ContentValues contentValues = new ContentValues();
+    contentValues.put(Voicemails.DATE, String.valueOf(voicemail.getTimestampMillis()));
+    contentValues.put(Voicemails.NUMBER, voicemail.getNumber());
+    contentValues.put(Voicemails.DURATION, String.valueOf(voicemail.getDuration()));
+    contentValues.put(Voicemails.SOURCE_PACKAGE, voicemail.getSourcePackage());
+    contentValues.put(Voicemails.SOURCE_DATA, voicemail.getSourceData());
+    contentValues.put(Voicemails.IS_READ, voicemail.isRead() ? 1 : 0);
+    contentValues.put(Voicemails.IS_OMTP_VOICEMAIL, 1);
+
+    PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount();
+    if (phoneAccount != null) {
+      contentValues.put(
+          Voicemails.PHONE_ACCOUNT_COMPONENT_NAME,
+          phoneAccount.getComponentName().flattenToString());
+      contentValues.put(Voicemails.PHONE_ACCOUNT_ID, phoneAccount.getId());
+    }
+
+    if (voicemail.getTranscription() != null) {
+      contentValues.put(Voicemails.TRANSCRIPTION, voicemail.getTranscription());
+    }
+
+    return contentValues;
+  }
+}
diff --git a/java/com/android/voicemail/impl/utils/VvmDumpHandler.java b/java/com/android/voicemail/impl/utils/VvmDumpHandler.java
new file mode 100644
index 0000000..5290f2c
--- /dev/null
+++ b/java/com/android/voicemail/impl/utils/VvmDumpHandler.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.utils;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VvmLog;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+public class VvmDumpHandler {
+
+  public static void dump(Context context, FileDescriptor fd, PrintWriter writer, String[] args) {
+    IndentingPrintWriter indentedWriter = new IndentingPrintWriter(writer, "  ");
+    indentedWriter.println("******* OmtpVvm *******");
+    indentedWriter.println("======= Configs =======");
+    indentedWriter.increaseIndent();
+    for (PhoneAccountHandle handle :
+        context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) {
+      OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(context, handle);
+      indentedWriter.println(config.toString());
+    }
+    indentedWriter.decreaseIndent();
+    indentedWriter.println("======== Logs =========");
+    VvmLog.dump(fd, indentedWriter, args);
+  }
+}
diff --git a/java/com/android/voicemail/impl/utils/XmlUtils.java b/java/com/android/voicemail/impl/utils/XmlUtils.java
new file mode 100644
index 0000000..f5703f3
--- /dev/null
+++ b/java/com/android/voicemail/impl/utils/XmlUtils.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.voicemail.impl.utils;
+
+import android.util.ArrayMap;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+public class XmlUtils {
+
+  public static final ArrayMap<String, ?> readThisArrayMapXml(
+      XmlPullParser parser, String endTag, String[] name, ReadMapCallback callback)
+      throws XmlPullParserException, java.io.IOException {
+    ArrayMap<String, Object> map = new ArrayMap<>();
+
+    int eventType = parser.getEventType();
+    do {
+      if (eventType == XmlPullParser.START_TAG) {
+        Object val = readThisValueXml(parser, name, callback, true);
+        map.put(name[0], val);
+      } else if (eventType == XmlPullParser.END_TAG) {
+        if (parser.getName().equals(endTag)) {
+          return map;
+        }
+        throw new XmlPullParserException("Expected " + endTag + " end tag at: " + parser.getName());
+      }
+      eventType = parser.next();
+    } while (eventType != XmlPullParser.END_DOCUMENT);
+
+    throw new XmlPullParserException("Document ended before " + endTag + " end tag");
+  }
+
+  /**
+   * Read an ArrayList object from an XmlPullParser. The XML data could previously have been
+   * generated by writeListXml(). The XmlPullParser must be positioned <em>after</em> the tag that
+   * begins the list.
+   *
+   * @param parser The XmlPullParser from which to read the list data.
+   * @param endTag Name of the tag that will end the list, usually "list".
+   * @param name An array of one string, used to return the name attribute of the list's tag.
+   * @return HashMap The newly generated list.
+   */
+  public static final ArrayList readThisListXml(
+      XmlPullParser parser,
+      String endTag,
+      String[] name,
+      ReadMapCallback callback,
+      boolean arrayMap)
+      throws XmlPullParserException, java.io.IOException {
+    ArrayList list = new ArrayList();
+
+    int eventType = parser.getEventType();
+    do {
+      if (eventType == XmlPullParser.START_TAG) {
+        Object val = readThisValueXml(parser, name, callback, arrayMap);
+        list.add(val);
+      } else if (eventType == XmlPullParser.END_TAG) {
+        if (parser.getName().equals(endTag)) {
+          return list;
+        }
+        throw new XmlPullParserException("Expected " + endTag + " end tag at: " + parser.getName());
+      }
+      eventType = parser.next();
+    } while (eventType != XmlPullParser.END_DOCUMENT);
+
+    throw new XmlPullParserException("Document ended before " + endTag + " end tag");
+  }
+
+  /**
+   * Read a String[] object from an XmlPullParser. The XML data could previously have been generated
+   * by writeStringArrayXml(). The XmlPullParser must be positioned <em>after</em> the tag that
+   * begins the list.
+   *
+   * @param parser The XmlPullParser from which to read the list data.
+   * @param endTag Name of the tag that will end the list, usually "string-array".
+   * @param name An array of one string, used to return the name attribute of the list's tag.
+   * @return Returns a newly generated String[].
+   */
+  public static String[] readThisStringArrayXml(XmlPullParser parser, String endTag, String[] name)
+      throws XmlPullParserException, java.io.IOException {
+
+    parser.next();
+
+    List<String> array = new ArrayList<>();
+
+    int eventType = parser.getEventType();
+    do {
+      if (eventType == XmlPullParser.START_TAG) {
+        if (parser.getName().equals("item")) {
+          try {
+            array.add(parser.getAttributeValue(null, "value"));
+          } catch (NullPointerException e) {
+            throw new XmlPullParserException("Need value attribute in item");
+          } catch (NumberFormatException e) {
+            throw new XmlPullParserException("Not a number in value attribute in item");
+          }
+        } else {
+          throw new XmlPullParserException("Expected item tag at: " + parser.getName());
+        }
+      } else if (eventType == XmlPullParser.END_TAG) {
+        if (parser.getName().equals(endTag)) {
+          return array.toArray(new String[0]);
+        } else if (parser.getName().equals("item")) {
+
+        } else {
+          throw new XmlPullParserException(
+              "Expected " + endTag + " end tag at: " + parser.getName());
+        }
+      }
+      eventType = parser.next();
+    } while (eventType != XmlPullParser.END_DOCUMENT);
+
+    throw new XmlPullParserException("Document ended before " + endTag + " end tag");
+  }
+
+  private static Object readThisValueXml(
+      XmlPullParser parser, String[] name, ReadMapCallback callback, boolean arrayMap)
+      throws XmlPullParserException, java.io.IOException {
+    final String valueName = parser.getAttributeValue(null, "name");
+    final String tagName = parser.getName();
+
+    Object res;
+
+    if (tagName.equals("null")) {
+      res = null;
+    } else if (tagName.equals("string")) {
+      String value = "";
+      int eventType;
+      while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) {
+        if (eventType == XmlPullParser.END_TAG) {
+          if (parser.getName().equals("string")) {
+            name[0] = valueName;
+            return value;
+          }
+          throw new XmlPullParserException("Unexpected end tag in <string>: " + parser.getName());
+        } else if (eventType == XmlPullParser.TEXT) {
+          value += parser.getText();
+        } else if (eventType == XmlPullParser.START_TAG) {
+          throw new XmlPullParserException("Unexpected start tag in <string>: " + parser.getName());
+        }
+      }
+      throw new XmlPullParserException("Unexpected end of document in <string>");
+    } else if ((res = readThisPrimitiveValueXml(parser, tagName)) != null) {
+      // all work already done by readThisPrimitiveValueXml
+    } else if (tagName.equals("string-array")) {
+      res = readThisStringArrayXml(parser, "string-array", name);
+      name[0] = valueName;
+      return res;
+    } else if (tagName.equals("list")) {
+      parser.next();
+      res = readThisListXml(parser, "list", name, callback, arrayMap);
+      name[0] = valueName;
+      return res;
+    } else if (callback != null) {
+      res = callback.readThisUnknownObjectXml(parser, tagName);
+      name[0] = valueName;
+      return res;
+    } else {
+      throw new XmlPullParserException("Unknown tag: " + tagName);
+    }
+
+    // Skip through to end tag.
+    int eventType;
+    while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) {
+      if (eventType == XmlPullParser.END_TAG) {
+        if (parser.getName().equals(tagName)) {
+          name[0] = valueName;
+          return res;
+        }
+        throw new XmlPullParserException(
+            "Unexpected end tag in <" + tagName + ">: " + parser.getName());
+      } else if (eventType == XmlPullParser.TEXT) {
+        throw new XmlPullParserException(
+            "Unexpected text in <" + tagName + ">: " + parser.getName());
+      } else if (eventType == XmlPullParser.START_TAG) {
+        throw new XmlPullParserException(
+            "Unexpected start tag in <" + tagName + ">: " + parser.getName());
+      }
+    }
+    throw new XmlPullParserException("Unexpected end of document in <" + tagName + ">");
+  }
+
+  private static final Object readThisPrimitiveValueXml(XmlPullParser parser, String tagName)
+      throws XmlPullParserException, java.io.IOException {
+    try {
+      if (tagName.equals("int")) {
+        return Integer.parseInt(parser.getAttributeValue(null, "value"));
+      } else if (tagName.equals("long")) {
+        return Long.valueOf(parser.getAttributeValue(null, "value"));
+      } else if (tagName.equals("float")) {
+        return Float.valueOf(parser.getAttributeValue(null, "value"));
+      } else if (tagName.equals("double")) {
+        return Double.valueOf(parser.getAttributeValue(null, "value"));
+      } else if (tagName.equals("boolean")) {
+        return Boolean.valueOf(parser.getAttributeValue(null, "value"));
+      } else {
+        return null;
+      }
+    } catch (NullPointerException e) {
+      throw new XmlPullParserException("Need value attribute in <" + tagName + ">");
+    } catch (NumberFormatException e) {
+      throw new XmlPullParserException("Not a number in value attribute in <" + tagName + ">");
+    }
+  }
+
+  public interface ReadMapCallback {
+
+    /**
+     * Called from readThisMapXml when a START_TAG is not recognized. The input stream is positioned
+     * within the start tag so that attributes can be read using in.getAttribute.
+     *
+     * @param in the XML input stream
+     * @param tag the START_TAG that was not recognized.
+     * @return the Object parsed from the stream which will be put into the map.
+     * @throws XmlPullParserException if the START_TAG is not recognized.
+     * @throws IOException on XmlPullParser serialization errors.
+     */
+    Object readThisUnknownObjectXml(XmlPullParser in, String tag)
+        throws XmlPullParserException, IOException;
+  }
+}
diff --git a/java/com/android/voicemailomtp/permissions.xml b/java/com/android/voicemail/permissions.xml
similarity index 93%
rename from java/com/android/voicemailomtp/permissions.xml
rename to java/com/android/voicemail/permissions.xml
index 9326d80..adb4b6f 100644
--- a/java/com/android/voicemailomtp/permissions.xml
+++ b/java/com/android/voicemail/permissions.xml
@@ -7,9 +7,9 @@
 
   <!-- Applications using this module should merge these permissions using android_manifest_merge -->
 
-  <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL" />
-  <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL" />
-  <uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL" />
+  <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL"/>
+  <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL"/>
+  <uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL"/>
   <uses-permission android:name="android.permission.WAKE_LOCK"/>
   <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
   <uses-permission android:name="android.permission.SEND_SMS"/>
diff --git a/java/com/android/voicemail/stub/StubVoicemailClient.java b/java/com/android/voicemail/stub/StubVoicemailClient.java
new file mode 100644
index 0000000..9481a0e
--- /dev/null
+++ b/java/com/android/voicemail/stub/StubVoicemailClient.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.stub;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.VoicemailClient;
+import java.util.List;
+import javax.inject.Inject;
+
+/**
+ * A no-op version of the voicemail module for build targets that don't support the new OTMP client.
+ */
+public final class StubVoicemailClient implements VoicemailClient {
+  @Inject
+  public StubVoicemailClient() {}
+
+  @Override
+  public void appendOmtpVoicemailSelectionClause(
+      Context context, StringBuilder where, List<String> selectionArgs) {}
+
+  @Override
+  public String getSettingsFragment() {
+    return null;
+  }
+
+  @Override
+  public boolean isVoicemailArchiveEnabled(Context context, PhoneAccountHandle phoneAccountHandle) {
+    return false;
+  }
+
+  @Override
+  public void setVoicemailArchiveEnabled(
+      Context context, PhoneAccountHandle phoneAccountHandle, boolean value) {}
+}
diff --git a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java b/java/com/android/voicemail/stub/StubVoicemailModule.java
similarity index 65%
copy from java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
copy to java/com/android/voicemail/stub/StubVoicemailModule.java
index 39c55d0..6c1552c 100644
--- a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
+++ b/java/com/android/voicemail/stub/StubVoicemailModule.java
@@ -14,19 +14,20 @@
  * limitations under the License
  */
 
-package com.android.dialer.enrichedcall;
+package com.android.voicemail.stub;
 
+import com.android.voicemail.VoicemailClient;
+import dagger.Binds;
 import dagger.Module;
-import dagger.Provides;
 import javax.inject.Singleton;
 
-/** Module which binds {@link EnrichedCallManagerStub}. */
+/**
+ * A no-op version of the voicemail module for build targets that don't support the new OTMP client.
+ */
 @Module
-public class StubEnrichedCallModule {
+public abstract class StubVoicemailModule {
 
-  @Provides
+  @Binds
   @Singleton
-  static EnrichedCallManager provideEnrichedCallManager() {
-    return new EnrichedCallManagerStub();
-  }
+  public abstract VoicemailClient bindVoicemailClient(StubVoicemailClient voicemailClient);
 }
diff --git a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java b/java/com/android/voicemail/testing/TestVoicemailModule.java
similarity index 61%
copy from java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
copy to java/com/android/voicemail/testing/TestVoicemailModule.java
index 39c55d0..8b7b34c 100644
--- a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
+++ b/java/com/android/voicemail/testing/TestVoicemailModule.java
@@ -14,19 +14,25 @@
  * limitations under the License
  */
 
-package com.android.dialer.enrichedcall;
+package com.android.voicemail.testing;
 
+import com.android.voicemail.VoicemailClient;
 import dagger.Module;
 import dagger.Provides;
 import javax.inject.Singleton;
 
-/** Module which binds {@link EnrichedCallManagerStub}. */
+/** Used to set a mock voicemail client for unit tests. */
 @Module
-public class StubEnrichedCallModule {
+public final class TestVoicemailModule {
+  private static VoicemailClient voicemailClient;
+
+  public static void setVoicemailClient(VoicemailClient voicemailClient) {
+    TestVoicemailModule.voicemailClient = voicemailClient;
+  }
 
   @Provides
   @Singleton
-  static EnrichedCallManager provideEnrichedCallManager() {
-    return new EnrichedCallManagerStub();
+  public static VoicemailClient provideVoicemailClient() {
+    return voicemailClient;
   }
 }
diff --git a/java/com/android/voicemailomtp/ActivationTask.java b/java/com/android/voicemailomtp/ActivationTask.java
deleted file mode 100644
index 7de81e6..0000000
--- a/java/com/android/voicemailomtp/ActivationTask.java
+++ /dev/null
@@ -1,305 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.Intent;
-import android.database.ContentObserver;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.provider.Settings;
-import android.provider.Settings.Global;
-import android.support.annotation.Nullable;
-import android.support.annotation.WorkerThread;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.ServiceState;
-import android.telephony.TelephonyManager;
-import com.android.voicemailomtp.protocol.VisualVoicemailProtocol;
-import com.android.voicemailomtp.scheduling.BaseTask;
-import com.android.voicemailomtp.scheduling.RetryPolicy;
-import com.android.voicemailomtp.sms.StatusMessage;
-import com.android.voicemailomtp.sms.StatusSmsFetcher;
-import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
-import com.android.voicemailomtp.sync.OmtpVvmSyncService;
-import com.android.voicemailomtp.sync.SyncTask;
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeoutException;
-
-/**
- * Task to activate the visual voicemail service. A request to activate VVM will be sent to the
- * carrier, which will respond with a STATUS SMS. The credentials will be updated from the SMS. If
- * the user is not provisioned provisioning will be attempted. Activation happens when the phone
- * boots, the SIM is inserted, signal returned when VVM is not activated yet, and when the carrier
- * spontaneously sent a STATUS SMS.
- */
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class ActivationTask extends BaseTask {
-
-    private static final String TAG = "VvmActivationTask";
-
-    private static final int RETRY_TIMES = 4;
-    private static final int RETRY_INTERVAL_MILLIS = 5_000;
-
-    private static final String EXTRA_MESSAGE_DATA_BUNDLE = "extra_message_data_bundle";
-
-    @Nullable
-    private static DeviceProvisionedObserver sDeviceProvisionedObserver;
-
-    private final RetryPolicy mRetryPolicy;
-
-    private Bundle mMessageData;
-
-    public ActivationTask() {
-        super(TASK_ACTIVATION);
-        mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS);
-        addPolicy(mRetryPolicy);
-    }
-
-    /**
-     * Has the user gone through the setup wizard yet.
-     */
-    private static boolean isDeviceProvisioned(Context context) {
-        return Settings.Global.getInt(
-            context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0) == 1;
-    }
-
-    /**
-     * @param messageData The optional bundle from {@link android.provider.VoicemailContract#
-     * EXTRA_VOICEMAIL_SMS_FIELDS}, if the task is initiated by a status SMS. If null the task will
-     * request a status SMS itself.
-     */
-    public static void start(Context context, PhoneAccountHandle phoneAccountHandle,
-            @Nullable Bundle messageData) {
-        if (!isDeviceProvisioned(context)) {
-            VvmLog.i(TAG, "Activation requested while device is not provisioned, postponing");
-            // Activation might need information such as system language to be set, so wait until
-            // the setup wizard is finished. The data bundle from the SMS will be re-requested upon
-            // activation.
-            queueActivationAfterProvisioned(context, phoneAccountHandle);
-            return;
-        }
-
-        Intent intent = BaseTask.createIntent(context, ActivationTask.class, phoneAccountHandle);
-        if (messageData != null) {
-            intent.putExtra(EXTRA_MESSAGE_DATA_BUNDLE, messageData);
-        }
-        context.startService(intent);
-    }
-
-    public void onCreate(Context context, Intent intent, int flags, int startId) {
-        super.onCreate(context, intent, flags, startId);
-        mMessageData = intent.getParcelableExtra(EXTRA_MESSAGE_DATA_BUNDLE);
-    }
-
-    @Override
-    public Intent createRestartIntent() {
-        Intent intent = super.createRestartIntent();
-        // mMessageData is discarded, request a fresh STATUS SMS for retries.
-        return intent;
-    }
-
-    @Override
-    @WorkerThread
-    public void onExecuteInBackgroundThread() {
-        Assert.isNotMainThread();
-
-        PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
-        if (phoneAccountHandle == null) {
-            // This should never happen
-            VvmLog.e(TAG, "null PhoneAccountHandle");
-            return;
-        }
-
-        OmtpVvmCarrierConfigHelper helper =
-                new OmtpVvmCarrierConfigHelper(getContext(), phoneAccountHandle);
-        if (!helper.isValid()) {
-            VvmLog.i(TAG, "VVM not supported on phoneAccountHandle " + phoneAccountHandle);
-            VoicemailStatus.disable(getContext(), phoneAccountHandle);
-            return;
-        }
-
-        // OmtpVvmCarrierConfigHelper can start the activation process; it will pass in a vvm
-        // content provider URI which we will use.  On some occasions, setting that URI will
-        // fail, so we will perform a few attempts to ensure that the vvm content provider has
-        // a good chance of being started up.
-        if (!VoicemailStatus.edit(getContext(), phoneAccountHandle)
-            .setType(helper.getVvmType())
-            .apply()) {
-            VvmLog.e(TAG, "Failed to configure content provider - " + helper.getVvmType());
-            fail();
-        }
-        VvmLog.i(TAG, "VVM content provider configured - " + helper.getVvmType());
-
-        if (!OmtpVvmSourceManager.getInstance(getContext())
-                .isVvmSourceRegistered(phoneAccountHandle)) {
-            // This account has not been activated before during the lifetime of this boot.
-            VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(getContext(),
-                    phoneAccountHandle);
-            if (preferences.getString(OmtpConstants.SERVER_ADDRESS, null) == null) {
-                // Only show the "activating" message if activation has not been completed before.
-                // Subsequent activations are more of a status check and usually does not
-                // concern the user.
-                helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle),
-                        OmtpEvents.CONFIG_ACTIVATING);
-            } else {
-                // The account has been activated on this device before. Pretend it is already
-                // activated. If there are any activation error it will overwrite this status.
-                helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle),
-                        OmtpEvents.CONFIG_ACTIVATING_SUBSEQUENT);
-            }
-
-        }
-        if (!hasSignal(getContext(), phoneAccountHandle)) {
-            VvmLog.i(TAG, "Service lost during activation, aborting");
-            // Restore the "NO SIGNAL" state since it will be overwritten by the CONFIG_ACTIVATING
-            // event.
-            helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle),
-                    OmtpEvents.NOTIFICATION_SERVICE_LOST);
-            // Don't retry, a new activation will be started after the signal returned.
-            return;
-        }
-
-        helper.activateSmsFilter();
-        VoicemailStatus.Editor status = mRetryPolicy.getVoicemailStatusEditor();
-
-        VisualVoicemailProtocol protocol = helper.getProtocol();
-
-        Bundle data;
-        if (mMessageData != null) {
-            // The content of STATUS SMS is provided to launch this task, no need to request it
-            // again.
-            data = mMessageData;
-        } else {
-            try (StatusSmsFetcher fetcher = new StatusSmsFetcher(getContext(),
-                    phoneAccountHandle)) {
-                protocol.startActivation(helper, fetcher.getSentIntent());
-                // Both the fetcher and OmtpMessageReceiver will be triggered, but
-                // OmtpMessageReceiver will just route the SMS back to ActivationTask, which will be
-                // rejected because the task is still running.
-                data = fetcher.get();
-            } catch (TimeoutException e) {
-                // The carrier is expected to return an STATUS SMS within STATUS_SMS_TIMEOUT_MILLIS
-                // handleEvent() will do the logging.
-                helper.handleEvent(status, OmtpEvents.CONFIG_STATUS_SMS_TIME_OUT);
-                fail();
-                return;
-            } catch (CancellationException e) {
-                VvmLog.e(TAG, "Unable to send status request SMS");
-                fail();
-                return;
-            } catch (InterruptedException | ExecutionException | IOException e) {
-                VvmLog.e(TAG, "can't get future STATUS SMS", e);
-                fail();
-                return;
-            }
-        }
-
-        StatusMessage message = new StatusMessage(data);
-        VvmLog.d(TAG, "STATUS SMS received: st=" + message.getProvisioningStatus()
-                + ", rc=" + message.getReturnCode());
-
-        if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_READY)) {
-            VvmLog.d(TAG, "subscriber ready, no activation required");
-            updateSource(getContext(), phoneAccountHandle, status, message);
-        } else {
-            if (helper.supportsProvisioning()) {
-                VvmLog.i(TAG, "Subscriber not ready, start provisioning");
-                helper.startProvisioning(this, phoneAccountHandle, status, message, data);
-
-            } else if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_NEW)) {
-                VvmLog.i(TAG, "Subscriber new but provisioning is not supported");
-                // Ignore the non-ready state and attempt to use the provided info as is.
-                // This is probably caused by not completing the new user tutorial.
-                updateSource(getContext(), phoneAccountHandle, status, message);
-            } else {
-                VvmLog.i(TAG, "Subscriber not ready but provisioning is not supported");
-                helper.handleEvent(status, OmtpEvents.CONFIG_SERVICE_NOT_AVAILABLE);
-            }
-        }
-    }
-
-    public static void updateSource(Context context, PhoneAccountHandle phone,
-            VoicemailStatus.Editor status, StatusMessage message) {
-        OmtpVvmSourceManager vvmSourceManager =
-                OmtpVvmSourceManager.getInstance(context);
-
-        if (OmtpConstants.SUCCESS.equals(message.getReturnCode())) {
-            OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(context, phone);
-            helper.handleEvent(status, OmtpEvents.CONFIG_REQUEST_STATUS_SUCCESS);
-
-            // Save the IMAP credentials in preferences so they are persistent and can be retrieved.
-            VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phone);
-            message.putStatus(prefs.edit()).apply();
-
-            // Add the source to indicate that it is active.
-            vvmSourceManager.addSource(phone);
-
-            SyncTask.start(context, phone, OmtpVvmSyncService.SYNC_FULL_SYNC);
-        } else {
-            VvmLog.e(TAG, "Visual voicemail not available for subscriber.");
-        }
-    }
-
-    private static boolean hasSignal(Context context, PhoneAccountHandle phoneAccountHandle) {
-        TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class)
-                .createForPhoneAccountHandle(phoneAccountHandle);
-        return telephonyManager.getServiceState().getState() == ServiceState.STATE_IN_SERVICE;
-    }
-
-    private static void queueActivationAfterProvisioned(Context context,
-            PhoneAccountHandle phoneAccountHandle) {
-        if (sDeviceProvisionedObserver == null) {
-            sDeviceProvisionedObserver = new DeviceProvisionedObserver(context);
-            context.getContentResolver()
-                .registerContentObserver(Settings.Global.getUriFor(Global.DEVICE_PROVISIONED),
-                    false, sDeviceProvisionedObserver);
-        }
-        sDeviceProvisionedObserver.addPhoneAcountHandle(phoneAccountHandle);
-    }
-
-    private static class DeviceProvisionedObserver extends ContentObserver {
-
-        private final Context mContext;
-        private final Set<PhoneAccountHandle> mPhoneAccountHandles = new HashSet<>();
-
-        private DeviceProvisionedObserver(Context context) {
-            super(null);
-            mContext = context;
-        }
-
-        public void addPhoneAcountHandle(PhoneAccountHandle phoneAccountHandle) {
-            mPhoneAccountHandles.add(phoneAccountHandle);
-        }
-
-        @Override
-        public void onChange(boolean selfChange) {
-            if (isDeviceProvisioned(mContext)) {
-                VvmLog.i(TAG, "device provisioned, resuming activation");
-                for (PhoneAccountHandle phoneAccountHandle : mPhoneAccountHandles) {
-                    start(mContext, phoneAccountHandle, null);
-                }
-                mContext.getContentResolver().unregisterContentObserver(sDeviceProvisionedObserver);
-                sDeviceProvisionedObserver = null;
-            }
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/AndroidManifest.xml b/java/com/android/voicemailomtp/AndroidManifest.xml
deleted file mode 100644
index 282a923..0000000
--- a/java/com/android/voicemailomtp/AndroidManifest.xml
+++ /dev/null
@@ -1,105 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2007 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-        package="com.android.voicemailomtp"
->
-
-    <uses-sdk
-        android:minSdkVersion="23"
-        android:targetSdkVersion="25" />
-
-    <application
-            android:allowBackup="false"
-            android:supportsRtl="true"
-            android:usesCleartextTraffic="true"
-            android:defaultToDeviceProtectedStorage="true"
-            android:directBootAware="true">
-
-        <activity android:name="com.android.voicemailomtp.settings.VoicemailSettingsActivity"
-          android:label="@string/voicemail_settings_label">
-            <intent-filter >
-                <!-- DO NOT RENAME. There are existing apps which use this string. -->
-                <action android:name="com.android.voicemailomtp.CallFeaturesSetting.ADD_VOICEMAIL" />
-                <category android:name="android.intent.category.DEFAULT" />
-            </intent-filter>
-            <intent-filter>
-                <action android:name="android.telephony.action.CONFIGURE_VOICEMAIL" />
-                <category android:name="android.intent.category.DEFAULT" />
-            </intent-filter>
-        </activity>
-
-        <receiver android:name="com.android.voicemailomtp.sms.OmtpMessageReceiver"
-            android:exported="false"
-            androidprv:systemUserOnly="true">
-            <intent-filter>
-                <action android:name="com.android.vociemailomtp.sms.sms_received"/>
-            </intent-filter>
-        </receiver>
-
-        <receiver
-            android:name="com.android.voicemailomtp.fetch.FetchVoicemailReceiver"
-            android:exported="true"
-            android:permission="com.android.voicemail.permission.READ_VOICEMAIL"
-            androidprv:systemUserOnly="true">
-            <intent-filter>
-                <action android:name="android.intent.action.FETCH_VOICEMAIL" />
-                    <data
-                        android:scheme="content"
-                        android:host="com.android.voicemail"
-                        android:mimeType="vnd.android.cursor.item/voicemail" />
-            </intent-filter>
-        </receiver>
-        <receiver
-            android:name="com.android.voicemailomtp.sync.OmtpVvmSyncReceiver"
-            android:exported="true"
-            android:permission="com.android.voicemail.permission.READ_VOICEMAIL"
-            androidprv:systemUserOnly="true">
-            <intent-filter>
-                <action android:name="android.provider.action.SYNC_VOICEMAIL"/>
-            </intent-filter>
-        </receiver>
-        <receiver
-            android:name="com.android.voicemailomtp.sync.VoicemailProviderChangeReceiver"
-          android:exported="true">
-            <intent-filter>
-                <action android:name="android.intent.action.PROVIDER_CHANGED" />
-                <data
-                    android:scheme="content"
-                    android:host="com.android.voicemail"
-                    android:mimeType="vnd.android.cursor.dir/voicemails"/>
-            </intent-filter>
-        </receiver>
-
-        <service
-          android:name="com.android.voicemailomtp.scheduling.TaskSchedulerService"
-          android:exported="false" />
-
-        <service
-          android:name="com.android.voicemailomtp.OmtpService"
-          android:permission="android.permission.BIND_VISUAL_VOICEMAIL_SERVICE"
-          android:exported="true">
-            <intent-filter>
-                <action android:name="android.telephony.VisualVoicemailService"/>
-            </intent-filter>
-        </service>
-        <activity android:name=".settings.VoicemailChangePinActivity"
-          android:exported="false"
-          android:windowSoftInputMode="stateVisible|adjustResize">
-          </activity>
-    </application>
-</manifest>
diff --git a/java/com/android/voicemailomtp/Assert.java b/java/com/android/voicemailomtp/Assert.java
deleted file mode 100644
index 1d295be..0000000
--- a/java/com/android/voicemailomtp/Assert.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-
-package com.android.voicemailomtp;
-
-import android.os.Looper;
-
-/**
- * Assertions which will result in program termination.
- */
-public class Assert {
-
-    private static Boolean sIsMainThreadForTest;
-
-    public static void isTrue(boolean condition) {
-        if (!condition) {
-            throw new AssertionError("Expected condition to be true");
-        }
-    }
-
-    public static void isMainThread() {
-        if (sIsMainThreadForTest != null) {
-            isTrue(sIsMainThreadForTest);
-            return;
-        }
-        isTrue(Looper.getMainLooper().equals(Looper.myLooper()));
-    }
-
-    public static void isNotMainThread() {
-        if (sIsMainThreadForTest != null) {
-            isTrue(!sIsMainThreadForTest);
-            return;
-        }
-        isTrue(!Looper.getMainLooper().equals(Looper.myLooper()));
-    }
-
-    public static void fail() {
-        throw new AssertionError("Fail");
-    }
-
-    /**
-     * Override the main thread status for tests. Set to null to revert to normal behavior
-     */
-    @NeededForTesting
-    public static void setIsMainThreadForTesting(Boolean isMainThread) {
-        sIsMainThreadForTest = isMainThread;
-    }
-}
diff --git a/java/com/android/voicemailomtp/DefaultOmtpEventHandler.java b/java/com/android/voicemailomtp/DefaultOmtpEventHandler.java
deleted file mode 100644
index 6a4b510..0000000
--- a/java/com/android/voicemailomtp/DefaultOmtpEventHandler.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp;
-
-import android.content.Context;
-import android.provider.VoicemailContract;
-import android.provider.VoicemailContract.Status;
-
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.OmtpEvents.Type;
-
-public class DefaultOmtpEventHandler {
-
-    private static final String TAG = "DefErrorCodeHandler";
-
-    public static void handleEvent(Context context, OmtpVvmCarrierConfigHelper config,
-        VoicemailStatus.Editor status, OmtpEvents event) {
-        switch (event.getType()) {
-            case Type.CONFIGURATION:
-                handleConfigurationEvent(context, status, event);
-                break;
-            case Type.DATA_CHANNEL:
-                handleDataChannelEvent(context, status, event);
-                break;
-            case Type.NOTIFICATION_CHANNEL:
-                handleNotificationChannelEvent(context, config, status, event);
-                break;
-            case Type.OTHER:
-                handleOtherEvent(context, status, event);
-                break;
-            default:
-                VvmLog.wtf(TAG, "invalid event type " + event.getType() + " for " + event);
-        }
-    }
-
-    private static void handleConfigurationEvent(Context context, VoicemailStatus.Editor status,
-            OmtpEvents event) {
-        switch (event) {
-            case CONFIG_DEFAULT_PIN_REPLACED:
-            case CONFIG_REQUEST_STATUS_SUCCESS:
-            case CONFIG_PIN_SET:
-                status
-                        .setConfigurationState(VoicemailContract.Status.CONFIGURATION_STATE_OK)
-                        .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
-                        .apply();
-                break;
-            case CONFIG_ACTIVATING:
-                // Wipe all errors from the last activation. All errors shown should be new errors
-                // for this activation.
-                status
-                        .setConfigurationState(Status.CONFIGURATION_STATE_CONFIGURING)
-                        .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
-                        .setDataChannelState(Status.DATA_CHANNEL_STATE_OK).apply();
-                break;
-            case CONFIG_ACTIVATING_SUBSEQUENT:
-                status
-                        .setConfigurationState(Status.CONFIGURATION_STATE_OK)
-                        .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
-                        .setDataChannelState(Status.DATA_CHANNEL_STATE_OK).apply();
-                break;
-            case CONFIG_SERVICE_NOT_AVAILABLE:
-                status
-                    .setConfigurationState(Status.CONFIGURATION_STATE_FAILED)
-                    .apply();
-                break;
-            case CONFIG_STATUS_SMS_TIME_OUT:
-                status
-                        .setConfigurationState(Status.CONFIGURATION_STATE_FAILED)
-                        .apply();
-                break;
-            default:
-                VvmLog.wtf(TAG, "invalid configuration event " + event);
-        }
-    }
-
-    private static void handleDataChannelEvent(Context context, VoicemailStatus.Editor status,
-            OmtpEvents event) {
-        switch (event) {
-            case DATA_IMAP_OPERATION_STARTED:
-            case DATA_IMAP_OPERATION_COMPLETED:
-                status
-                        .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
-                        .apply();
-                break;
-
-            case DATA_NO_CONNECTION:
-                status
-                        .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION)
-                        .apply();
-                break;
-
-            case DATA_NO_CONNECTION_CELLULAR_REQUIRED:
-                status
-                        .setDataChannelState(
-                                Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED)
-                        .apply();
-                break;
-            case DATA_INVALID_PORT:
-                status
-                        .setDataChannelState(
-                                VoicemailContract.Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION)
-                        .apply();
-                break;
-            case DATA_CANNOT_RESOLVE_HOST_ON_NETWORK:
-                status
-                        .setDataChannelState(
-                                VoicemailContract.Status.DATA_CHANNEL_STATE_SERVER_CONNECTION_ERROR)
-                        .apply();
-                break;
-            case DATA_SSL_INVALID_HOST_NAME:
-            case DATA_CANNOT_ESTABLISH_SSL_SESSION:
-            case DATA_IOE_ON_OPEN:
-            case DATA_GENERIC_IMAP_IOE:
-                status
-                        .setDataChannelState(
-                                VoicemailContract.Status.DATA_CHANNEL_STATE_COMMUNICATION_ERROR)
-                        .apply();
-                break;
-            case DATA_BAD_IMAP_CREDENTIAL:
-            case DATA_AUTH_UNKNOWN_USER:
-            case DATA_AUTH_UNKNOWN_DEVICE:
-            case DATA_AUTH_INVALID_PASSWORD:
-            case DATA_AUTH_MAILBOX_NOT_INITIALIZED:
-            case DATA_AUTH_SERVICE_NOT_PROVISIONED:
-            case DATA_AUTH_SERVICE_NOT_ACTIVATED:
-            case DATA_AUTH_USER_IS_BLOCKED:
-                status
-                        .setDataChannelState(
-                                VoicemailContract.Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION)
-                        .apply();
-                break;
-
-            case DATA_REJECTED_SERVER_RESPONSE:
-            case DATA_INVALID_INITIAL_SERVER_RESPONSE:
-            case DATA_MAILBOX_OPEN_FAILED:
-            case DATA_SSL_EXCEPTION:
-            case DATA_ALL_SOCKET_CONNECTION_FAILED:
-                status
-                        .setDataChannelState(
-                                VoicemailContract.Status.DATA_CHANNEL_STATE_SERVER_ERROR)
-                        .apply();
-                break;
-
-            default:
-                VvmLog.wtf(TAG, "invalid data channel event " + event);
-        }
-    }
-
-    private static void handleNotificationChannelEvent(Context context,
-        OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, OmtpEvents event) {
-        switch (event) {
-            case NOTIFICATION_IN_SERVICE:
-                status
-                        .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
-                        // Clear the error state. A sync should follow signal return so any error
-                        // will be reposted.
-                        .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
-                        .apply();
-                break;
-            case NOTIFICATION_SERVICE_LOST:
-                status.setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION);
-                if (config.isCellularDataRequired()) {
-                    status.setDataChannelState(
-                            Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED);
-                }
-                status.apply();
-                break;
-            default:
-                VvmLog.wtf(TAG, "invalid notification channel event " + event);
-        }
-    }
-
-    private static void handleOtherEvent(Context context, VoicemailStatus.Editor status,
-            OmtpEvents event) {
-        switch (event) {
-            case OTHER_SOURCE_REMOVED:
-                status
-                        .setConfigurationState(Status.CONFIGURATION_STATE_NOT_CONFIGURED)
-                        .setNotificationChannelState(
-                                Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION)
-                        .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION)
-                        .apply();
-                break;
-            default:
-                VvmLog.wtf(TAG, "invalid other event " + event);
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/OmtpConstants.java b/java/com/android/voicemailomtp/OmtpConstants.java
deleted file mode 100644
index da2b998..0000000
--- a/java/com/android/voicemailomtp/OmtpConstants.java
+++ /dev/null
@@ -1,248 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp;
-
-import android.support.annotation.IntDef;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Wrapper class to hold relevant OMTP constants as defined in the OMTP spec. <p> In essence this is
- * a programmatic representation of the relevant portions of OMTP spec.
- */
-public class OmtpConstants {
-    public static final String SMS_FIELD_SEPARATOR = ";";
-    public static final String SMS_KEY_VALUE_SEPARATOR = "=";
-    public static final String SMS_PREFIX_SEPARATOR = ":";
-
-    public static final String SYNC_SMS_PREFIX = "SYNC";
-    public static final String STATUS_SMS_PREFIX = "STATUS";
-
-    // This is the format designated by the OMTP spec.
-    public static final String DATE_TIME_FORMAT = "dd/MM/yyyy HH:mm Z";
-
-    /** OMTP protocol versions. */
-    public static final String PROTOCOL_VERSION1_1 = "11";
-    public static final String PROTOCOL_VERSION1_2 = "12";
-    public static final String PROTOCOL_VERSION1_3 = "13";
-
-    ///////////////////////// Client/Mobile originated SMS //////////////////////
-
-    /** Mobile Originated requests */
-    public static final String ACTIVATE_REQUEST = "Activate";
-    public static final String DEACTIVATE_REQUEST = "Deactivate";
-    public static final String STATUS_REQUEST = "Status";
-
-    /** fields that can be present in a Mobile Originated OMTP SMS */
-    public static final String CLIENT_TYPE = "ct";
-    public static final String APPLICATION_PORT = "pt";
-    public static final String PROTOCOL_VERSION = "pv";
-
-
-    //////////////////////////////// Sync SMS fields ////////////////////////////
-
-    /**
-     * Sync SMS fields.
-     * <p>
-     * Each string constant is the field's key in the SMS body which is used by the parser to
-     * identify the field's value, if present, in the SMS body.
-     */
-
-    /**
-     * The event that triggered this SYNC SMS.
-     * See {@link OmtpConstants#SYNC_TRIGGER_EVENT_VALUES}
-     */
-    public static final String SYNC_TRIGGER_EVENT = "ev";
-    public static final String MESSAGE_UID = "id";
-    public static final String MESSAGE_LENGTH = "l";
-    public static final String NUM_MESSAGE_COUNT = "c";
-    /** See {@link OmtpConstants#CONTENT_TYPE_VALUES} */
-    public static final String CONTENT_TYPE = "t";
-    public static final String SENDER = "s";
-    public static final String TIME = "dt";
-
-    /**
-     * SYNC message trigger events.
-     * <p>
-     * These are the possible values of {@link OmtpConstants#SYNC_TRIGGER_EVENT}.
-     */
-    public static final String NEW_MESSAGE = "NM";
-    public static final String MAILBOX_UPDATE = "MBU";
-    public static final String GREETINGS_UPDATE = "GU";
-
-    public static final String[] SYNC_TRIGGER_EVENT_VALUES = {
-        NEW_MESSAGE,
-        MAILBOX_UPDATE,
-        GREETINGS_UPDATE
-    };
-
-    /**
-     * Content types supported by OMTP VVM.
-     * <p>
-     * These are the possible values of {@link OmtpConstants#CONTENT_TYPE}.
-     */
-    public static final String VOICE = "v";
-    public static final String VIDEO = "o";
-    public static final String FAX = "f";
-    /** Voice message deposited by an external application */
-    public static final String INFOTAINMENT = "i";
-    /** Empty Call Capture - i.e. voicemail with no voice message. */
-    public static final String ECC = "e";
-
-    public static final String[] CONTENT_TYPE_VALUES = {VOICE, VIDEO, FAX, INFOTAINMENT, ECC};
-
-    ////////////////////////////// Status SMS fields ////////////////////////////
-
-    /**
-     * Status SMS fields.
-     * <p>
-     * Each string constant is the field's key in the SMS body which is used by the parser to
-     * identify the field's value, if present, in the SMS body.
-     */
-    /** See {@link OmtpConstants#PROVISIONING_STATUS_VALUES} */
-    public static final String PROVISIONING_STATUS = "st";
-    /** See {@link OmtpConstants#RETURN_CODE_VALUES} */
-    public static final String RETURN_CODE = "rc";
-    /** URL to send users to for activation VVM */
-    public static final String SUBSCRIPTION_URL = "rs";
-    /** IMAP4/SMTP server IP address or fully qualified domain name */
-    public static final String SERVER_ADDRESS = "srv";
-    /** Phone number to access voicemails through Telephony User Interface */
-    public static final String TUI_ACCESS_NUMBER = "tui";
-    public static final String TUI_PASSWORD_LENGTH = "pw_len";
-    /** Number to send client origination SMS */
-    public static final String CLIENT_SMS_DESTINATION_NUMBER = "dn";
-    public static final String IMAP_PORT = "ipt";
-    public static final String IMAP_USER_NAME = "u";
-    public static final String IMAP_PASSWORD = "pw";
-    public static final String SMTP_PORT = "spt";
-    public static final String SMTP_USER_NAME = "smtp_u";
-    public static final String SMTP_PASSWORD = "smtp_pw";
-
-    /**
-     * User provisioning status values.
-     * <p>
-     * Referred by {@link OmtpConstants#PROVISIONING_STATUS}.
-     */
-    public static final String SUBSCRIBER_NEW = "N";
-    public static final String SUBSCRIBER_READY = "R";
-    public static final String SUBSCRIBER_PROVISIONED = "P";
-    public static final String SUBSCRIBER_UNKNOWN = "U";
-    public static final String SUBSCRIBER_BLOCKED = "B";
-
-    public static final String[] PROVISIONING_STATUS_VALUES = {
-        SUBSCRIBER_NEW,
-        SUBSCRIBER_READY,
-        SUBSCRIBER_PROVISIONED,
-        SUBSCRIBER_UNKNOWN,
-        SUBSCRIBER_BLOCKED
-    };
-
-    /**
-     * The return code included in a status message.
-     * <p>
-     * These are the possible values of {@link OmtpConstants#RETURN_CODE}.
-     */
-    public static final String SUCCESS = "0";
-    public static final String SYSTEM_ERROR = "1";
-    public static final String SUBSCRIBER_ERROR = "2";
-    public static final String MAILBOX_UNKNOWN = "3";
-    public static final String VVM_NOT_ACTIVATED = "4";
-    public static final String VVM_NOT_PROVISIONED = "5";
-    public static final String VVM_CLIENT_UKNOWN = "6";
-    public static final String VVM_MAILBOX_NOT_INITIALIZED = "7";
-
-    public static final String[] RETURN_CODE_VALUES = {
-        SUCCESS,
-        SYSTEM_ERROR,
-        SUBSCRIBER_ERROR,
-        MAILBOX_UNKNOWN,
-        VVM_NOT_ACTIVATED,
-        VVM_NOT_PROVISIONED,
-        VVM_CLIENT_UKNOWN,
-        VVM_MAILBOX_NOT_INITIALIZED,
-    };
-
-    /**
-     * A map of all the field keys to the possible values they can have.
-     */
-    public static final Map<String, String[]> possibleValuesMap = new HashMap<String, String[]>() {{
-        put(SYNC_TRIGGER_EVENT, SYNC_TRIGGER_EVENT_VALUES);
-        put(CONTENT_TYPE, CONTENT_TYPE_VALUES);
-        put(PROVISIONING_STATUS, PROVISIONING_STATUS_VALUES);
-        put(RETURN_CODE, RETURN_CODE_VALUES);
-    }};
-
-    /**
-     * IMAP command extensions
-     */
-
-    /**
-     * OMTP spec v1.3 2.3.1 Change password request syntax
-     *
-     * This changes the PIN to access the Telephone User Interface, the traditional voicemail
-     * system.
-     */
-    public static final String IMAP_CHANGE_TUI_PWD_FORMAT = "XCHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
-
-    /**
-     * OMTP spec v1.3 2.4.1 Change languate request syntax
-     *
-     * This changes the language in the Telephone User Interface.
-     */
-    public static final String IMAP_CHANGE_VM_LANG_FORMAT = "XCHANGE_VM_LANG LANG=%1$s";
-
-    /**
-     * OMTP spec v1.3 2.5.1 Close NUT Request syntax
-     *
-     * This disables the new user tutorial, the message played to new users calling in the Telephone
-     * User Interface.
-     */
-    public static final String IMAP_CLOSE_NUT = "XCLOSE_NUT";
-
-    /**
-     * Possible NO responses for CHANGE_TUI_PWD
-     */
-
-    public static final String RESPONSE_CHANGE_PIN_TOO_SHORT = "password too short";
-    public static final String RESPONSE_CHANGE_PIN_TOO_LONG = "password too long";
-    public static final String RESPONSE_CHANGE_PIN_TOO_WEAK = "password too weak";
-    public static final String RESPONSE_CHANGE_PIN_MISMATCH = "old password mismatch";
-    public static final String RESPONSE_CHANGE_PIN_INVALID_CHARACTER =
-            "password contains invalid characters";
-
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(value = {CHANGE_PIN_SUCCESS, CHANGE_PIN_TOO_SHORT, CHANGE_PIN_TOO_LONG,
-            CHANGE_PIN_TOO_WEAK, CHANGE_PIN_MISMATCH, CHANGE_PIN_INVALID_CHARACTER,
-            CHANGE_PIN_SYSTEM_ERROR})
-
-    public @interface ChangePinResult {
-
-    }
-
-    public static final int CHANGE_PIN_SUCCESS = 0;
-    public static final int CHANGE_PIN_TOO_SHORT = 1;
-    public static final int CHANGE_PIN_TOO_LONG = 2;
-    public static final int CHANGE_PIN_TOO_WEAK = 3;
-    public static final int CHANGE_PIN_MISMATCH = 4;
-    public static final int CHANGE_PIN_INVALID_CHARACTER = 5;
-    public static final int CHANGE_PIN_SYSTEM_ERROR = 6;
-
-    /** Indicates the client is Google visual voicemail version 1.0. */
-    public static final String CLIENT_TYPE_GOOGLE_10 = "google.vvm.10";
-}
diff --git a/java/com/android/voicemailomtp/OmtpEvents.java b/java/com/android/voicemailomtp/OmtpEvents.java
deleted file mode 100644
index d5c2a8b..0000000
--- a/java/com/android/voicemailomtp/OmtpEvents.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp;
-
-import android.support.annotation.IntDef;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Events internal to the OMTP client. These should be translated into {@link
- * android.provider.VoicemailContract.Status} error codes before writing into the voicemail status
- * table.
- */
-public enum OmtpEvents {
-
-    // Configuration State
-    CONFIG_REQUEST_STATUS_SUCCESS(Type.CONFIGURATION, true),
-
-    CONFIG_PIN_SET(Type.CONFIGURATION, true),
-    // The voicemail PIN is replaced with a generated PIN, user should change it.
-    CONFIG_DEFAULT_PIN_REPLACED(Type.CONFIGURATION, true),
-    CONFIG_ACTIVATING(Type.CONFIGURATION, true),
-    // There are already activation records, this is only a book-keeping activation.
-    CONFIG_ACTIVATING_SUBSEQUENT(Type.CONFIGURATION, true),
-    CONFIG_STATUS_SMS_TIME_OUT(Type.CONFIGURATION),
-    CONFIG_SERVICE_NOT_AVAILABLE(Type.CONFIGURATION),
-
-    // Data channel State
-
-    // A new sync has started, old errors in data channel should be cleared.
-    DATA_IMAP_OPERATION_STARTED(Type.DATA_CHANNEL, true),
-    // Successfully downloaded/uploaded data from the server, which means the data channel is clear.
-    DATA_IMAP_OPERATION_COMPLETED(Type.DATA_CHANNEL, true),
-    // The port provided in the STATUS SMS is invalid.
-    DATA_INVALID_PORT(Type.DATA_CHANNEL),
-    // No connection to the internet, and the carrier requires cellular data
-    DATA_NO_CONNECTION_CELLULAR_REQUIRED(Type.DATA_CHANNEL),
-    // No connection to the internet.
-    DATA_NO_CONNECTION(Type.DATA_CHANNEL),
-    // Address lookup for the server hostname failed. DNS error?
-    DATA_CANNOT_RESOLVE_HOST_ON_NETWORK(Type.DATA_CHANNEL),
-    // All destination address that resolves to the server hostname are rejected or timed out
-    DATA_ALL_SOCKET_CONNECTION_FAILED(Type.DATA_CHANNEL),
-    // Failed to establish SSL with the server, either with a direct SSL connection or by
-    // STARTTLS command
-    DATA_CANNOT_ESTABLISH_SSL_SESSION(Type.DATA_CHANNEL),
-    // Identity of the server cannot be verified.
-    DATA_SSL_INVALID_HOST_NAME(Type.DATA_CHANNEL),
-    // The server rejected our username/password
-    DATA_BAD_IMAP_CREDENTIAL(Type.DATA_CHANNEL),
-
-    DATA_AUTH_UNKNOWN_USER(Type.DATA_CHANNEL),
-    DATA_AUTH_UNKNOWN_DEVICE(Type.DATA_CHANNEL),
-    DATA_AUTH_INVALID_PASSWORD(Type.DATA_CHANNEL),
-    DATA_AUTH_MAILBOX_NOT_INITIALIZED(Type.DATA_CHANNEL),
-    DATA_AUTH_SERVICE_NOT_PROVISIONED(Type.DATA_CHANNEL),
-    DATA_AUTH_SERVICE_NOT_ACTIVATED(Type.DATA_CHANNEL),
-    DATA_AUTH_USER_IS_BLOCKED(Type.DATA_CHANNEL),
-
-    // A command to the server didn't result with an "OK" or continuation request
-    DATA_REJECTED_SERVER_RESPONSE(Type.DATA_CHANNEL),
-    // The server did not greet us with a "OK", possibly not a IMAP server.
-    DATA_INVALID_INITIAL_SERVER_RESPONSE(Type.DATA_CHANNEL),
-    // An IOException occurred while trying to open an ImapConnection
-    // TODO: reduce scope
-    DATA_IOE_ON_OPEN(Type.DATA_CHANNEL),
-    // The SELECT command on a mailbox is rejected
-    DATA_MAILBOX_OPEN_FAILED(Type.DATA_CHANNEL),
-    // An IOException has occurred
-    // TODO: reduce scope
-    DATA_GENERIC_IMAP_IOE(Type.DATA_CHANNEL),
-    // An SslException has occurred while opening an ImapConnection
-    // TODO: reduce scope
-    DATA_SSL_EXCEPTION(Type.DATA_CHANNEL),
-
-    // Notification Channel
-
-    // Cell signal restored, can received VVM SMSs
-    NOTIFICATION_IN_SERVICE(Type.NOTIFICATION_CHANNEL, true),
-    // Cell signal lost, cannot received VVM SMSs
-    NOTIFICATION_SERVICE_LOST(Type.NOTIFICATION_CHANNEL, false),
-
-
-    // Other
-    OTHER_SOURCE_REMOVED(Type.OTHER, false),
-
-    // VVM3
-    VVM3_NEW_USER_SETUP_FAILED,
-    // Table 4. client internal error handling
-    VVM3_VMG_DNS_FAILURE,
-    VVM3_SPG_DNS_FAILURE,
-    VVM3_VMG_CONNECTION_FAILED,
-    VVM3_SPG_CONNECTION_FAILED,
-    VVM3_VMG_TIMEOUT,
-    VVM3_STATUS_SMS_TIMEOUT,
-
-    VVM3_SUBSCRIBER_PROVISIONED,
-    VVM3_SUBSCRIBER_BLOCKED,
-    VVM3_SUBSCRIBER_UNKNOWN;
-
-    public static class Type {
-
-        @Retention(RetentionPolicy.SOURCE)
-        @IntDef({CONFIGURATION, DATA_CHANNEL, NOTIFICATION_CHANNEL, OTHER})
-        public @interface Values {
-
-        }
-
-        public static final int CONFIGURATION = 1;
-        public static final int DATA_CHANNEL = 2;
-        public static final int NOTIFICATION_CHANNEL = 3;
-        public static final int OTHER = 4;
-    }
-
-    private final int mType;
-    private final boolean mIsSuccess;
-
-    OmtpEvents(int type, boolean isSuccess) {
-        mType = type;
-        mIsSuccess = isSuccess;
-    }
-
-    OmtpEvents(int type) {
-        mType = type;
-        mIsSuccess = false;
-    }
-
-    OmtpEvents() {
-        mType = Type.OTHER;
-        mIsSuccess = false;
-    }
-
-    @Type.Values
-    public int getType() {
-        return mType;
-    }
-
-    public boolean isSuccess() {
-        return mIsSuccess;
-    }
-
-}
diff --git a/java/com/android/voicemailomtp/OmtpService.java b/java/com/android/voicemailomtp/OmtpService.java
deleted file mode 100644
index 261a7cb..0000000
--- a/java/com/android/voicemailomtp/OmtpService.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp;
-
-import android.content.Intent;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.VisualVoicemailService;
-import android.telephony.VisualVoicemailSms;
-
-import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
-
-public class OmtpService extends VisualVoicemailService {
-
-    private static String TAG = "VvmOmtpService";
-
-    public static final String ACTION_SMS_RECEIVED = "com.android.vociemailomtp.sms.sms_received";
-
-    public static final String EXTRA_VOICEMAIL_SMS = "extra_voicemail_sms";
-
-    @Override
-    public void onCellServiceConnected(VisualVoicemailTask task,
-            final PhoneAccountHandle phoneAccountHandle) {
-        VvmLog.i(TAG, "onCellServiceConnected");
-        ActivationTask
-                .start(OmtpService.this, phoneAccountHandle, null);
-        task.finish();
-    }
-
-    @Override
-    public void onSmsReceived(VisualVoicemailTask task, final VisualVoicemailSms sms) {
-        VvmLog.i(TAG, "onSmsReceived");
-        Intent intent = new Intent(ACTION_SMS_RECEIVED);
-        intent.setPackage(getPackageName());
-        intent.putExtra(EXTRA_VOICEMAIL_SMS, sms);
-        sendBroadcast(intent);
-        task.finish();
-    }
-
-    @Override
-    public void onSimRemoved(final VisualVoicemailTask task,
-            final PhoneAccountHandle phoneAccountHandle) {
-        VvmLog.i(TAG, "onSimRemoved");
-        OmtpVvmSourceManager.getInstance(OmtpService.this).removeSource(phoneAccountHandle);
-        task.finish();
-    }
-
-    @Override
-    public void onStopped(VisualVoicemailTask task) {
-        VvmLog.i(TAG, "onStopped");
-    }
-}
diff --git a/java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java b/java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java
deleted file mode 100644
index b3e72d2..0000000
--- a/java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java
+++ /dev/null
@@ -1,423 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.os.Bundle;
-import android.os.PersistableBundle;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.CarrierConfigManager;
-import android.telephony.TelephonyManager;
-import android.telephony.VisualVoicemailService;
-import android.telephony.VisualVoicemailSmsFilterSettings;
-import android.text.TextUtils;
-import android.util.ArraySet;
-
-import com.android.voicemailomtp.protocol.VisualVoicemailProtocol;
-import com.android.voicemailomtp.protocol.VisualVoicemailProtocolFactory;
-import com.android.voicemailomtp.sms.StatusMessage;
-
-import java.util.Arrays;
-import java.util.Set;
-
-/**
- * Manages carrier dependent visual voicemail configuration values. The primary source is the value
- * retrieved from CarrierConfigManager. If CarrierConfigManager does not provide the config
- * (KEY_VVM_TYPE_STRING is empty, or "hidden" configs), then the value hardcoded in telephony will
- * be used (in res/xml/vvm_config.xml)
- *
- * Hidden configs are new configs that are planned for future APIs, or miscellaneous settings that
- * may clutter CarrierConfigManager too much.
- *
- * The current hidden configs are: {@link #getSslPort()} {@link #getDisabledCapabilities()}
- */
-public class OmtpVvmCarrierConfigHelper {
-
-    private static final String TAG = "OmtpVvmCarrierCfgHlpr";
-
-    static final String KEY_VVM_TYPE_STRING = CarrierConfigManager.KEY_VVM_TYPE_STRING;
-    static final String KEY_VVM_DESTINATION_NUMBER_STRING =
-            CarrierConfigManager.KEY_VVM_DESTINATION_NUMBER_STRING;
-    static final String KEY_VVM_PORT_NUMBER_INT =
-            CarrierConfigManager.KEY_VVM_PORT_NUMBER_INT;
-    static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING =
-            CarrierConfigManager.KEY_CARRIER_VVM_PACKAGE_NAME_STRING;
-    static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY =
-            "carrier_vvm_package_name_string_array";
-    static final String KEY_VVM_PREFETCH_BOOL =
-            CarrierConfigManager.KEY_VVM_PREFETCH_BOOL;
-    static final String KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL =
-            CarrierConfigManager.KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL;
-
-    /**
-     * @see #getSslPort()
-     */
-    static final String KEY_VVM_SSL_PORT_NUMBER_INT =
-            "vvm_ssl_port_number_int";
-
-    /**
-     * @see #isLegacyModeEnabled()
-     */
-    static final String KEY_VVM_LEGACY_MODE_ENABLED_BOOL =
-            "vvm_legacy_mode_enabled_bool";
-
-    /**
-     * Ban a capability reported by the server from being used. The array of string should be a
-     * subset of the capabilities returned IMAP CAPABILITY command.
-     *
-     * @see #getDisabledCapabilities()
-     */
-    static final String KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY =
-            "vvm_disabled_capabilities_string_array";
-    static final String KEY_VVM_CLIENT_PREFIX_STRING =
-            "vvm_client_prefix_string";
-
-    private final Context mContext;
-    private final PersistableBundle mCarrierConfig;
-    private final String mVvmType;
-    private final VisualVoicemailProtocol mProtocol;
-    private final PersistableBundle mTelephonyConfig;
-
-    private PhoneAccountHandle mPhoneAccountHandle;
-
-    public OmtpVvmCarrierConfigHelper(Context context, PhoneAccountHandle handle) {
-        mContext = context;
-        mPhoneAccountHandle = handle;
-        mCarrierConfig = getCarrierConfig();
-
-        TelephonyManager telephonyManager =
-                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
-        mTelephonyConfig = new TelephonyVvmConfigManager(context.getResources())
-                .getConfig(telephonyManager.createForPhoneAccountHandle(mPhoneAccountHandle)
-                        .getSimOperator());
-
-        mVvmType = getVvmType();
-        mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
-
-    }
-
-    @VisibleForTesting
-    OmtpVvmCarrierConfigHelper(Context context, PersistableBundle carrierConfig,
-            PersistableBundle telephonyConfig) {
-        mContext = context;
-        mCarrierConfig = carrierConfig;
-        mTelephonyConfig = telephonyConfig;
-        mVvmType = getVvmType();
-        mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
-    }
-
-    public Context getContext() {
-        return mContext;
-    }
-
-    @Nullable
-    public PhoneAccountHandle getPhoneAccountHandle() {
-        return mPhoneAccountHandle;
-    }
-
-    /**
-     * return whether the carrier's visual voicemail is supported, with KEY_VVM_TYPE_STRING set as a
-     * known protocol.
-     */
-    public boolean isValid() {
-        return mProtocol != null;
-    }
-
-    @Nullable
-    public String getVvmType() {
-        return (String) getValue(KEY_VVM_TYPE_STRING);
-    }
-
-    @Nullable
-    public VisualVoicemailProtocol getProtocol() {
-        return mProtocol;
-    }
-
-    /**
-     * @returns arbitrary String stored in the config file. Used for protocol specific values.
-     */
-    @Nullable
-    public String getString(String key) {
-        return (String) getValue(key);
-    }
-
-    @Nullable
-    public Set<String> getCarrierVvmPackageNames() {
-        Set<String> names = getCarrierVvmPackageNames(mCarrierConfig);
-        if (names != null) {
-            return names;
-        }
-        return getCarrierVvmPackageNames(mTelephonyConfig);
-    }
-
-    private static Set<String> getCarrierVvmPackageNames(@Nullable PersistableBundle bundle) {
-        if (bundle == null) {
-            return null;
-        }
-        Set<String> names = new ArraySet<>();
-        if (bundle.containsKey(KEY_CARRIER_VVM_PACKAGE_NAME_STRING)) {
-            names.add(bundle.getString(KEY_CARRIER_VVM_PACKAGE_NAME_STRING));
-        }
-        if (bundle.containsKey(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY)) {
-            names.addAll(Arrays.asList(
-                    bundle.getStringArray(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY)));
-        }
-        if (names.isEmpty()) {
-            return null;
-        }
-        return names;
-    }
-
-    /**
-     * For checking upon sim insertion whether visual voicemail should be enabled. This method does
-     * so by checking if the carrier's voicemail app is installed.
-     */
-    public boolean isEnabledByDefault() {
-        if (!isValid()) {
-            return false;
-        }
-
-        Set<String> carrierPackages = getCarrierVvmPackageNames();
-        if (carrierPackages == null) {
-            return true;
-        }
-        for (String packageName : carrierPackages) {
-            try {
-                mContext.getPackageManager().getPackageInfo(packageName, 0);
-                return false;
-            } catch (NameNotFoundException e) {
-                // Do nothing.
-            }
-        }
-        return true;
-    }
-
-    public boolean isCellularDataRequired() {
-        return (boolean) getValue(KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL, false);
-    }
-
-    public boolean isPrefetchEnabled() {
-        return (boolean) getValue(KEY_VVM_PREFETCH_BOOL, true);
-    }
-
-
-    public int getApplicationPort() {
-        return (int) getValue(KEY_VVM_PORT_NUMBER_INT, 0);
-    }
-
-    @Nullable
-    public String getDestinationNumber() {
-        return (String) getValue(KEY_VVM_DESTINATION_NUMBER_STRING);
-    }
-
-    /**
-     * Hidden config.
-     *
-     * @return Port to start a SSL IMAP connection directly.
-     *
-     * TODO: make config public and add to CarrierConfigManager
-     */
-    public int getSslPort() {
-        return (int) getValue(KEY_VVM_SSL_PORT_NUMBER_INT, 0);
-    }
-
-    /**
-     * Hidden Config.
-     *
-     * <p>Sometimes the server states it supports a certain feature but we found they have bug on
-     * the server side. For example, in b/28717550 the server reported AUTH=DIGEST-MD5 capability
-     * but using it to login will cause subsequent response to be erroneous.
-     *
-     * @return A set of capabilities that is reported by the IMAP CAPABILITY command, but determined
-     * to have issues and should not be used.
-     */
-    @Nullable
-    public Set<String> getDisabledCapabilities() {
-        Set<String> disabledCapabilities = getDisabledCapabilities(mCarrierConfig);
-        if (disabledCapabilities != null) {
-            return disabledCapabilities;
-        }
-        return getDisabledCapabilities(mTelephonyConfig);
-    }
-
-    @Nullable
-    private static Set<String> getDisabledCapabilities(@Nullable PersistableBundle bundle) {
-        if (bundle == null) {
-            return null;
-        }
-        if (!bundle.containsKey(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY)) {
-            return null;
-        }
-        ArraySet<String> result = new ArraySet<String>();
-        result.addAll(
-                Arrays.asList(bundle.getStringArray(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY)));
-        return result;
-    }
-
-    public String getClientPrefix() {
-        String prefix = (String) getValue(KEY_VVM_CLIENT_PREFIX_STRING);
-        if (prefix != null) {
-            return prefix;
-        }
-        return "//VVM";
-    }
-
-    /**
-     * Should legacy mode be used when the OMTP VVM client is disabled?
-     *
-     * <p>Legacy mode is a mode that on the carrier side visual voicemail is still activated, but on
-     * the client side all network operations are disabled. SMSs are still monitored so a new
-     * message SYNC SMS will be translated to show a message waiting indicator, like traditional
-     * voicemails.
-     *
-     * <p>This is for carriers that does not support VVM deactivation so voicemail can continue to
-     * function without the data cost.
-     */
-    public boolean isLegacyModeEnabled() {
-        return (boolean) getValue(KEY_VVM_LEGACY_MODE_ENABLED_BOOL, false);
-    }
-
-    public void startActivation() {
-        PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
-        if (phoneAccountHandle == null) {
-            // This should never happen
-            // Error logged in getPhoneAccountHandle().
-            return;
-        }
-
-        if (mVvmType == null || mVvmType.isEmpty()) {
-            // The VVM type is invalid; we should never have gotten here in the first place since
-            // this is loaded initially in the constructor, and callers should check isValid()
-            // before trying to start activation anyways.
-            VvmLog.e(TAG, "startActivation : vvmType is null or empty for account " +
-                    phoneAccountHandle);
-            return;
-        }
-
-        if (mProtocol != null) {
-            ActivationTask.start(mContext, mPhoneAccountHandle, null);
-        }
-    }
-
-    public void activateSmsFilter() {
-        VisualVoicemailService.setSmsFilterSettings(mContext, getPhoneAccountHandle(),
-                new VisualVoicemailSmsFilterSettings.Builder()
-                        .setClientPrefix(getClientPrefix())
-                        .build());
-    }
-
-    public void startDeactivation() {
-        if (!isLegacyModeEnabled()) {
-            // SMS should still be filtered in legacy mode
-            VisualVoicemailService.setSmsFilterSettings(mContext, getPhoneAccountHandle(), null);
-        }
-        if (mProtocol != null) {
-            mProtocol.startDeactivation(this);
-        }
-    }
-
-    public boolean supportsProvisioning() {
-        if (mProtocol != null) {
-            return mProtocol.supportsProvisioning();
-        }
-        return false;
-    }
-
-    public void startProvisioning(ActivationTask task, PhoneAccountHandle phone,
-        VoicemailStatus.Editor status, StatusMessage message, Bundle data) {
-        if (mProtocol != null) {
-            mProtocol.startProvisioning(task, phone, this, status, message, data);
-        }
-    }
-
-    public void requestStatus(@Nullable PendingIntent sentIntent) {
-        if (mProtocol != null) {
-            mProtocol.requestStatus(this, sentIntent);
-        }
-    }
-
-    public void handleEvent(VoicemailStatus.Editor status, OmtpEvents event) {
-        VvmLog.i(TAG, "OmtpEvent:" + event);
-        if (mProtocol != null) {
-            mProtocol.handleEvent(mContext, this, status, event);
-        }
-    }
-
-    @Override
-    public String toString() {
-        StringBuilder builder = new StringBuilder("OmtpVvmCarrierConfigHelper [");
-        builder.append("phoneAccountHandle: ").append(mPhoneAccountHandle)
-                .append(", carrierConfig: ").append(mCarrierConfig != null)
-                .append(", telephonyConfig: ").append(mTelephonyConfig != null)
-                .append(", type: ").append(getVvmType())
-                .append(", destinationNumber: ").append(getDestinationNumber())
-                .append(", applicationPort: ").append(getApplicationPort())
-                .append(", sslPort: ").append(getSslPort())
-                .append(", isEnabledByDefault: ").append(isEnabledByDefault())
-                .append(", isCellularDataRequired: ").append(isCellularDataRequired())
-                .append(", isPrefetchEnabled: ").append(isPrefetchEnabled())
-                .append(", isLegacyModeEnabled: ").append(isLegacyModeEnabled())
-                .append("]");
-        return builder.toString();
-    }
-
-    @Nullable
-    private PersistableBundle getCarrierConfig() {
-
-        CarrierConfigManager carrierConfigManager = (CarrierConfigManager)
-                mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
-        if (carrierConfigManager == null) {
-            VvmLog.w(TAG, "No carrier config service found.");
-            return null;
-        }
-
-        PersistableBundle config = TelephonyManagerStub
-                .getCarrirConfigForPhoneAccountHandle(getContext(), mPhoneAccountHandle);
-
-        if (TextUtils.isEmpty(config.getString(CarrierConfigManager.KEY_VVM_TYPE_STRING))) {
-            return null;
-        }
-        return config;
-    }
-
-    @Nullable
-    private Object getValue(String key) {
-        return getValue(key, null);
-    }
-
-    @Nullable
-    private Object getValue(String key, Object defaultValue) {
-        Object result;
-        if (mCarrierConfig != null) {
-            result = mCarrierConfig.get(key);
-            if (result != null) {
-                return result;
-            }
-        }
-        if (mTelephonyConfig != null) {
-            result = mTelephonyConfig.get(key);
-            if (result != null) {
-                return result;
-            }
-        }
-        return defaultValue;
-    }
-
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/SubscriptionInfoHelper.java b/java/com/android/voicemailomtp/SubscriptionInfoHelper.java
deleted file mode 100644
index b916247..0000000
--- a/java/com/android/voicemailomtp/SubscriptionInfoHelper.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp;
-
-import android.app.ActionBar;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.text.TextUtils;
-
-/**
- * Helper for manipulating intents or components with subscription-related information.
- *
- * In settings, subscription ids and labels are passed along to indicate that settings
- * are being changed for particular subscriptions. This helper provides functions for
- * helping extract this info and perform common operations using this info.
- */
-public class SubscriptionInfoHelper {
-    public static final int NO_SUB_ID = -1;
-
-    // Extra on intent containing the id of a subscription.
-    public static final String SUB_ID_EXTRA =
-            "com.android.voicemailomtp.settings.SubscriptionInfoHelper.SubscriptionId";
-    // Extra on intent containing the label of a subscription.
-    private static final String SUB_LABEL_EXTRA =
-            "com.android.voicemailomtp.settings.SubscriptionInfoHelper.SubscriptionLabel";
-
-    private static Context mContext;
-
-    private static int mSubId = NO_SUB_ID;
-    private static String mSubLabel;
-
-    /**
-     * Instantiates the helper, by extracting the subscription id and label from the intent.
-     */
-    public SubscriptionInfoHelper(Context context, Intent intent) {
-        mContext = context;
-        mSubId = intent.getIntExtra(SUB_ID_EXTRA, NO_SUB_ID);
-        mSubLabel = intent.getStringExtra(SUB_LABEL_EXTRA);
-    }
-
-    /**
-     * Sets the action bar title to the string specified by the given resource id, formatting
-     * it with the subscription label. This assumes the resource string is formattable with a
-     * string-type specifier.
-     *
-     * If the subscription label does not exists, leave the existing title.
-     */
-    public void setActionBarTitle(ActionBar actionBar, Resources res, int resId) {
-        if (actionBar == null || TextUtils.isEmpty(mSubLabel)) {
-            return;
-        }
-
-        String title = String.format(res.getString(resId), mSubLabel);
-        actionBar.setTitle(title);
-    }
-
-    public int getSubId() {
-        return mSubId;
-    }
-}
diff --git a/java/com/android/voicemailomtp/TelephonyManagerStub.java b/java/com/android/voicemailomtp/TelephonyManagerStub.java
deleted file mode 100644
index e2e5dac..0000000
--- a/java/com/android/voicemailomtp/TelephonyManagerStub.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build.VERSION_CODES;
-import android.os.PersistableBundle;
-import android.telecom.PhoneAccount;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.CarrierConfigManager;
-import android.telephony.SubscriptionManager;
-import android.telephony.TelephonyManager;
-import java.lang.reflect.Method;
-
-/**
- * Temporary stub for public APIs that should be added into telephony manager.
- *
- * <p>TODO(b/32637799) remove this.
- */
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class TelephonyManagerStub {
-
-    private static final String TAG = "TelephonyManagerStub";
-
-    public static void showVoicemailNotification(int voicemailCount) {
-
-    }
-
-    /**
-     * Dismisses the message waiting (voicemail) indicator.
-     *
-     * @param subId the subscription id we should dismiss the notification for.
-     */
-    public static void clearMwiIndicator(int subId) {
-
-    }
-
-    public static void setShouldCheckVisualVoicemailConfigurationForMwi(int subId,
-            boolean enabled) {
-
-    }
-
-    public static int getSubIdForPhoneAccount(Context context, PhoneAccount phoneAccount) {
-        // Hidden
-        TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
-        try {
-            Method method = TelephonyManager.class
-                    .getMethod("getSubIdForPhoneAccount", PhoneAccount.class);
-            return (int) method.invoke(telephonyManager, phoneAccount);
-        } catch (Exception e) {
-            VvmLog.e(TAG, "reflection call to getSubIdForPhoneAccount failed:", e);
-        }
-        return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
-    }
-
-    public static String getNetworkSpecifierForPhoneAccountHandle(Context context,
-            PhoneAccountHandle phoneAccountHandle) {
-        return String.valueOf(SubscriptionManager.getDefaultDataSubscriptionId());
-    }
-
-    public static PersistableBundle getCarrirConfigForPhoneAccountHandle(Context context,
-            PhoneAccountHandle phoneAccountHandle) {
-        return context.getSystemService(CarrierConfigManager.class).getConfig();
-    }
-}
diff --git a/java/com/android/voicemailomtp/TelephonyVvmConfigManager.java b/java/com/android/voicemailomtp/TelephonyVvmConfigManager.java
deleted file mode 100644
index ab13d36..0000000
--- a/java/com/android/voicemailomtp/TelephonyVvmConfigManager.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp;
-
-import android.content.res.Resources;
-import android.os.PersistableBundle;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.util.ArrayMap;
-import com.android.voicemailomtp.utils.XmlUtils;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Map;
-import java.util.Map.Entry;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-/**
- * Load and caches telephony vvm config from res/xml/vvm_config.xml
- */
-public class TelephonyVvmConfigManager {
-
-    private static final String TAG = "TelephonyVvmCfgMgr";
-
-  private static final boolean USE_DEBUG_CONFIG = false;
-
-    private static final String TAG_PERSISTABLEMAP = "pbundle_as_map";
-
-    static final String KEY_MCCMNC = "mccmnc";
-
-    private static Map<String, PersistableBundle> sCachedConfigs;
-
-    private final Map<String, PersistableBundle> mConfigs;
-
-    public TelephonyVvmConfigManager(Resources resources) {
-        if (sCachedConfigs == null) {
-            sCachedConfigs = loadConfigs(resources.getXml(R.xml.vvm_config));
-        }
-        mConfigs = sCachedConfigs;
-    }
-
-    @VisibleForTesting
-    TelephonyVvmConfigManager(XmlPullParser parser) {
-        mConfigs = loadConfigs(parser);
-    }
-
-    @Nullable
-    public PersistableBundle getConfig(String mccMnc) {
-        if (USE_DEBUG_CONFIG) {
-            return mConfigs.get("TEST");
-        }
-        return mConfigs.get(mccMnc);
-    }
-
-    private static Map<String, PersistableBundle> loadConfigs(XmlPullParser parser) {
-        Map<String, PersistableBundle> configs = new ArrayMap<>();
-        try {
-            ArrayList list = readBundleList(parser);
-            for (Object object : list) {
-                if (!(object instanceof PersistableBundle)) {
-                    throw new IllegalArgumentException("PersistableBundle expected, got " + object);
-                }
-                PersistableBundle bundle = (PersistableBundle) object;
-                String[] mccMncs = bundle.getStringArray(KEY_MCCMNC);
-                if (mccMncs == null) {
-                    throw new IllegalArgumentException("MCCMNC is null");
-                }
-                for (String mccMnc : mccMncs) {
-                    configs.put(mccMnc, bundle);
-                }
-            }
-        } catch (IOException | XmlPullParserException e) {
-            throw new RuntimeException(e);
-        }
-        return configs;
-    }
-
-    @Nullable
-    public static ArrayList readBundleList(XmlPullParser in) throws IOException,
-            XmlPullParserException {
-        final int outerDepth = in.getDepth();
-        int event;
-        while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
-                (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
-            if (event == XmlPullParser.START_TAG) {
-                final String startTag = in.getName();
-                final String[] tagName = new String[1];
-                in.next();
-                return XmlUtils.readThisListXml(in, startTag, tagName,
-                        new MyReadMapCallback(), false);
-            }
-        }
-        return null;
-    }
-
-    public static PersistableBundle restoreFromXml(XmlPullParser in) throws IOException,
-            XmlPullParserException {
-        final int outerDepth = in.getDepth();
-        final String startTag = in.getName();
-        final String[] tagName = new String[1];
-        int event;
-        while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
-                (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
-            if (event == XmlPullParser.START_TAG) {
-                ArrayMap<String, ?> map =
-                        XmlUtils.readThisArrayMapXml(in, startTag, tagName,
-                                new MyReadMapCallback());
-                PersistableBundle result = new PersistableBundle();
-                for (Entry<String, ?> entry : map.entrySet()) {
-                    Object value = entry.getValue();
-                    if (value instanceof Integer) {
-                        result.putInt(entry.getKey(), (int) value);
-                    } else if (value instanceof Boolean) {
-                        result.putBoolean(entry.getKey(), (boolean) value);
-                    } else if (value instanceof String) {
-                        result.putString(entry.getKey(), (String) value);
-                    } else if (value instanceof String[]) {
-                        result.putStringArray(entry.getKey(), (String[]) value);
-                    } else if (value instanceof PersistableBundle) {
-                        result.putPersistableBundle(entry.getKey(), (PersistableBundle) value);
-                    }
-                }
-                return result;
-            }
-        }
-        return PersistableBundle.EMPTY;
-    }
-
-    static class MyReadMapCallback implements XmlUtils.ReadMapCallback {
-
-        @Override
-        public Object readThisUnknownObjectXml(XmlPullParser in, String tag)
-                throws XmlPullParserException, IOException {
-            if (TAG_PERSISTABLEMAP.equals(tag)) {
-                return restoreFromXml(in);
-            }
-            throw new XmlPullParserException("Unknown tag=" + tag);
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/VisualVoicemailPreferences.java b/java/com/android/voicemailomtp/VisualVoicemailPreferences.java
deleted file mode 100644
index 5bc2c69..0000000
--- a/java/com/android/voicemailomtp/VisualVoicemailPreferences.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import java.util.Set;
-
-/**
- * Save visual voicemail values in shared preferences to be retrieved later. Because a voicemail
- * source is tied 1:1 to a phone account, the phone account handle is used in the key for each
- * voicemail source and the associated data.
- */
-public class VisualVoicemailPreferences {
-
-    private static final String VISUAL_VOICEMAIL_SHARED_PREFS_KEY_PREFIX =
-            "visual_voicemail_";
-
-    private final SharedPreferences mPreferences;
-    private final PhoneAccountHandle mPhoneAccountHandle;
-
-    public VisualVoicemailPreferences(Context context, PhoneAccountHandle phoneAccountHandle) {
-        mPreferences = PreferenceManager.getDefaultSharedPreferences(context);
-        mPhoneAccountHandle = phoneAccountHandle;
-    }
-
-    public class Editor {
-
-        private final SharedPreferences.Editor mEditor;
-
-        private Editor() {
-            mEditor = mPreferences.edit();
-        }
-
-        public void apply() {
-            mEditor.apply();
-        }
-
-        public Editor putBoolean(String key, boolean value) {
-            mEditor.putBoolean(getKey(key), value);
-            return this;
-        }
-
-        @NeededForTesting
-        public Editor putFloat(String key, float value) {
-            mEditor.putFloat(getKey(key), value);
-            return this;
-        }
-
-        public Editor putInt(String key, int value) {
-            mEditor.putInt(getKey(key), value);
-            return this;
-        }
-
-        @NeededForTesting
-        public Editor putLong(String key, long value) {
-            mEditor.putLong(getKey(key), value);
-            return this;
-        }
-
-        public Editor putString(String key, String value) {
-            mEditor.putString(getKey(key), value);
-            return this;
-        }
-
-        @NeededForTesting
-        public Editor putStringSet(String key, Set<String> value) {
-            mEditor.putStringSet(getKey(key), value);
-            return this;
-        }
-    }
-
-    public Editor edit() {
-        return new Editor();
-    }
-
-    public boolean getBoolean(String key, boolean defValue) {
-        return getValue(key, defValue);
-    }
-
-    @NeededForTesting
-    public float getFloat(String key, float defValue) {
-        return getValue(key, defValue);
-    }
-
-    public int getInt(String key, int defValue) {
-        return getValue(key, defValue);
-    }
-
-    @NeededForTesting
-    public long getLong(String key, long defValue) {
-        return getValue(key, defValue);
-    }
-
-    public String getString(String key, String defValue) {
-        return getValue(key, defValue);
-    }
-
-    @Nullable
-    public String getString(String key) {
-        return getValue(key, null);
-    }
-
-    @NeededForTesting
-    public Set<String> getStringSet(String key, Set<String> defValue) {
-        return getValue(key, defValue);
-    }
-
-    public boolean contains(String key) {
-        return mPreferences.contains(getKey(key));
-    }
-
-    private <T> T getValue(String key, T defValue) {
-        if (!contains(key)) {
-            return defValue;
-        }
-        Object object = mPreferences.getAll().get(getKey(key));
-        if (object == null) {
-            return defValue;
-        }
-        return (T) object;
-    }
-
-    private String getKey(String key) {
-        return VISUAL_VOICEMAIL_SHARED_PREFS_KEY_PREFIX + key + "_" + mPhoneAccountHandle.getId();
-    }
-}
diff --git a/java/com/android/voicemailomtp/Voicemail.java b/java/com/android/voicemailomtp/Voicemail.java
deleted file mode 100644
index 9d83951..0000000
--- a/java/com/android/voicemailomtp/Voicemail.java
+++ /dev/null
@@ -1,330 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp;
-
-import android.net.Uri;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.telecom.PhoneAccountHandle;
-import android.text.TextUtils;
-
-/**
- * Represents a single voicemail stored in the voicemail content provider.
- */
-public class Voicemail implements Parcelable {
-
-    private final Long mTimestamp;
-    private final String mNumber;
-    private final PhoneAccountHandle mPhoneAccount;
-    private final Long mId;
-    private final Long mDuration;
-    private final String mSource;
-    private final String mProviderData;
-    private final Uri mUri;
-    private final Boolean mIsRead;
-    private final Boolean mHasContent;
-    private final String mTranscription;
-
-    private Voicemail(Long timestamp, String number, PhoneAccountHandle phoneAccountHandle, Long id,
-            Long duration, String source, String providerData, Uri uri, Boolean isRead,
-            Boolean hasContent, String transcription) {
-        mTimestamp = timestamp;
-        mNumber = number;
-        mPhoneAccount = phoneAccountHandle;
-        mId = id;
-        mDuration = duration;
-        mSource = source;
-        mProviderData = providerData;
-        mUri = uri;
-        mIsRead = isRead;
-        mHasContent = hasContent;
-        mTranscription = transcription;
-    }
-
-    /**
-     * Create a {@link Builder} for a new {@link Voicemail} to be inserted. <p> The number and the
-     * timestamp are mandatory for insertion.
-     */
-    public static Builder createForInsertion(long timestamp, String number) {
-        return new Builder().setNumber(number).setTimestamp(timestamp);
-    }
-
-    /**
-     * Create a {@link Builder} for a {@link Voicemail} to be updated (or deleted). <p> The id and
-     * source data fields are mandatory for update - id is necessary for updating the database and
-     * source data is necessary for updating the server.
-     */
-    public static Builder createForUpdate(long id, String sourceData) {
-        return new Builder().setId(id).setSourceData(sourceData);
-    }
-
-    /**
-     * Builder pattern for creating a {@link Voicemail}. The builder must be created with the {@link
-     * #createForInsertion(long, String)} method. <p> This class is <b>not thread safe</b>
-     */
-    public static class Builder {
-
-        private Long mBuilderTimestamp;
-        private String mBuilderNumber;
-        private PhoneAccountHandle mBuilderPhoneAccount;
-        private Long mBuilderId;
-        private Long mBuilderDuration;
-        private String mBuilderSourcePackage;
-        private String mBuilderSourceData;
-        private Uri mBuilderUri;
-        private Boolean mBuilderIsRead;
-        private boolean mBuilderHasContent;
-        private String mBuilderTranscription;
-
-        /**
-         * You should use the correct factory method to construct a builder.
-         */
-        private Builder() {
-        }
-
-        public Builder setNumber(String number) {
-            mBuilderNumber = number;
-            return this;
-        }
-
-        public Builder setTimestamp(long timestamp) {
-            mBuilderTimestamp = timestamp;
-            return this;
-        }
-
-        public Builder setPhoneAccount(PhoneAccountHandle phoneAccount) {
-            mBuilderPhoneAccount = phoneAccount;
-            return this;
-        }
-
-        public Builder setId(long id) {
-            mBuilderId = id;
-            return this;
-        }
-
-        public Builder setDuration(long duration) {
-            mBuilderDuration = duration;
-            return this;
-        }
-
-        public Builder setSourcePackage(String sourcePackage) {
-            mBuilderSourcePackage = sourcePackage;
-            return this;
-        }
-
-        public Builder setSourceData(String sourceData) {
-            mBuilderSourceData = sourceData;
-            return this;
-        }
-
-        public Builder setUri(Uri uri) {
-            mBuilderUri = uri;
-            return this;
-        }
-
-        public Builder setIsRead(boolean isRead) {
-            mBuilderIsRead = isRead;
-            return this;
-        }
-
-        public Builder setHasContent(boolean hasContent) {
-            mBuilderHasContent = hasContent;
-            return this;
-        }
-
-        public Builder setTranscription(String transcription) {
-            mBuilderTranscription = transcription;
-            return this;
-        }
-
-        public Voicemail build() {
-            mBuilderId = mBuilderId == null ? -1 : mBuilderId;
-            mBuilderTimestamp = mBuilderTimestamp == null ? 0 : mBuilderTimestamp;
-            mBuilderDuration = mBuilderDuration == null ? 0 : mBuilderDuration;
-            mBuilderIsRead = mBuilderIsRead == null ? false : mBuilderIsRead;
-            return new Voicemail(mBuilderTimestamp, mBuilderNumber, mBuilderPhoneAccount,
-                    mBuilderId, mBuilderDuration, mBuilderSourcePackage, mBuilderSourceData,
-                    mBuilderUri, mBuilderIsRead, mBuilderHasContent, mBuilderTranscription);
-        }
-    }
-
-    /**
-     * The identifier of the voicemail in the content provider. <p> This may be missing in the case
-     * of a new {@link Voicemail} that we plan to insert into the content provider, since until it
-     * has been inserted we don't know what id it should have. If none is specified, we return -1.
-     */
-    public long getId() {
-        return mId;
-    }
-
-    /**
-     * The number of the person leaving the voicemail, empty string if unknown, null if not set.
-     */
-    public String getNumber() {
-        return mNumber;
-    }
-
-    /**
-     * The phone account associated with the voicemail, null if not set.
-     */
-    public PhoneAccountHandle getPhoneAccount() {
-        return mPhoneAccount;
-    }
-
-    /**
-     * The timestamp the voicemail was received, in millis since the epoch, zero if not set.
-     */
-    public long getTimestampMillis() {
-        return mTimestamp;
-    }
-
-    /**
-     * Gets the duration of the voicemail in millis, or zero if the field is not set.
-     */
-    public long getDuration() {
-        return mDuration;
-    }
-
-    /**
-     * Returns the package name of the source that added this voicemail, or null if this field is
-     * not set.
-     */
-    public String getSourcePackage() {
-        return mSource;
-    }
-
-    /**
-     * Returns the application-specific data type stored with the voicemail, or null if this field
-     * is not set. <p> Source data is typically used as an identifier to uniquely identify the
-     * voicemail against the voicemail server. This is likely to be something like the IMAP UID, or
-     * some other server-generated identifying string.
-     */
-    public String getSourceData() {
-        return mProviderData;
-    }
-
-    /**
-     * Gets the Uri that can be used to refer to this voicemail, and to make it play. <p> Returns
-     * null if we don't know the Uri.
-     */
-    public Uri getUri() {
-        return mUri;
-    }
-
-    /**
-     * Tells us if the voicemail message has been marked as read. <p> Always returns false if this
-     * field has not been set, i.e. if hasRead() returns false.
-     */
-    public boolean isRead() {
-        return mIsRead;
-    }
-
-    /**
-     * Tells us if there is content stored at the Uri.
-     */
-    public boolean hasContent() {
-        return mHasContent;
-    }
-
-    /**
-     * Returns the text transcription of this voicemail, or null if this field is not set.
-     */
-    public String getTranscription() {
-        return mTranscription;
-    }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(Parcel dest, int flags) {
-        dest.writeLong(mTimestamp);
-        writeCharSequence(dest, mNumber);
-        if (mPhoneAccount == null) {
-            dest.writeInt(0);
-        } else {
-            dest.writeInt(1);
-            mPhoneAccount.writeToParcel(dest, flags);
-        }
-        dest.writeLong(mId);
-        dest.writeLong(mDuration);
-        writeCharSequence(dest, mSource);
-        writeCharSequence(dest, mProviderData);
-        if (mUri == null) {
-            dest.writeInt(0);
-        } else {
-            dest.writeInt(1);
-            mUri.writeToParcel(dest, flags);
-        }
-        if (mIsRead) {
-            dest.writeInt(1);
-        } else {
-            dest.writeInt(0);
-        }
-        if (mHasContent) {
-            dest.writeInt(1);
-        } else {
-            dest.writeInt(0);
-        }
-        writeCharSequence(dest, mTranscription);
-    }
-
-    public static final Creator<Voicemail> CREATOR
-            = new Creator<Voicemail>() {
-        @Override
-        public Voicemail createFromParcel(Parcel in) {
-            return new Voicemail(in);
-        }
-
-        @Override
-        public Voicemail[] newArray(int size) {
-            return new Voicemail[size];
-        }
-    };
-
-    private Voicemail(Parcel in) {
-        mTimestamp = in.readLong();
-        mNumber = (String) readCharSequence(in);
-        if (in.readInt() > 0) {
-            mPhoneAccount = PhoneAccountHandle.CREATOR.createFromParcel(in);
-        } else {
-            mPhoneAccount = null;
-        }
-        mId = in.readLong();
-        mDuration = in.readLong();
-        mSource = (String) readCharSequence(in);
-        mProviderData = (String) readCharSequence(in);
-        if (in.readInt() > 0) {
-            mUri = Uri.CREATOR.createFromParcel(in);
-        } else {
-            mUri = null;
-        }
-        mIsRead = in.readInt() > 0 ? true : false;
-        mHasContent = in.readInt() > 0 ? true : false;
-        mTranscription = (String) readCharSequence(in);
-    }
-
-    private static CharSequence readCharSequence(Parcel in) {
-        return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
-    }
-
-    public static void writeCharSequence(Parcel dest, CharSequence val) {
-        TextUtils.writeToParcel(val, dest, 0);
-    }
-}
diff --git a/java/com/android/voicemailomtp/VoicemailStatus.java b/java/com/android/voicemailomtp/VoicemailStatus.java
deleted file mode 100644
index 6300793..0000000
--- a/java/com/android/voicemailomtp/VoicemailStatus.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.net.Uri;
-import android.provider.VoicemailContract;
-import android.provider.VoicemailContract.Status;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-
-public class VoicemailStatus {
-
-    private static final String TAG = "VvmStatus";
-
-    public static class Editor {
-
-        private final Context mContext;
-        @Nullable
-        private final PhoneAccountHandle mPhoneAccountHandle;
-
-        private ContentValues mValues = new ContentValues();
-
-        private Editor(Context context, PhoneAccountHandle phoneAccountHandle) {
-            mContext = context;
-            mPhoneAccountHandle = phoneAccountHandle;
-            if (mPhoneAccountHandle == null) {
-                VvmLog.w(TAG, "VoicemailStatus.Editor created with null phone account, status will"
-                        + " not be written");
-            }
-        }
-
-        @Nullable
-        public PhoneAccountHandle getPhoneAccountHandle() {
-            return mPhoneAccountHandle;
-        }
-
-        public Editor setType(String type) {
-            mValues.put(Status.SOURCE_TYPE, type);
-            return this;
-        }
-
-        public Editor setConfigurationState(int configurationState) {
-            mValues.put(Status.CONFIGURATION_STATE, configurationState);
-            return this;
-        }
-
-        public Editor setDataChannelState(int dataChannelState) {
-            mValues.put(Status.DATA_CHANNEL_STATE, dataChannelState);
-            return this;
-        }
-
-        public Editor setNotificationChannelState(int notificationChannelState) {
-            mValues.put(Status.NOTIFICATION_CHANNEL_STATE, notificationChannelState);
-            return this;
-        }
-
-        public Editor setQuota(int occupied, int total) {
-            if (occupied == VoicemailContract.Status.QUOTA_UNAVAILABLE
-                    && total == VoicemailContract.Status.QUOTA_UNAVAILABLE) {
-                return this;
-            }
-
-            mValues.put(Status.QUOTA_OCCUPIED, occupied);
-            mValues.put(Status.QUOTA_TOTAL, total);
-            return this;
-        }
-
-        /**
-         * Apply the changes to the {@link VoicemailStatus} {@link #Editor}.
-         *
-         * @return {@code true} if the changes were successfully applied, {@code false} otherwise.
-         */
-        public boolean apply() {
-            if (mPhoneAccountHandle == null) {
-                return false;
-            }
-            mValues.put(Status.PHONE_ACCOUNT_COMPONENT_NAME,
-                    mPhoneAccountHandle.getComponentName().flattenToString());
-            mValues.put(Status.PHONE_ACCOUNT_ID, mPhoneAccountHandle.getId());
-            ContentResolver contentResolver = mContext.getContentResolver();
-            Uri statusUri = VoicemailContract.Status.buildSourceUri(mContext.getPackageName());
-            try {
-                contentResolver.insert(statusUri, mValues);
-            } catch (IllegalArgumentException iae) {
-                VvmLog.e(TAG, "apply :: failed to insert content resolver ", iae);
-                mValues.clear();
-                return false;
-            }
-            mValues.clear();
-            return true;
-        }
-
-        public ContentValues getValues() {
-            return mValues;
-        }
-    }
-
-    /**
-     * A voicemail status editor that the decision of whether to actually write to the database can
-     * be deferred. This object will be passed around as a usual {@link Editor}, but {@link
-     * #apply()} doesn't do anything. If later the creator of this object decides any status changes
-     * written to it should be committed, {@link #deferredApply()} should be called.
-     */
-    public static class DeferredEditor extends Editor {
-
-        private DeferredEditor(Context context, PhoneAccountHandle phoneAccountHandle) {
-            super(context, phoneAccountHandle);
-        }
-
-        @Override
-        public boolean apply() {
-            // Do nothing
-            return true;
-        }
-
-        public void deferredApply() {
-            super.apply();
-        }
-    }
-
-    public static Editor edit(Context context, PhoneAccountHandle phoneAccountHandle) {
-        return new Editor(context, phoneAccountHandle);
-    }
-
-    /**
-     * Reset the status to the "disabled" state, which the UI should not show anything for this
-     * phoneAccountHandle.
-     */
-    public static void disable(Context context, PhoneAccountHandle phoneAccountHandle) {
-        edit(context, phoneAccountHandle)
-                .setConfigurationState(Status.CONFIGURATION_STATE_NOT_CONFIGURED)
-                .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION)
-                .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION)
-                .apply();
-    }
-
-    public static DeferredEditor deferredEdit(Context context,
-            PhoneAccountHandle phoneAccountHandle) {
-        return new DeferredEditor(context, phoneAccountHandle);
-    }
-}
diff --git a/java/com/android/voicemailomtp/VvmLog.java b/java/com/android/voicemailomtp/VvmLog.java
deleted file mode 100644
index 2add66a..0000000
--- a/java/com/android/voicemailomtp/VvmLog.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp;
-
-import android.util.Log;
-import com.android.voicemailomtp.utils.IndentingPrintWriter;
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.ArrayDeque;
-import java.util.Calendar;
-import java.util.Deque;
-import java.util.Iterator;
-
-/**
- * Helper methods for adding to OMTP visual voicemail local logs.
- */
-public class VvmLog {
-
-    private static final int MAX_OMTP_VVM_LOGS = 100;
-
-    private static final LocalLog sLocalLog = new LocalLog(MAX_OMTP_VVM_LOGS);
-
-    public static void log(String tag, String log) {
-        sLocalLog.log(tag + ": " + log);
-    }
-
-    public static void dump(FileDescriptor fd, PrintWriter printwriter, String[] args) {
-        IndentingPrintWriter indentingPrintWriter = new IndentingPrintWriter(printwriter, "  ");
-        indentingPrintWriter.increaseIndent();
-        sLocalLog.dump(fd, indentingPrintWriter, args);
-        indentingPrintWriter.decreaseIndent();
-    }
-
-    public static int e(String tag, String log) {
-        log(tag, log);
-        return Log.e(tag, log);
-    }
-
-    public static int e(String tag, String log, Throwable e) {
-        log(tag, log + " " + e);
-        return Log.e(tag, log, e);
-    }
-
-    public static int w(String tag, String log) {
-        log(tag, log);
-        return Log.w(tag, log);
-    }
-
-    public static int w(String tag, String log, Throwable e) {
-        log(tag, log + " " + e);
-        return Log.w(tag, log, e);
-    }
-
-    public static int i(String tag, String log) {
-        log(tag, log);
-        return Log.i(tag, log);
-    }
-
-    public static int i(String tag, String log, Throwable e) {
-        log(tag, log + " " + e);
-        return Log.i(tag, log, e);
-    }
-
-    public static int d(String tag, String log) {
-        log(tag, log);
-        return Log.d(tag, log);
-    }
-
-    public static int d(String tag, String log, Throwable e) {
-        log(tag, log + " " + e);
-        return Log.d(tag, log, e);
-    }
-
-    public static int v(String tag, String log) {
-        log(tag, log);
-        return Log.v(tag, log);
-    }
-
-    public static int v(String tag, String log, Throwable e) {
-        log(tag, log + " " + e);
-        return Log.v(tag, log, e);
-    }
-
-    public static int wtf(String tag, String log) {
-        log(tag, log);
-        return Log.wtf(tag, log);
-    }
-
-    public static int wtf(String tag, String log, Throwable e) {
-        log(tag, log + " " + e);
-        return Log.wtf(tag, log, e);
-    }
-
-    /**
-     * Redact personally identifiable information for production users. If we are running in verbose
-     * mode, return the original string, otherwise return a SHA-1 hash of the input string.
-     */
-    public static String pii(Object pii) {
-        if (pii == null) {
-            return String.valueOf(pii);
-        }
-        return "[PII]";
-    }
-
-    public static class LocalLog {
-
-        private final Deque<String> mLog;
-        private final int mMaxLines;
-
-        public LocalLog(int maxLines) {
-            mMaxLines = Math.max(0, maxLines);
-            mLog = new ArrayDeque<>(mMaxLines);
-        }
-
-        public void log(String msg) {
-            if (mMaxLines <= 0) {
-                return;
-            }
-            Calendar c = Calendar.getInstance();
-            c.setTimeInMillis(System.currentTimeMillis());
-            append(String.format("%tm-%td %tH:%tM:%tS.%tL - %s", c, c, c, c, c, c, msg));
-        }
-
-        private synchronized void append(String logLine) {
-            while (mLog.size() >= mMaxLines) {
-                mLog.remove();
-            }
-            mLog.add(logLine);
-        }
-
-        public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-            Iterator<String> itr = mLog.iterator();
-            while (itr.hasNext()) {
-                pw.println(itr.next());
-            }
-        }
-
-        public synchronized void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
-            Iterator<String> itr = mLog.descendingIterator();
-            while (itr.hasNext()) {
-                pw.println(itr.next());
-            }
-        }
-
-        public static class ReadOnlyLocalLog {
-
-            private final LocalLog mLog;
-
-            ReadOnlyLocalLog(LocalLog log) {
-                mLog = log;
-            }
-
-            public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-                mLog.dump(fd, pw, args);
-            }
-
-            public void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
-                mLog.reverseDump(fd, pw, args);
-            }
-        }
-
-        public ReadOnlyLocalLog readOnlyLocalLog() {
-            return new ReadOnlyLocalLog(this);
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/VvmPackageInstallReceiver.java b/java/com/android/voicemailomtp/VvmPackageInstallReceiver.java
deleted file mode 100644
index 7d9eee9..0000000
--- a/java/com/android/voicemailomtp/VvmPackageInstallReceiver.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.telecom.PhoneAccountHandle;
-
-import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil;
-import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
-
-import java.util.Set;
-
-/**
- * When a new package is installed, check if it matches any of the vvm carrier apps of the currently
- * enabled dialer vvm sources.
- */
-public class VvmPackageInstallReceiver extends BroadcastReceiver {
-
-    private static final String TAG = "VvmPkgInstallReceiver";
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        if (intent.getData() == null) {
-            return;
-        }
-
-        String packageName = intent.getData().getSchemeSpecificPart();
-        if (packageName == null) {
-            return;
-        }
-
-        OmtpVvmSourceManager vvmSourceManager = OmtpVvmSourceManager.getInstance(context);
-        Set<PhoneAccountHandle> phoneAccounts = vvmSourceManager.getOmtpVvmSources();
-        for (PhoneAccountHandle phoneAccount : phoneAccounts) {
-            if (VisualVoicemailSettingsUtil.isEnabledUserSet(context, phoneAccount)) {
-                // Skip the check if this voicemail source's setting is overridden by the user.
-                continue;
-            }
-
-            OmtpVvmCarrierConfigHelper carrierConfigHelper = new OmtpVvmCarrierConfigHelper(
-                    context, phoneAccount);
-            if (carrierConfigHelper.getCarrierVvmPackageNames() == null) {
-                continue;
-            }
-            if (carrierConfigHelper.getCarrierVvmPackageNames().contains(packageName)) {
-                // Force deactivate the client. The user can re-enable it in the settings.
-                // There are no need to update the settings for deactivation. At this point, if the
-                // default value is used it should be false because a carrier package is present.
-                VvmLog.i(TAG, "Carrier VVM package installed, disabling system VVM client");
-                OmtpVvmSourceManager.getInstance(context).removeSource(phoneAccount);
-                carrierConfigHelper.startDeactivation();
-            }
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/VvmPhoneStateListener.java b/java/com/android/voicemailomtp/VvmPhoneStateListener.java
deleted file mode 100644
index 1a3013d..0000000
--- a/java/com/android/voicemailomtp/VvmPhoneStateListener.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp;
-
-import android.content.Context;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.PhoneStateListener;
-import android.telephony.ServiceState;
-
-import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
-import com.android.voicemailomtp.sync.OmtpVvmSyncService;
-import com.android.voicemailomtp.sync.SyncTask;
-import com.android.voicemailomtp.sync.VoicemailStatusQueryHelper;
-
-/**
- * Check if service is lost and indicate this in the voicemail status.
- */
-public class VvmPhoneStateListener extends PhoneStateListener {
-
-    private static final String TAG = "VvmPhoneStateListener";
-
-    private PhoneAccountHandle mPhoneAccount;
-    private Context mContext;
-    private int mPreviousState = -1;
-
-    public VvmPhoneStateListener(Context context, PhoneAccountHandle accountHandle) {
-        // TODO: b/32637799 too much trouble to call super constructor through reflection,
-        // just use non-phoneAccountHandle version for now.
-        super();
-        mContext = context;
-        mPhoneAccount = accountHandle;
-    }
-
-    @Override
-    public void onServiceStateChanged(ServiceState serviceState) {
-        if (mPhoneAccount == null) {
-            VvmLog.e(TAG, "onServiceStateChanged on phoneAccount " + mPhoneAccount
-                    + " with invalid phoneAccountHandle, ignoring");
-            return;
-        }
-
-        int state = serviceState.getState();
-        if (state == mPreviousState || (state != ServiceState.STATE_IN_SERVICE
-                && mPreviousState != ServiceState.STATE_IN_SERVICE)) {
-            // Only interested in state changes or transitioning into or out of "in service".
-            // Otherwise just quit.
-            mPreviousState = state;
-            return;
-        }
-
-        OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(mContext, mPhoneAccount);
-
-        if (state == ServiceState.STATE_IN_SERVICE) {
-            VoicemailStatusQueryHelper voicemailStatusQueryHelper =
-                    new VoicemailStatusQueryHelper(mContext);
-            if (voicemailStatusQueryHelper.isVoicemailSourceConfigured(mPhoneAccount)) {
-                if (!voicemailStatusQueryHelper.isNotificationsChannelActive(mPhoneAccount)) {
-                    VvmLog
-                            .v(TAG, "Notifications channel is active for " + mPhoneAccount);
-                    helper.handleEvent(VoicemailStatus.edit(mContext, mPhoneAccount),
-                        OmtpEvents.NOTIFICATION_IN_SERVICE);
-                }
-            }
-
-            if (OmtpVvmSourceManager.getInstance(mContext).isVvmSourceRegistered(mPhoneAccount)) {
-                VvmLog
-                        .v(TAG, "Signal returned: requesting resync for " + mPhoneAccount);
-                // If the source is already registered, run a full sync in case something was missed
-                // while signal was down.
-                SyncTask.start(mContext, mPhoneAccount, OmtpVvmSyncService.SYNC_FULL_SYNC);
-            } else {
-                VvmLog.v(TAG,
-                        "Signal returned: reattempting activation for " + mPhoneAccount);
-                // Otherwise initiate an activation because this means that an OMTP source was
-                // recognized but either the activation text was not successfully sent or a response
-                // was not received.
-                helper.startActivation();
-            }
-        } else {
-            VvmLog.v(TAG, "Notifications channel is inactive for " + mPhoneAccount);
-
-            if (!OmtpVvmSourceManager.getInstance(mContext).isVvmSourceRegistered(mPhoneAccount)) {
-                return;
-            }
-            helper.handleEvent(VoicemailStatus.edit(mContext, mPhoneAccount),
-                OmtpEvents.NOTIFICATION_SERVICE_LOST);
-        }
-        mPreviousState = state;
-    }
-}
diff --git a/java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java b/java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java
deleted file mode 100644
index 85fea80..0000000
--- a/java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.fetch;
-
-import android.annotation.TargetApi;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.net.Network;
-import android.net.Uri;
-import android.os.Build.VERSION_CODES;
-import android.provider.VoicemailContract;
-import android.provider.VoicemailContract.Voicemails;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.os.BuildCompat;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.TelephonyManager;
-import android.text.TextUtils;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.imap.ImapHelper;
-import com.android.voicemailomtp.imap.ImapHelper.InitializingException;
-import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
-import com.android.voicemailomtp.sync.VvmNetworkRequestCallback;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class FetchVoicemailReceiver extends BroadcastReceiver {
-
-    private static final String TAG = "FetchVoicemailReceiver";
-
-    final static String[] PROJECTION = new String[]{
-            Voicemails.SOURCE_DATA,      // 0
-            Voicemails.PHONE_ACCOUNT_ID, // 1
-            Voicemails.PHONE_ACCOUNT_COMPONENT_NAME, // 2
-    };
-
-    public static final int SOURCE_DATA = 0;
-    public static final int PHONE_ACCOUNT_ID = 1;
-    public static final int PHONE_ACCOUNT_COMPONENT_NAME = 2;
-
-    // Number of retries
-    private static final int NETWORK_RETRY_COUNT = 3;
-
-    private ContentResolver mContentResolver;
-    private Uri mUri;
-    private VvmNetworkRequestCallback mNetworkCallback;
-    private Context mContext;
-    private String mUid;
-    private PhoneAccountHandle mPhoneAccount;
-    private int mRetryCount = NETWORK_RETRY_COUNT;
-
-    @Override
-    public void onReceive(final Context context, Intent intent) {
-        if (VoicemailContract.ACTION_FETCH_VOICEMAIL.equals(intent.getAction())) {
-            VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL received");
-            mContext = context;
-            mContentResolver = context.getContentResolver();
-            mUri = intent.getData();
-
-            if (mUri == null) {
-                VvmLog.w(TAG,
-                        VoicemailContract.ACTION_FETCH_VOICEMAIL + " intent sent with no data");
-                return;
-            }
-
-            if (!context.getPackageName().equals(
-                    mUri.getQueryParameter(VoicemailContract.PARAM_KEY_SOURCE_PACKAGE))) {
-                // Ignore if the fetch request is for a voicemail not from this package.
-                VvmLog.e(TAG,
-                        "ACTION_FETCH_VOICEMAIL from foreign pacakge " + context.getPackageName());
-                return;
-            }
-
-            Cursor cursor = mContentResolver.query(mUri, PROJECTION, null, null, null);
-            if (cursor == null) {
-                VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL query returned null");
-                return;
-            }
-            try {
-                if (cursor.moveToFirst()) {
-                    mUid = cursor.getString(SOURCE_DATA);
-                    String accountId = cursor.getString(PHONE_ACCOUNT_ID);
-                    if (TextUtils.isEmpty(accountId)) {
-                        TelephonyManager telephonyManager = (TelephonyManager)
-                                context.getSystemService(Context.TELEPHONY_SERVICE);
-                        accountId = telephonyManager.getSimSerialNumber();
-
-                        if (TextUtils.isEmpty(accountId)) {
-                            VvmLog.e(TAG, "Account null and no default sim found.");
-                            return;
-                        }
-                    }
-
-                    mPhoneAccount = new PhoneAccountHandle(
-                            ComponentName.unflattenFromString(
-                                    cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME)),
-                            cursor.getString(PHONE_ACCOUNT_ID));
-                    if (!OmtpVvmSourceManager.getInstance(context)
-                            .isVvmSourceRegistered(mPhoneAccount)) {
-                        mPhoneAccount = getAccountFromMarshmallowAccount(context, mPhoneAccount);
-                        if (mPhoneAccount == null) {
-                            VvmLog.w(TAG, "Account not registered - cannot retrieve message.");
-                            return;
-                        }
-                        VvmLog.i(TAG, "Fetching voicemail with Marshmallow PhoneAccountHandle");
-                    }
-                    VvmLog.i(TAG, "Requesting network to fetch voicemail");
-                    mNetworkCallback = new fetchVoicemailNetworkRequestCallback(context,
-                            mPhoneAccount);
-                    mNetworkCallback.requestNetwork();
-                }
-            } finally {
-                cursor.close();
-            }
-        }
-    }
-
-    /**
-     * In ag/930496 the format of PhoneAccountHandle has changed between Marshmallow and Nougat.
-     * This method attempts to search the account from the old database in registered sources using
-     * the old format. There's a chance of M phone account collisions on multi-SIM devices, but
-     * visual voicemail is not supported on M multi-SIM.
-     */
-    @Nullable
-    private static PhoneAccountHandle getAccountFromMarshmallowAccount(Context context,
-            PhoneAccountHandle oldAccount) {
-        if (!BuildCompat.isAtLeastN()) {
-            return null;
-        }
-        for (PhoneAccountHandle handle : OmtpVvmSourceManager.getInstance(context)
-                .getOmtpVvmSources()) {
-            if (getIccSerialNumberFromFullIccSerialNumber(handle.getId())
-                    .equals(oldAccount.getId())) {
-                return handle;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * getIccSerialNumber() is used for ID before N, and getFullIccSerialNumber() after.
-     * getIccSerialNumber() stops at the first hex char.
-     */
-    @NonNull
-    private static String getIccSerialNumberFromFullIccSerialNumber(@NonNull String id) {
-        for(int i =0;i<id.length();i++){
-            if(!Character.isDigit(id.charAt(i))){
-                return id.substring(0,i);
-            }
-        }
-        return id;
-    }
-
-    private class fetchVoicemailNetworkRequestCallback extends VvmNetworkRequestCallback {
-
-        public fetchVoicemailNetworkRequestCallback(Context context,
-                PhoneAccountHandle phoneAccount) {
-            super(context, phoneAccount, VoicemailStatus.edit(context, phoneAccount));
-        }
-
-        @Override
-        public void onAvailable(final Network network) {
-            super.onAvailable(network);
-            fetchVoicemail(network, getVoicemailStatusEditor());
-        }
-    }
-
-    private void fetchVoicemail(final Network network, final VoicemailStatus.Editor status) {
-        Executor executor = Executors.newCachedThreadPool();
-        executor.execute(new Runnable() {
-            @Override
-            public void run() {
-                try {
-                    while (mRetryCount > 0) {
-                        VvmLog.i(TAG, "fetching voicemail, retry count=" + mRetryCount);
-                        try (ImapHelper imapHelper = new ImapHelper(mContext, mPhoneAccount,
-                            network, status)) {
-                            boolean success = imapHelper.fetchVoicemailPayload(
-                                    new VoicemailFetchedCallback(mContext, mUri, mPhoneAccount),
-                                    mUid);
-                            if (!success && mRetryCount > 0) {
-                                VvmLog.i(TAG, "fetch voicemail failed, retrying");
-                                mRetryCount--;
-                            } else {
-                                return;
-                            }
-                        } catch (InitializingException e) {
-                          VvmLog.w(TAG, "Can't retrieve Imap credentials ", e);
-                            return;
-                        }
-                    }
-                } finally {
-                    if (mNetworkCallback != null) {
-                        mNetworkCallback.releaseNetwork();
-                    }
-                }
-            }
-        });
-    }
-}
diff --git a/java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java b/java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java
deleted file mode 100644
index 7479c4c..0000000
--- a/java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.fetch;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.net.Uri;
-import android.provider.VoicemailContract.Voicemails;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import android.telecom.TelecomManager;
-import com.android.voicemailomtp.R;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.imap.VoicemailPayload;
-import java.io.IOException;
-import java.io.OutputStream;
-import org.apache.commons.io.IOUtils;
-
-/**
- * Callback for when a voicemail payload is fetched. It copies the returned stream to the data
- * file corresponding to the voicemail.
- */
-public class VoicemailFetchedCallback {
-    private static final String TAG = "VoicemailFetchedCallback";
-
-    private final Context mContext;
-    private final ContentResolver mContentResolver;
-    private final Uri mUri;
-    private final PhoneAccountHandle mPhoneAccountHandle;
-
-    public VoicemailFetchedCallback(Context context, Uri uri,
-        PhoneAccountHandle phoneAccountHandle) {
-        mContext = context;
-        mContentResolver = context.getContentResolver();
-        mUri = uri;
-        mPhoneAccountHandle = phoneAccountHandle;
-    }
-
-    /**
-     * Saves the voicemail payload data into the voicemail provider then sets the "has_content" bit
-     * of the voicemail to "1".
-     *
-     * @param voicemailPayload The object containing the content data for the voicemail
-     */
-    public void setVoicemailContent(@Nullable VoicemailPayload voicemailPayload) {
-        if (voicemailPayload == null) {
-            VvmLog.i(TAG, "Payload not found, message has unsupported format");
-            ContentValues values = new ContentValues();
-            values.put(Voicemails.TRANSCRIPTION,
-                mContext.getString(R.string.vvm_unsupported_message_format,
-                        mContext.getSystemService(TelecomManager.class)
-                                .getVoiceMailNumber(mPhoneAccountHandle)));
-            updateVoicemail(values);
-            return;
-        }
-
-        VvmLog.d(TAG, String.format("Writing new voicemail content: %s", mUri));
-        OutputStream outputStream = null;
-
-        try {
-            outputStream = mContentResolver.openOutputStream(mUri);
-            byte[] inputBytes = voicemailPayload.getBytes();
-            if (inputBytes != null) {
-                outputStream.write(inputBytes);
-            }
-        } catch (IOException e) {
-            VvmLog.w(TAG, String.format("File not found for %s", mUri));
-            return;
-        } finally {
-            IOUtils.closeQuietly(outputStream);
-        }
-
-        // Update mime_type & has_content after we are done with file update.
-        ContentValues values = new ContentValues();
-        values.put(Voicemails.MIME_TYPE, voicemailPayload.getMimeType());
-        values.put(Voicemails.HAS_CONTENT, true);
-        updateVoicemail(values);
-    }
-
-    private void updateVoicemail(ContentValues values) {
-        int updatedCount = mContentResolver.update(mUri, values, null, null);
-        if (updatedCount != 1) {
-            VvmLog
-                .e(TAG, "Updating voicemail should have updated 1 row, was: " + updatedCount);
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/imap/ImapHelper.java b/java/com/android/voicemailomtp/imap/ImapHelper.java
deleted file mode 100644
index b2a40fb..0000000
--- a/java/com/android/voicemailomtp/imap/ImapHelper.java
+++ /dev/null
@@ -1,711 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.imap;
-
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.Network;
-import android.net.NetworkInfo;
-import android.provider.VoicemailContract;
-import android.telecom.PhoneAccountHandle;
-import android.util.Base64;
-
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.OmtpConstants.ChangePinResult;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VisualVoicemailPreferences;
-import com.android.voicemailomtp.Voicemail;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.fetch.VoicemailFetchedCallback;
-import com.android.voicemailomtp.mail.Address;
-import com.android.voicemailomtp.mail.Body;
-import com.android.voicemailomtp.mail.BodyPart;
-import com.android.voicemailomtp.mail.FetchProfile;
-import com.android.voicemailomtp.mail.Flag;
-import com.android.voicemailomtp.mail.Message;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.Multipart;
-import com.android.voicemailomtp.mail.TempDirectory;
-import com.android.voicemailomtp.mail.internet.MimeMessage;
-import com.android.voicemailomtp.mail.store.ImapConnection;
-import com.android.voicemailomtp.mail.store.ImapFolder;
-import com.android.voicemailomtp.mail.store.ImapStore;
-import com.android.voicemailomtp.mail.store.imap.ImapConstants;
-import com.android.voicemailomtp.mail.store.imap.ImapResponse;
-import com.android.voicemailomtp.mail.utils.LogUtils;
-import com.android.voicemailomtp.sync.OmtpVvmSyncService.TranscriptionFetchedCallback;
-
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Locale;
-import org.apache.commons.io.IOUtils;
-
-/**
- * A helper interface to abstract commands sent across IMAP interface for a given account.
- */
-public class ImapHelper implements Closeable {
-
-    private static final String TAG = "ImapHelper";
-
-    private ImapFolder mFolder;
-    private ImapStore mImapStore;
-
-    private final Context mContext;
-    private final PhoneAccountHandle mPhoneAccount;
-    private final Network mNetwork;
-    private final VoicemailStatus.Editor mStatus;
-
-    VisualVoicemailPreferences mPrefs;
-    private static final String PREF_KEY_QUOTA_OCCUPIED = "quota_occupied_";
-    private static final String PREF_KEY_QUOTA_TOTAL = "quota_total_";
-
-    private int mQuotaOccupied;
-    private int mQuotaTotal;
-
-    private final OmtpVvmCarrierConfigHelper mConfig;
-
-    public class InitializingException extends Exception {
-
-        public InitializingException(String message) {
-            super(message);
-        }
-    }
-
-    public ImapHelper(Context context, PhoneAccountHandle phoneAccount, Network network,
-        VoicemailStatus.Editor status)
-        throws InitializingException {
-        this(context,
-                new OmtpVvmCarrierConfigHelper(
-                        context,
-                        phoneAccount),
-                phoneAccount,
-                network,
-                status);
-    }
-
-    public ImapHelper(Context context, OmtpVvmCarrierConfigHelper config,
-        PhoneAccountHandle phoneAccount, Network network, VoicemailStatus.Editor status)
-        throws InitializingException {
-        mContext = context;
-        mPhoneAccount = phoneAccount;
-        mNetwork = network;
-        mStatus = status;
-        mConfig = config;
-        mPrefs = new VisualVoicemailPreferences(context,
-                phoneAccount);
-
-        try {
-            TempDirectory.setTempDirectory(context);
-
-            String username = mPrefs.getString(OmtpConstants.IMAP_USER_NAME, null);
-            String password = mPrefs.getString(OmtpConstants.IMAP_PASSWORD, null);
-            String serverName = mPrefs.getString(OmtpConstants.SERVER_ADDRESS, null);
-            int port = Integer.parseInt(
-                    mPrefs.getString(OmtpConstants.IMAP_PORT, null));
-            int auth = ImapStore.FLAG_NONE;
-
-            int sslPort = mConfig.getSslPort();
-            if (sslPort != 0) {
-                port = sslPort;
-                auth = ImapStore.FLAG_SSL;
-            }
-
-            mImapStore = new ImapStore(
-                    context, this, username, password, port, serverName, auth, network);
-        } catch (NumberFormatException e) {
-            handleEvent(OmtpEvents.DATA_INVALID_PORT);
-            LogUtils.w(TAG, "Could not parse port number");
-            throw new InitializingException("cannot initialize ImapHelper:" + e.toString());
-        }
-
-        mQuotaOccupied = mPrefs
-                .getInt(PREF_KEY_QUOTA_OCCUPIED, VoicemailContract.Status.QUOTA_UNAVAILABLE);
-        mQuotaTotal = mPrefs
-                .getInt(PREF_KEY_QUOTA_TOTAL, VoicemailContract.Status.QUOTA_UNAVAILABLE);
-    }
-
-    @Override
-    public void close() {
-        mImapStore.closeConnection();
-    }
-
-    public boolean isRoaming() {
-        ConnectivityManager connectivityManager = (ConnectivityManager) mContext.getSystemService(
-                Context.CONNECTIVITY_SERVICE);
-        NetworkInfo info = connectivityManager.getNetworkInfo(mNetwork);
-        if (info == null) {
-            return false;
-        }
-        return info.isRoaming();
-    }
-
-    public OmtpVvmCarrierConfigHelper getConfig() {
-        return mConfig;
-    }
-
-    public ImapConnection connect() {
-        return mImapStore.getConnection();
-    }
-
-    /**
-     * The caller thread will block until the method returns.
-     */
-    public boolean markMessagesAsRead(List<Voicemail> voicemails) {
-        return setFlags(voicemails, Flag.SEEN);
-    }
-
-    /**
-     * The caller thread will block until the method returns.
-     */
-    public boolean markMessagesAsDeleted(List<Voicemail> voicemails) {
-        return setFlags(voicemails, Flag.DELETED);
-    }
-
-    public void handleEvent(OmtpEvents event) {
-        mConfig.handleEvent(mStatus, event);
-    }
-
-    /**
-     * Set flags on the server for a given set of voicemails.
-     *
-     * @param voicemails The voicemails to set flags for.
-     * @param flags The flags to set on the voicemails.
-     * @return {@code true} if the operation completes successfully, {@code false} otherwise.
-     */
-    private boolean setFlags(List<Voicemail> voicemails, String... flags) {
-        if (voicemails.size() == 0) {
-            return false;
-        }
-        try {
-            mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
-            if (mFolder != null) {
-                mFolder.setFlags(convertToImapMessages(voicemails), flags, true);
-                return true;
-            }
-            return false;
-        } catch (MessagingException e) {
-            LogUtils.e(TAG, e, "Messaging exception");
-            return false;
-        } finally {
-            closeImapFolder();
-        }
-    }
-
-    /**
-     * Fetch a list of voicemails from the server.
-     *
-     * @return A list of voicemail objects containing data about voicemails stored on the server.
-     */
-    public List<Voicemail> fetchAllVoicemails() {
-        List<Voicemail> result = new ArrayList<Voicemail>();
-        Message[] messages;
-        try {
-            mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
-            if (mFolder == null) {
-                // This means we were unable to successfully open the folder.
-                return null;
-            }
-
-            // This method retrieves lightweight messages containing only the uid of the message.
-            messages = mFolder.getMessages(null);
-
-            for (Message message : messages) {
-                // Get the voicemail details (message structure).
-                MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
-                if (messageStructureWrapper != null) {
-                    result.add(getVoicemailFromMessageStructure(messageStructureWrapper));
-                }
-            }
-            return result;
-        } catch (MessagingException e) {
-            LogUtils.e(TAG, e, "Messaging Exception");
-            return null;
-        } finally {
-            closeImapFolder();
-        }
-    }
-
-    /**
-     * Extract voicemail details from the message structure. Also fetch transcription if a
-     * transcription exists.
-     */
-    private Voicemail getVoicemailFromMessageStructure(
-            MessageStructureWrapper messageStructureWrapper) throws MessagingException {
-        Message messageDetails = messageStructureWrapper.messageStructure;
-
-        TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
-        if (messageStructureWrapper.transcriptionBodyPart != null) {
-            FetchProfile fetchProfile = new FetchProfile();
-            fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
-
-            mFolder.fetch(new Message[]{messageDetails}, fetchProfile, listener);
-        }
-
-        // Found an audio attachment, this is a valid voicemail.
-        long time = messageDetails.getSentDate().getTime();
-        String number = getNumber(messageDetails.getFrom());
-        boolean isRead = Arrays.asList(messageDetails.getFlags()).contains(Flag.SEEN);
-        return Voicemail.createForInsertion(time, number)
-                .setPhoneAccount(mPhoneAccount)
-                .setSourcePackage(mContext.getPackageName())
-                .setSourceData(messageDetails.getUid())
-                .setIsRead(isRead)
-                .setTranscription(listener.getVoicemailTranscription())
-                .build();
-    }
-
-    /**
-     * The "from" field of a visual voicemail IMAP message is the number of the caller who left the
-     * message. Extract this number from the list of "from" addresses.
-     *
-     * @param fromAddresses A list of addresses that comprise the "from" line.
-     * @return The number of the voicemail sender.
-     */
-    private String getNumber(Address[] fromAddresses) {
-        if (fromAddresses != null && fromAddresses.length > 0) {
-            if (fromAddresses.length != 1) {
-                LogUtils.w(TAG, "More than one from addresses found. Using the first one.");
-            }
-            String sender = fromAddresses[0].getAddress();
-            int atPos = sender.indexOf('@');
-            if (atPos != -1) {
-                // Strip domain part of the address.
-                sender = sender.substring(0, atPos);
-            }
-            return sender;
-        }
-        return null;
-    }
-
-    /**
-     * Fetches the structure of the given message and returns a wrapper containing the message
-     * structure and the transcription structure (if applicable).
-     *
-     * @throws MessagingException if fetching the structure of the message fails
-     */
-    private MessageStructureWrapper fetchMessageStructure(Message message)
-            throws MessagingException {
-        LogUtils.d(TAG, "Fetching message structure for " + message.getUid());
-
-        MessageStructureFetchedListener listener = new MessageStructureFetchedListener();
-
-        FetchProfile fetchProfile = new FetchProfile();
-        fetchProfile.addAll(Arrays.asList(FetchProfile.Item.FLAGS, FetchProfile.Item.ENVELOPE,
-                FetchProfile.Item.STRUCTURE));
-
-        // The IMAP folder fetch method will call "messageRetrieved" on the listener when the
-        // message is successfully retrieved.
-        mFolder.fetch(new Message[]{message}, fetchProfile, listener);
-        return listener.getMessageStructure();
-    }
-
-    public boolean fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid) {
-        try {
-            mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
-            if (mFolder == null) {
-                // This means we were unable to successfully open the folder.
-                return false;
-            }
-            Message message = mFolder.getMessage(uid);
-            if (message == null) {
-                return false;
-            }
-            VoicemailPayload voicemailPayload = fetchVoicemailPayload(message);
-            callback.setVoicemailContent(voicemailPayload);
-            return true;
-        } catch (MessagingException e) {
-        } finally {
-            closeImapFolder();
-        }
-        return false;
-    }
-
-    /**
-     * Fetches the body of the given message and returns the parsed voicemail payload.
-     *
-     * @throws MessagingException if fetching the body of the message fails
-     */
-    private VoicemailPayload fetchVoicemailPayload(Message message)
-            throws MessagingException {
-        LogUtils.d(TAG, "Fetching message body for " + message.getUid());
-
-        MessageBodyFetchedListener listener = new MessageBodyFetchedListener();
-
-        FetchProfile fetchProfile = new FetchProfile();
-        fetchProfile.add(FetchProfile.Item.BODY);
-
-        mFolder.fetch(new Message[]{message}, fetchProfile, listener);
-        return listener.getVoicemailPayload();
-    }
-
-    public boolean fetchTranscription(TranscriptionFetchedCallback callback, String uid) {
-        try {
-            mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
-            if (mFolder == null) {
-                // This means we were unable to successfully open the folder.
-                return false;
-            }
-
-            Message message = mFolder.getMessage(uid);
-            if (message == null) {
-                return false;
-            }
-
-            MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
-            if (messageStructureWrapper != null) {
-                TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
-                if (messageStructureWrapper.transcriptionBodyPart != null) {
-                    FetchProfile fetchProfile = new FetchProfile();
-                    fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
-
-                    // This method is called synchronously so the transcription will be populated
-                    // in the listener once the next method is called.
-                    mFolder.fetch(new Message[]{message}, fetchProfile, listener);
-                    callback.setVoicemailTranscription(listener.getVoicemailTranscription());
-                }
-            }
-            return true;
-        } catch (MessagingException e) {
-            LogUtils.e(TAG, e, "Messaging Exception");
-            return false;
-        } finally {
-            closeImapFolder();
-        }
-    }
-
-
-    @ChangePinResult
-    public int changePin(String oldPin, String newPin)
-            throws MessagingException {
-        ImapConnection connection = mImapStore.getConnection();
-        try {
-            String command = getConfig().getProtocol()
-                    .getCommand(OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT);
-            connection.sendCommand(
-                    String.format(Locale.US, command, newPin, oldPin), true);
-            return getChangePinResultFromImapResponse(connection.readResponse());
-        } catch (IOException ioe) {
-            VvmLog.e(TAG, "changePin: ", ioe);
-            return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR;
-        } finally {
-            connection.destroyResponses();
-        }
-    }
-
-    public void changeVoicemailTuiLanguage(String languageCode)
-            throws MessagingException {
-        ImapConnection connection = mImapStore.getConnection();
-        try {
-            String command = getConfig().getProtocol()
-                    .getCommand(OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT);
-            connection.sendCommand(
-                    String.format(Locale.US, command, languageCode), true);
-        } catch (IOException ioe) {
-            LogUtils.e(TAG, ioe.toString());
-        } finally {
-            connection.destroyResponses();
-        }
-    }
-
-    public void closeNewUserTutorial() throws MessagingException {
-        ImapConnection connection = mImapStore.getConnection();
-        try {
-            String command = getConfig().getProtocol()
-                    .getCommand(OmtpConstants.IMAP_CLOSE_NUT);
-            connection.executeSimpleCommand(command, false);
-        } catch (IOException ioe) {
-            throw new MessagingException(MessagingException.SERVER_ERROR, ioe.toString());
-        } finally {
-            connection.destroyResponses();
-        }
-    }
-
-    @ChangePinResult
-    private static int getChangePinResultFromImapResponse(ImapResponse response)
-            throws MessagingException {
-        if (!response.isTagged()) {
-            throw new MessagingException(MessagingException.SERVER_ERROR,
-                    "tagged response expected");
-        }
-        if (!response.isOk()) {
-            String message = response.getStringOrEmpty(1).getString();
-            LogUtils.d(TAG, "change PIN failed: " + message);
-            if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_SHORT.equals(message)) {
-                return OmtpConstants.CHANGE_PIN_TOO_SHORT;
-            }
-            if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_LONG.equals(message)) {
-                return OmtpConstants.CHANGE_PIN_TOO_LONG;
-            }
-            if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_WEAK.equals(message)) {
-                return OmtpConstants.CHANGE_PIN_TOO_WEAK;
-            }
-            if (OmtpConstants.RESPONSE_CHANGE_PIN_MISMATCH.equals(message)) {
-                return OmtpConstants.CHANGE_PIN_MISMATCH;
-            }
-            if (OmtpConstants.RESPONSE_CHANGE_PIN_INVALID_CHARACTER.equals(message)) {
-                return OmtpConstants.CHANGE_PIN_INVALID_CHARACTER;
-            }
-            return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR;
-        }
-        LogUtils.d(TAG, "change PIN succeeded");
-        return OmtpConstants.CHANGE_PIN_SUCCESS;
-    }
-
-    public void updateQuota() {
-        try {
-            mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
-            if (mFolder == null) {
-                // This means we were unable to successfully open the folder.
-                return;
-            }
-            updateQuota(mFolder);
-        } catch (MessagingException e) {
-            LogUtils.e(TAG, e, "Messaging Exception");
-        } finally {
-            closeImapFolder();
-        }
-    }
-
-    private void updateQuota(ImapFolder folder) throws MessagingException {
-        setQuota(folder.getQuota());
-    }
-
-    private void setQuota(ImapFolder.Quota quota) {
-        if (quota == null) {
-            return;
-        }
-        if (quota.occupied == mQuotaOccupied && quota.total == mQuotaTotal) {
-            VvmLog.v(TAG, "Quota hasn't changed");
-            return;
-        }
-        mQuotaOccupied = quota.occupied;
-        mQuotaTotal = quota.total;
-        VoicemailStatus.edit(mContext, mPhoneAccount)
-                .setQuota(mQuotaOccupied, mQuotaTotal)
-                .apply();
-        mPrefs.edit()
-                .putInt(PREF_KEY_QUOTA_OCCUPIED, mQuotaOccupied)
-                .putInt(PREF_KEY_QUOTA_TOTAL, mQuotaTotal)
-                .apply();
-        VvmLog.v(TAG, "Quota changed to " + mQuotaOccupied + "/" + mQuotaTotal);
-    }
-
-    /**
-     * A wrapper to hold a message with its header details and the structure for transcriptions (so
-     * they can be fetched in the future).
-     */
-    public class MessageStructureWrapper {
-
-        public Message messageStructure;
-        public BodyPart transcriptionBodyPart;
-
-        public MessageStructureWrapper() {
-        }
-    }
-
-    /**
-     * Listener for the message structure being fetched.
-     */
-    private final class MessageStructureFetchedListener
-            implements ImapFolder.MessageRetrievalListener {
-
-        private MessageStructureWrapper mMessageStructure;
-
-        public MessageStructureFetchedListener() {
-        }
-
-        public MessageStructureWrapper getMessageStructure() {
-            return mMessageStructure;
-        }
-
-        @Override
-        public void messageRetrieved(Message message) {
-            LogUtils.d(TAG, "Fetched message structure for " + message.getUid());
-            LogUtils.d(TAG, "Message retrieved: " + message);
-            try {
-                mMessageStructure = getMessageOrNull(message);
-                if (mMessageStructure == null) {
-                    LogUtils.d(TAG, "This voicemail does not have an attachment...");
-                    return;
-                }
-            } catch (MessagingException e) {
-                LogUtils.e(TAG, e, "Messaging Exception");
-                closeImapFolder();
-            }
-        }
-
-        /**
-         * Check if this IMAP message is a valid voicemail and whether it contains a transcription.
-         *
-         * @param message The IMAP message.
-         * @return The MessageStructureWrapper object corresponding to an IMAP message and
-         * transcription.
-         */
-        private MessageStructureWrapper getMessageOrNull(Message message)
-                throws MessagingException {
-            if (!message.getMimeType().startsWith("multipart/")) {
-                LogUtils.w(TAG, "Ignored non multi-part message");
-                return null;
-            }
-
-            MessageStructureWrapper messageStructureWrapper = new MessageStructureWrapper();
-
-            Multipart multipart = (Multipart) message.getBody();
-            for (int i = 0; i < multipart.getCount(); ++i) {
-                BodyPart bodyPart = multipart.getBodyPart(i);
-                String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
-                LogUtils.d(TAG, "bodyPart mime type: " + bodyPartMimeType);
-
-                if (bodyPartMimeType.startsWith("audio/")) {
-                    messageStructureWrapper.messageStructure = message;
-                } else if (bodyPartMimeType.startsWith("text/")) {
-                    messageStructureWrapper.transcriptionBodyPart = bodyPart;
-                } else {
-                    VvmLog.v(TAG, "Unknown bodyPart MIME: " + bodyPartMimeType);
-                }
-            }
-
-            if (messageStructureWrapper.messageStructure != null) {
-                return messageStructureWrapper;
-            }
-
-            // No attachment found, this is not a voicemail.
-            return null;
-        }
-    }
-
-    /**
-     * Listener for the message body being fetched.
-     */
-    private final class MessageBodyFetchedListener implements ImapFolder.MessageRetrievalListener {
-
-        private VoicemailPayload mVoicemailPayload;
-
-        /**
-         * Returns the fetch voicemail payload.
-         */
-        public VoicemailPayload getVoicemailPayload() {
-            return mVoicemailPayload;
-        }
-
-        @Override
-        public void messageRetrieved(Message message) {
-            LogUtils.d(TAG, "Fetched message body for " + message.getUid());
-            LogUtils.d(TAG, "Message retrieved: " + message);
-            try {
-                mVoicemailPayload = getVoicemailPayloadFromMessage(message);
-            } catch (MessagingException e) {
-                LogUtils.e(TAG, "Messaging Exception:", e);
-            } catch (IOException e) {
-                LogUtils.e(TAG, "IO Exception:", e);
-            }
-        }
-
-        private VoicemailPayload getVoicemailPayloadFromMessage(Message message)
-                throws MessagingException, IOException {
-            Multipart multipart = (Multipart) message.getBody();
-            List<String> mimeTypes = new ArrayList<>();
-            for (int i = 0; i < multipart.getCount(); ++i) {
-                BodyPart bodyPart = multipart.getBodyPart(i);
-                String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
-                mimeTypes.add(bodyPartMimeType);
-                if (bodyPartMimeType.startsWith("audio/")) {
-                    byte[] bytes = getDataFromBody(bodyPart.getBody());
-                    LogUtils.d(TAG, String.format("Fetched %s bytes of data", bytes.length));
-                    return new VoicemailPayload(bodyPartMimeType, bytes);
-                }
-            }
-            LogUtils.e(TAG, "No audio attachment found on this voicemail, mimeTypes:" + mimeTypes);
-            return null;
-        }
-    }
-
-    /**
-     * Listener for the transcription being fetched.
-     */
-    private final class TranscriptionFetchedListener implements
-            ImapFolder.MessageRetrievalListener {
-
-        private String mVoicemailTranscription;
-
-        /**
-         * Returns the fetched voicemail transcription.
-         */
-        public String getVoicemailTranscription() {
-            return mVoicemailTranscription;
-        }
-
-        @Override
-        public void messageRetrieved(Message message) {
-            LogUtils.d(TAG, "Fetched transcription for " + message.getUid());
-            try {
-                mVoicemailTranscription = new String(getDataFromBody(message.getBody()));
-            } catch (MessagingException e) {
-                LogUtils.e(TAG, "Messaging Exception:", e);
-            } catch (IOException e) {
-                LogUtils.e(TAG, "IO Exception:", e);
-            }
-        }
-    }
-
-    private ImapFolder openImapFolder(String modeReadWrite) {
-        try {
-            if (mImapStore == null) {
-                return null;
-            }
-            ImapFolder folder = new ImapFolder(mImapStore, ImapConstants.INBOX);
-            folder.open(modeReadWrite);
-            return folder;
-        } catch (MessagingException e) {
-            LogUtils.e(TAG, e, "Messaging Exception");
-        }
-        return null;
-    }
-
-    private Message[] convertToImapMessages(List<Voicemail> voicemails) {
-        Message[] messages = new Message[voicemails.size()];
-        for (int i = 0; i < voicemails.size(); ++i) {
-            messages[i] = new MimeMessage();
-            messages[i].setUid(voicemails.get(i).getSourceData());
-        }
-        return messages;
-    }
-
-    private void closeImapFolder() {
-        if (mFolder != null) {
-            mFolder.close(true);
-        }
-    }
-
-    private byte[] getDataFromBody(Body body) throws IOException, MessagingException {
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
-        BufferedOutputStream bufferedOut = new BufferedOutputStream(out);
-        try {
-            body.writeTo(bufferedOut);
-            return Base64.decode(out.toByteArray(), Base64.DEFAULT);
-        } finally {
-            IOUtils.closeQuietly(bufferedOut);
-            IOUtils.closeQuietly(out);
-        }
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/Address.java b/java/com/android/voicemailomtp/mail/Address.java
deleted file mode 100644
index ed3f44c..0000000
--- a/java/com/android/voicemailomtp/mail/Address.java
+++ /dev/null
@@ -1,541 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.VisibleForTesting;
-import android.text.Html;
-import android.text.TextUtils;
-import android.text.util.Rfc822Token;
-import android.text.util.Rfc822Tokenizer;
-import com.android.voicemailomtp.mail.utils.LogUtils;
-import java.util.ArrayList;
-import java.util.regex.Pattern;
-import org.apache.james.mime4j.codec.EncoderUtil;
-import org.apache.james.mime4j.decoder.DecoderUtil;
-
-/**
- * This class represent email address.
- *
- * RFC822 email address may have following format.
- *   "name" <address> (comment)
- *   "name" <address>
- *   name <address>
- *   address
- * Name and comment part should be MIME/base64 encoded in header if necessary.
- *
- */
-public class Address implements Parcelable {
-    public static final String ADDRESS_DELIMETER = ",";
-    /**
-     *  Address part, in the form local_part@domain_part. No surrounding angle brackets.
-     */
-    private String mAddress;
-
-    /**
-     * Name part. No surrounding double quote, and no MIME/base64 encoding.
-     * This must be null if Address has no name part.
-     */
-    private String mPersonal;
-
-    /**
-     * When personal is set, it will return the first token of the personal
-     * string. Otherwise, it will return the e-mail address up to the '@' sign.
-     */
-    private String mSimplifiedName;
-
-    // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$'
-    private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$");
-    // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$'
-    private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$");
-    // Regex that matches escaped character '\\([\\"])'
-    private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])");
-
-    // TODO: LOCAL_PART and DOMAIN_PART_PART are too permissive and can be improved.
-    // TODO: Fix this to better constrain comments.
-    /** Regex for the local part of an email address. */
-    private static final String LOCAL_PART = "[^@]+";
-    /** Regex for each part of the domain part, i.e. the thing between the dots. */
-    private static final String DOMAIN_PART_PART = "[[\\w][\\d]\\-\\(\\)\\[\\]]+";
-    /** Regex for the domain part, which is two or more {@link #DOMAIN_PART_PART} separated by . */
-    private static final String DOMAIN_PART =
-            "(" + DOMAIN_PART_PART + "\\.)+" + DOMAIN_PART_PART;
-
-    /** Pattern to check if an email address is valid. */
-    private static final Pattern EMAIL_ADDRESS =
-            Pattern.compile("\\A" + LOCAL_PART + "@" + DOMAIN_PART + "\\z");
-
-    private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
-
-    // delimiters are chars that do not appear in an email address, used by fromHeader
-    private static final char LIST_DELIMITER_EMAIL = '\1';
-    private static final char LIST_DELIMITER_PERSONAL = '\2';
-
-    private static final String LOG_TAG = "Email Address";
-
-    @VisibleForTesting
-    public Address(String address) {
-        setAddress(address);
-    }
-
-    public Address(String address, String personal) {
-        setPersonal(personal);
-        setAddress(address);
-    }
-
-    /**
-     * Returns a simplified string for this e-mail address.
-     * When a name is known, it will return the first token of that name. Otherwise, it will
-     * return the e-mail address up to the '@' sign.
-     */
-    public String getSimplifiedName() {
-        if (mSimplifiedName == null) {
-            if (TextUtils.isEmpty(mPersonal) && !TextUtils.isEmpty(mAddress)) {
-                int atSign = mAddress.indexOf('@');
-                mSimplifiedName = (atSign != -1) ? mAddress.substring(0, atSign) : "";
-            } else if (!TextUtils.isEmpty(mPersonal)) {
-
-                // TODO: use Contacts' NameSplitter for more reliable first-name extraction
-
-                int end = mPersonal.indexOf(' ');
-                while (end > 0 && mPersonal.charAt(end - 1) == ',') {
-                    end--;
-                }
-                mSimplifiedName = (end < 1) ? mPersonal : mPersonal.substring(0, end);
-
-            } else {
-                LogUtils.w(LOG_TAG, "Unable to get a simplified name");
-                mSimplifiedName = "";
-            }
-        }
-        return mSimplifiedName;
-    }
-
-    public static synchronized Address getEmailAddress(String rawAddress) {
-        if (TextUtils.isEmpty(rawAddress)) {
-            return null;
-        }
-        String name, address;
-        final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(rawAddress);
-        if (tokens.length > 0) {
-            final String tokenizedName = tokens[0].getName();
-            name = tokenizedName != null ? Html.fromHtml(tokenizedName.trim()).toString()
-                    : "";
-            address = Html.fromHtml(tokens[0].getAddress()).toString();
-        } else {
-            name = "";
-            address = rawAddress == null ?
-                    "" : Html.fromHtml(rawAddress).toString();
-        }
-        return new Address(address, name);
-    }
-
-    public String getAddress() {
-        return mAddress;
-    }
-
-    public void setAddress(String address) {
-        mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");
-    }
-
-    /**
-     * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
-     *
-     * @return Name part of email address. Returns null if it is omitted.
-     */
-    public String getPersonal() {
-        return mPersonal;
-    }
-
-    /**
-     * Set personal part from UTF-16 string. Optional surrounding double quote will be removed.
-     * It will be also unquoted and MIME/base64 decoded.
-     *
-     * @param personal name part of email address as UTF-16 string. Null is acceptable.
-     */
-    public void setPersonal(String personal) {
-        mPersonal = decodeAddressPersonal(personal);
-    }
-
-    /**
-     * Decodes name from UTF-16 string. Optional surrounding double quote will be removed.
-     * It will be also unquoted and MIME/base64 decoded.
-     *
-     * @param personal name part of email address as UTF-16 string. Null is acceptable.
-     */
-    public static String decodeAddressPersonal(String personal) {
-        if (personal != null) {
-            personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
-            personal = UNQUOTE.matcher(personal).replaceAll("$1");
-            personal = DecoderUtil.decodeEncodedWords(personal);
-            if (personal.length() == 0) {
-                personal = null;
-            }
-        }
-        return personal;
-    }
-
-    /**
-     * This method is used to check that all the addresses that the user
-     * entered in a list (e.g. To:) are valid, so that none is dropped.
-     */
-    @VisibleForTesting
-    public static boolean isAllValid(String addressList) {
-        // This code mimics the parse() method below.
-        // I don't know how to better avoid the code-duplication.
-        if (addressList != null && addressList.length() > 0) {
-            Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
-            for (int i = 0, length = tokens.length; i < length; ++i) {
-                Rfc822Token token = tokens[i];
-                String address = token.getAddress();
-                if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
-                    return false;
-                }
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Parse a comma-delimited list of addresses in RFC822 format and return an
-     * array of Address objects.
-     *
-     * @param addressList Address list in comma-delimited string.
-     * @return An array of 0 or more Addresses.
-     */
-    public static Address[] parse(String addressList) {
-        if (addressList == null || addressList.length() == 0) {
-            return EMPTY_ADDRESS_ARRAY;
-        }
-        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
-        ArrayList<Address> addresses = new ArrayList<Address>();
-        for (int i = 0, length = tokens.length; i < length; ++i) {
-            Rfc822Token token = tokens[i];
-            String address = token.getAddress();
-            if (!TextUtils.isEmpty(address)) {
-                if (isValidAddress(address)) {
-                    String name = token.getName();
-                    if (TextUtils.isEmpty(name)) {
-                        name = null;
-                    }
-                    addresses.add(new Address(address, name));
-                }
-            }
-        }
-        return addresses.toArray(new Address[addresses.size()]);
-    }
-
-    /**
-     * Checks whether a string email address is valid.
-     * E.g. name@domain.com is valid.
-     */
-    @VisibleForTesting
-    static boolean isValidAddress(final String address) {
-        return EMAIL_ADDRESS.matcher(address).find();
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (o instanceof Address) {
-            // It seems that the spec says that the "user" part is case-sensitive,
-            // while the domain part in case-insesitive.
-            // So foo@yahoo.com and Foo@yahoo.com are different.
-            // This may seem non-intuitive from the user POV, so we
-            // may re-consider it if it creates UI trouble.
-            // A problem case is "replyAll" sending to both
-            // a@b.c and to A@b.c, which turn out to be the same on the server.
-            // Leave unchanged for now (i.e. case-sensitive).
-            return getAddress().equals(((Address) o).getAddress());
-        }
-        return super.equals(o);
-    }
-
-    @Override
-    public int hashCode() {
-        return getAddress().hashCode();
-    }
-
-    /**
-     * Get human readable address string.
-     * Do not use this for email header.
-     *
-     * @return Human readable address string.  Not quoted and not encoded.
-     */
-    @Override
-    public String toString() {
-        if (mPersonal != null && !mPersonal.equals(mAddress)) {
-            if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
-                return ensureQuotedString(mPersonal) + " <" + mAddress + ">";
-            } else {
-                return mPersonal + " <" + mAddress + ">";
-            }
-        } else {
-            return mAddress;
-        }
-    }
-
-    /**
-     * Ensures that the given string starts and ends with the double quote character. The string is
-     * not modified in any way except to add the double quote character to start and end if it's not
-     * already there.
-     *
-     * sample -> "sample"
-     * "sample" -> "sample"
-     * ""sample"" -> "sample"
-     * "sample"" -> "sample"
-     * sa"mp"le -> "sa"mp"le"
-     * "sa"mp"le" -> "sa"mp"le"
-     * (empty string) -> ""
-     * " -> ""
-     */
-    private static String ensureQuotedString(String s) {
-        if (s == null) {
-            return null;
-        }
-        if (!s.matches("^\".*\"$")) {
-            return "\"" + s + "\"";
-        } else {
-            return s;
-        }
-    }
-
-    /**
-     * Get human readable comma-delimited address string.
-     *
-     * @param addresses Address array
-     * @return Human readable comma-delimited address string.
-     */
-    @VisibleForTesting
-    public static String toString(Address[] addresses) {
-        return toString(addresses, ADDRESS_DELIMETER);
-    }
-
-    /**
-     * Get human readable address strings joined with the specified separator.
-     *
-     * @param addresses Address array
-     * @param separator Separator
-     * @return Human readable comma-delimited address string.
-     */
-    public static String toString(Address[] addresses, String separator) {
-        if (addresses == null || addresses.length == 0) {
-            return null;
-        }
-        if (addresses.length == 1) {
-            return addresses[0].toString();
-        }
-        StringBuilder sb = new StringBuilder(addresses[0].toString());
-        for (int i = 1; i < addresses.length; i++) {
-            sb.append(separator);
-            // TODO: investigate why this .trim() is needed.
-            sb.append(addresses[i].toString().trim());
-        }
-        return sb.toString();
-    }
-
-    /**
-     * Get RFC822/MIME compatible address string.
-     *
-     * @return RFC822/MIME compatible address string.
-     * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary.
-     */
-    public String toHeader() {
-        if (mPersonal != null) {
-            return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
-        } else {
-            return mAddress;
-        }
-    }
-
-    /**
-     * Get RFC822/MIME compatible comma-delimited address string.
-     *
-     * @param addresses Address array
-     * @return RFC822/MIME compatible comma-delimited address string.
-     * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary.
-     */
-    public static String toHeader(Address[] addresses) {
-        if (addresses == null || addresses.length == 0) {
-            return null;
-        }
-        if (addresses.length == 1) {
-            return addresses[0].toHeader();
-        }
-        StringBuilder sb = new StringBuilder(addresses[0].toHeader());
-        for (int i = 1; i < addresses.length; i++) {
-            // We need space character to be able to fold line.
-            sb.append(", ");
-            sb.append(addresses[i].toHeader());
-        }
-        return sb.toString();
-    }
-
-    /**
-     * Get Human friendly address string.
-     *
-     * @return the personal part of this Address, or the address part if the
-     * personal part is not available
-     */
-    @VisibleForTesting
-    public String toFriendly() {
-        if (mPersonal != null && mPersonal.length() > 0) {
-            return mPersonal;
-        } else {
-            return mAddress;
-        }
-    }
-
-    /**
-     * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
-     * details on the per-address conversion).
-     *
-     * @param addresses Array of Address[] values
-     * @return A comma-delimited string listing all of the addresses supplied.  Null if source
-     * was null or empty.
-     */
-    @VisibleForTesting
-    public static String toFriendly(Address[] addresses) {
-        if (addresses == null || addresses.length == 0) {
-            return null;
-        }
-        if (addresses.length == 1) {
-            return addresses[0].toFriendly();
-        }
-        StringBuilder sb = new StringBuilder(addresses[0].toFriendly());
-        for (int i = 1; i < addresses.length; i++) {
-            sb.append(", ");
-            sb.append(addresses[i].toFriendly());
-        }
-        return sb.toString();
-    }
-
-    /**
-     * Returns exactly the same result as Address.toString(Address.fromHeader(addressList)).
-     */
-    @VisibleForTesting
-    public static String fromHeaderToString(String addressList) {
-        return toString(fromHeader(addressList));
-    }
-
-    /**
-     * Returns exactly the same result as Address.toHeader(Address.parse(addressList)).
-     */
-    @VisibleForTesting
-    public static String parseToHeader(String addressList) {
-        return Address.toHeader(Address.parse(addressList));
-    }
-
-    /**
-     * Returns null if the addressList has 0 addresses, otherwise returns the first address.
-     * The same as Address.fromHeader(addressList)[0] for non-empty list.
-     * This is an utility method that offers some performance optimization opportunities.
-     */
-    @VisibleForTesting
-    public static Address firstAddress(String addressList) {
-        Address[] array = fromHeader(addressList);
-        return array.length > 0 ? array[0] : null;
-    }
-
-    /**
-     * This method exists to convert an address list formatted in a deprecated legacy format to the
-     * standard RFC822 header format. {@link #fromHeader(String)} is capable of reading the legacy
-     * format and the RFC822 format. {@link #toHeader()} always produces the RFC822 format.
-     *
-     * This implementation is brute-force, and could be replaced with a more efficient version
-     * if desired.
-     */
-    public static String reformatToHeader(String addressList) {
-        return toHeader(fromHeader(addressList));
-    }
-
-    /**
-     * @param addressList a CSV of RFC822 addresses or the deprecated legacy string format
-     * @return array of addresses parsed from <code>addressList</code>
-     */
-    @VisibleForTesting
-    public static Address[] fromHeader(String addressList) {
-        if (addressList == null || addressList.length() == 0) {
-            return EMPTY_ADDRESS_ARRAY;
-        }
-        // IF we're CSV, just parse
-        if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1) &&
-                (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) {
-            return Address.parse(addressList);
-        }
-        // Otherwise, do backward-compatible unpack
-        ArrayList<Address> addresses = new ArrayList<Address>();
-        int length = addressList.length();
-        int pairStartIndex = 0;
-        int pairEndIndex;
-
-        /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
-           is used, not for every email address; i.e. not for every iteration of the while().
-           This reduces the theoretical complexity from quadratic to linear,
-           and provides some speed-up in practice by removing redundant scans of the string.
-        */
-        int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
-
-        while (pairStartIndex < length) {
-            pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
-            if (pairEndIndex == -1) {
-                pairEndIndex = length;
-            }
-            Address address;
-            if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
-                // in this case the DELIMITER_PERSONAL is in a future pair,
-                // so don't use personal, and don't update addressEndIndex
-                address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
-            } else {
-                address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
-                        addressList.substring(addressEndIndex + 1, pairEndIndex));
-                // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
-                addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
-            }
-            addresses.add(address);
-            pairStartIndex = pairEndIndex + 1;
-        }
-        return addresses.toArray(new Address[addresses.size()]);
-    }
-
-    public static final Creator<Address> CREATOR = new Creator<Address>() {
-        @Override
-        public Address createFromParcel(Parcel parcel) {
-            return new Address(parcel);
-        }
-
-        @Override
-        public Address[] newArray(int size) {
-            return new Address[size];
-        }
-    };
-
-    public Address(Parcel in) {
-        setPersonal(in.readString());
-        setAddress(in.readString());
-    }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(Parcel out, int flags) {
-        out.writeString(mPersonal);
-        out.writeString(mAddress);
-    }
-}
diff --git a/java/com/android/voicemailomtp/mail/AuthenticationFailedException.java b/java/com/android/voicemailomtp/mail/AuthenticationFailedException.java
deleted file mode 100644
index 995d5d3..0000000
--- a/java/com/android/voicemailomtp/mail/AuthenticationFailedException.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail;
-
-public class AuthenticationFailedException extends MessagingException {
-    public static final long serialVersionUID = -1;
-
-    public AuthenticationFailedException(String message) {
-        super(MessagingException.AUTHENTICATION_FAILED, message);
-    }
-
-    public AuthenticationFailedException(int exceptionType, String message) {
-        super(exceptionType, message);
-    }
-
-    public AuthenticationFailedException(String message, Throwable throwable) {
-        super(MessagingException.AUTHENTICATION_FAILED, message, throwable);
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/Base64Body.java b/java/com/android/voicemailomtp/mail/Base64Body.java
deleted file mode 100644
index 6e1deff..0000000
--- a/java/com/android/voicemailomtp/mail/Base64Body.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import android.util.Base64;
-import android.util.Base64OutputStream;
-
-import org.apache.commons.io.IOUtils;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-public class Base64Body implements Body {
-    private final InputStream mSource;
-    // Because we consume the input stream, we can only write out once
-    private boolean mAlreadyWritten;
-
-    public Base64Body(InputStream source) {
-        mSource = source;
-    }
-
-    @Override
-    public InputStream getInputStream() throws MessagingException {
-        return mSource;
-    }
-
-    /**
-     * This method consumes the input stream, so can only be called once
-     * @param out Stream to write to
-     * @throws IllegalStateException If called more than once
-     * @throws IOException
-     * @throws MessagingException
-     */
-    @Override
-    public void writeTo(OutputStream out)
-            throws IllegalStateException, IOException, MessagingException {
-        if (mAlreadyWritten) {
-            throw new IllegalStateException("Base64Body can only be written once");
-        }
-        mAlreadyWritten = true;
-        try {
-            final Base64OutputStream b64out = new Base64OutputStream(out, Base64.DEFAULT);
-            IOUtils.copyLarge(mSource, b64out);
-        } finally {
-            mSource.close();
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/mail/FetchProfile.java b/java/com/android/voicemailomtp/mail/FetchProfile.java
deleted file mode 100644
index d050692..0000000
--- a/java/com/android/voicemailomtp/mail/FetchProfile.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import java.util.ArrayList;
-
-/**
- * <pre>
- * A FetchProfile is a list of items that should be downloaded in bulk for a set of messages.
- * FetchProfile can contain the following objects:
- *      FetchProfile.Item:      Described below.
- *      Message:                Indicates that the body of the entire message should be fetched.
- *                              Synonymous with FetchProfile.Item.BODY.
- *      Part:                   Indicates that the given Part should be fetched. The provider
- *                              is expected have previously created the given BodyPart and stored
- *                              any information it needs to download the content.
- * </pre>
- */
-public class FetchProfile extends ArrayList<Fetchable> {
-    /**
-     * Default items available for pre-fetching. It should be expected that any
-     * item fetched by using these items could potentially include all of the
-     * previous items.
-     */
-    public enum Item implements Fetchable {
-        /**
-         * Download the flags of the message.
-         */
-        FLAGS,
-
-        /**
-         * Download the envelope of the message. This should include at minimum
-         * the size and the following headers: date, subject, from, content-type, to, cc
-         */
-        ENVELOPE,
-
-        /**
-         * Download the structure of the message. This maps directly to IMAP's BODYSTRUCTURE
-         * and may map to other providers.
-         * The provider should, if possible, fill in a properly formatted MIME structure in
-         * the message without actually downloading any message data. If the provider is not
-         * capable of this operation it should specifically set the body of the message to null
-         * so that upper levels can detect that a full body download is needed.
-         */
-        STRUCTURE,
-
-        /**
-         * A sane portion of the entire message, cut off at a provider determined limit.
-         * This should generally be around 50kB.
-         */
-        BODY_SANE,
-
-        /**
-         * The entire message.
-         */
-        BODY,
-    }
-
-    /**
-     * @return the first {@link Part} in this collection, or null if it doesn't contain
-     * {@link Part}.
-     */
-    public Part getFirstPart() {
-        for (Fetchable o : this) {
-            if (o instanceof Part) {
-                return (Part) o;
-            }
-        }
-        return null;
-    }
-}
diff --git a/java/com/android/voicemailomtp/mail/FixedLengthInputStream.java b/java/com/android/voicemailomtp/mail/FixedLengthInputStream.java
deleted file mode 100644
index 65655ef..0000000
--- a/java/com/android/voicemailomtp/mail/FixedLengthInputStream.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * A filtering InputStream that stops allowing reads after the given length has been read. This
- * is used to allow a client to read directly from an underlying protocol stream without reading
- * past where the protocol handler intended the client to read.
- */
-public class FixedLengthInputStream extends InputStream {
-    private final InputStream mIn;
-    private final int mLength;
-    private int mCount;
-
-    public FixedLengthInputStream(InputStream in, int length) {
-        this.mIn = in;
-        this.mLength = length;
-    }
-
-    @Override
-    public int available() throws IOException {
-        return mLength - mCount;
-    }
-
-    @Override
-    public int read() throws IOException {
-        if (mCount < mLength) {
-            mCount++;
-            return mIn.read();
-        } else {
-            return -1;
-        }
-    }
-
-    @Override
-    public int read(byte[] b, int offset, int length) throws IOException {
-        if (mCount < mLength) {
-            int d = mIn.read(b, offset, Math.min(mLength - mCount, length));
-            if (d == -1) {
-                return -1;
-            } else {
-                mCount += d;
-                return d;
-            }
-        } else {
-            return -1;
-        }
-    }
-
-    @Override
-    public int read(byte[] b) throws IOException {
-        return read(b, 0, b.length);
-    }
-
-    public int getLength() {
-        return mLength;
-    }
-
-    @Override
-    public String toString() {
-        return String.format("FixedLengthInputStream(in=%s, length=%d)", mIn.toString(), mLength);
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/Flag.java b/java/com/android/voicemailomtp/mail/Flag.java
deleted file mode 100644
index a9f9270..0000000
--- a/java/com/android/voicemailomtp/mail/Flag.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-/**
- * Flags that can be applied to Messages.
- */
-public class Flag {
-    // If adding new flags: ALL FLAGS MUST BE UPPER CASE.
-    public static final String DELETED = "deleted";
-    public static final String SEEN = "seen";
-    public static final String ANSWERED = "answered";
-    public static final String FLAGGED = "flagged";
-    public static final String DRAFT = "draft";
-    public static final String RECENT = "recent";
-}
diff --git a/java/com/android/voicemailomtp/mail/MailTransport.java b/java/com/android/voicemailomtp/mail/MailTransport.java
deleted file mode 100644
index 3bf851f..0000000
--- a/java/com/android/voicemailomtp/mail/MailTransport.java
+++ /dev/null
@@ -1,344 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import android.content.Context;
-import android.net.Network;
-import android.support.annotation.VisibleForTesting;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.imap.ImapHelper;
-import com.android.voicemailomtp.mail.store.ImapStore;
-import com.android.voicemailomtp.mail.utils.LogUtils;
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.Socket;
-import java.util.ArrayList;
-import java.util.List;
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLException;
-import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSession;
-import javax.net.ssl.SSLSocket;
-
-/**
- * Make connection and perform operations on mail server by reading and writing lines.
- */
-public class MailTransport {
-    private static final String TAG = "MailTransport";
-
-    // TODO protected eventually
-    /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000;
-    /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000;
-
-    private static final HostnameVerifier HOSTNAME_VERIFIER =
-            HttpsURLConnection.getDefaultHostnameVerifier();
-
-    private final Context mContext;
-    private final ImapHelper mImapHelper;
-    private final Network mNetwork;
-    private final String mHost;
-    private final int mPort;
-    private Socket mSocket;
-    private BufferedInputStream mIn;
-    private BufferedOutputStream mOut;
-    private final int mFlags;
-    private SocketCreator mSocketCreator;
-    private InetSocketAddress mAddress;
-
-    public MailTransport(Context context, ImapHelper imapHelper, Network network, String address,
-            int port, int flags) {
-        mContext = context;
-        mImapHelper = imapHelper;
-        mNetwork = network;
-        mHost = address;
-        mPort = port;
-        mFlags = flags;
-    }
-
-    /**
-     * Returns a new transport, using the current transport as a model. The new transport is
-     * configured identically, but not opened or connected in any way.
-     */
-    @Override
-    public MailTransport clone() {
-        return new MailTransport(mContext, mImapHelper, mNetwork, mHost, mPort, mFlags);
-    }
-
-    public boolean canTrySslSecurity() {
-        return (mFlags & ImapStore.FLAG_SSL) != 0;
-    }
-
-    public boolean canTrustAllCertificates() {
-        return (mFlags & ImapStore.FLAG_TRUST_ALL) != 0;
-    }
-
-    /**
-     * Attempts to open a connection using the Uri supplied for connection parameters.  Will attempt
-     * an SSL connection if indicated.
-     */
-    public void open() throws MessagingException {
-        LogUtils.d(TAG, "*** IMAP open " + mHost + ":" + String.valueOf(mPort));
-
-        List<InetSocketAddress> socketAddresses = new ArrayList<InetSocketAddress>();
-
-        if (mNetwork == null) {
-            socketAddresses.add(new InetSocketAddress(mHost, mPort));
-        } else {
-            try {
-                InetAddress[] inetAddresses = mNetwork.getAllByName(mHost);
-                if (inetAddresses.length == 0) {
-                    throw new MessagingException(MessagingException.IOERROR,
-                            "Host name " + mHost + "cannot be resolved on designated network");
-                }
-                for (int i = 0; i < inetAddresses.length; i++) {
-                    socketAddresses.add(new InetSocketAddress(inetAddresses[i], mPort));
-                }
-            } catch (IOException ioe) {
-                LogUtils.d(TAG, ioe.toString());
-                mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_RESOLVE_HOST_ON_NETWORK);
-                throw new MessagingException(MessagingException.IOERROR, ioe.toString());
-            }
-        }
-
-        boolean success = false;
-        while (socketAddresses.size() > 0) {
-            mSocket = createSocket();
-            try {
-                mAddress = socketAddresses.remove(0);
-                mSocket.connect(mAddress, SOCKET_CONNECT_TIMEOUT);
-
-                if (canTrySslSecurity()) {
-                    /*
-                    SSLSocket cannot be created with a connection timeout, so instead of doing a
-                    direct SSL connection, we connect with a normal connection and upgrade it into
-                    SSL
-                     */
-                    reopenTls();
-                } else {
-                    mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
-                    mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
-                    mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
-                }
-                success = true;
-                return;
-            } catch (IOException ioe) {
-                LogUtils.d(TAG, ioe.toString());
-                if (socketAddresses.size() == 0) {
-                    // Only throw an error when there are no more sockets to try.
-                    mImapHelper.handleEvent(OmtpEvents.DATA_ALL_SOCKET_CONNECTION_FAILED);
-                    throw new MessagingException(MessagingException.IOERROR, ioe.toString());
-                }
-            } finally {
-                if (!success) {
-                    try {
-                        mSocket.close();
-                        mSocket = null;
-                    } catch (IOException ioe) {
-                        throw new MessagingException(MessagingException.IOERROR, ioe.toString());
-                    }
-
-                }
-            }
-        }
-    }
-
-    // For testing. We need something that can replace the behavior of "new Socket()"
-    @VisibleForTesting
-    interface SocketCreator {
-
-        Socket createSocket() throws MessagingException;
-    }
-
-    @VisibleForTesting
-    void setSocketCreator(SocketCreator creator) {
-        mSocketCreator = creator;
-    }
-
-    protected Socket createSocket() throws MessagingException {
-        if (mSocketCreator != null) {
-            return mSocketCreator.createSocket();
-        }
-
-        if (mNetwork == null) {
-            LogUtils.v(TAG, "createSocket: network not specified");
-            return new Socket();
-        }
-
-        try {
-            LogUtils.v(TAG, "createSocket: network specified");
-            return mNetwork.getSocketFactory().createSocket();
-        } catch (IOException ioe) {
-            LogUtils.d(TAG, ioe.toString());
-            throw new MessagingException(MessagingException.IOERROR, ioe.toString());
-        }
-    }
-
-    /**
-     * Attempts to reopen a normal connection into a TLS connection.
-     */
-    public void reopenTls() throws MessagingException {
-        try {
-            LogUtils.d(TAG, "open: converting to TLS socket");
-            mSocket = HttpsURLConnection.getDefaultSSLSocketFactory()
-                    .createSocket(mSocket, mAddress.getHostName(), mAddress.getPort(), true);
-            // After the socket connects to an SSL server, confirm that the hostname is as
-            // expected
-            if (!canTrustAllCertificates()) {
-                verifyHostname(mSocket, mHost);
-            }
-            mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
-            mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
-            mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
-
-        } catch (SSLException e) {
-            LogUtils.d(TAG, e.toString());
-            throw new CertificateValidationException(e.getMessage(), e);
-        } catch (IOException ioe) {
-            LogUtils.d(TAG, ioe.toString());
-            throw new MessagingException(MessagingException.IOERROR, ioe.toString());
-        }
-    }
-
-    /**
-     * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
-     * service but is not in the public API.
-     *
-     * Verify the hostname of the certificate used by the other end of a
-     * connected socket. It is harmless to call this method redundantly if the hostname has already
-     * been verified.
-     *
-     * <p>Wildcard certificates are allowed to verify any matching hostname,
-     * so "foo.bar.example.com" is verified if the peer has a certificate
-     * for "*.example.com".
-     *
-     * @param socket An SSL socket which has been connected to a server
-     * @param hostname The expected hostname of the remote server
-     * @throws IOException if something goes wrong handshaking with the server
-     * @throws SSLPeerUnverifiedException if the server cannot prove its identity
-      */
-    private void verifyHostname(Socket socket, String hostname) throws IOException {
-        // The code at the start of OpenSSLSocketImpl.startHandshake()
-        // ensures that the call is idempotent, so we can safely call it.
-        SSLSocket ssl = (SSLSocket) socket;
-        ssl.startHandshake();
-
-        SSLSession session = ssl.getSession();
-        if (session == null) {
-            mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_ESTABLISH_SSL_SESSION);
-            throw new SSLException("Cannot verify SSL socket without session");
-        }
-        // TODO: Instead of reporting the name of the server we think we're connecting to,
-        // we should be reporting the bad name in the certificate.  Unfortunately this is buried
-        // in the verifier code and is not available in the verifier API, and extracting the
-        // CN & alts is beyond the scope of this patch.
-        if (!HOSTNAME_VERIFIER.verify(hostname, session)) {
-            mImapHelper.handleEvent(OmtpEvents.DATA_SSL_INVALID_HOST_NAME);
-            throw new SSLPeerUnverifiedException("Certificate hostname not useable for server: "
-                    + session.getPeerPrincipal());
-        }
-    }
-
-    public boolean isOpen() {
-        return (mIn != null && mOut != null &&
-                mSocket != null && mSocket.isConnected() && !mSocket.isClosed());
-    }
-
-    /**
-     * Close the connection.  MUST NOT return any exceptions - must be "best effort" and safe.
-     */
-    public void close() {
-        try {
-            mIn.close();
-        } catch (Exception e) {
-            // May fail if the connection is already closed.
-        }
-        try {
-            mOut.close();
-        } catch (Exception e) {
-            // May fail if the connection is already closed.
-        }
-        try {
-            mSocket.close();
-        } catch (Exception e) {
-            // May fail if the connection is already closed.
-        }
-        mIn = null;
-        mOut = null;
-        mSocket = null;
-    }
-
-    public String getHost() {
-        return mHost;
-    }
-
-    public InputStream getInputStream() {
-        return mIn;
-    }
-
-    public OutputStream getOutputStream() {
-        return mOut;
-    }
-
-    /**
-     * Writes a single line to the server using \r\n termination.
-     */
-    public void writeLine(String s, String sensitiveReplacement) throws IOException {
-        if (sensitiveReplacement != null) {
-            LogUtils.d(TAG, ">>> " + sensitiveReplacement);
-        } else {
-            LogUtils.d(TAG, ">>> " + s);
-        }
-
-        OutputStream out = getOutputStream();
-        out.write(s.getBytes());
-        out.write('\r');
-        out.write('\n');
-        out.flush();
-    }
-
-    /**
-     * Reads a single line from the server, using either \r\n or \n as the delimiter.  The
-     * delimiter char(s) are not included in the result.
-     */
-    public String readLine(boolean loggable) throws IOException {
-        StringBuffer sb = new StringBuffer();
-        InputStream in = getInputStream();
-        int d;
-        while ((d = in.read()) != -1) {
-            if (((char)d) == '\r') {
-                continue;
-            } else if (((char)d) == '\n') {
-                break;
-            } else {
-                sb.append((char)d);
-            }
-        }
-        if (d == -1) {
-            LogUtils.d(TAG, "End of stream reached while trying to read line.");
-        }
-        String ret = sb.toString();
-        if (loggable) {
-            LogUtils.d(TAG, "<<< " + ret);
-        }
-        return ret;
-    }
-}
diff --git a/java/com/android/voicemailomtp/mail/MeetingInfo.java b/java/com/android/voicemailomtp/mail/MeetingInfo.java
deleted file mode 100644
index 0505bbf..0000000
--- a/java/com/android/voicemailomtp/mail/MeetingInfo.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-public class MeetingInfo {
-    // Predefined tags; others can be added
-    public static final String MEETING_DTSTAMP = "DTSTAMP";
-    public static final String MEETING_UID = "UID";
-    public static final String MEETING_ORGANIZER_EMAIL = "ORGMAIL";
-    public static final String MEETING_DTSTART = "DTSTART";
-    public static final String MEETING_DTEND = "DTEND";
-    public static final String MEETING_TITLE = "TITLE";
-    public static final String MEETING_LOCATION = "LOC";
-    public static final String MEETING_RESPONSE_REQUESTED = "RESPONSE";
-    public static final String MEETING_ALL_DAY = "ALLDAY";
-}
diff --git a/java/com/android/voicemailomtp/mail/Message.java b/java/com/android/voicemailomtp/mail/Message.java
deleted file mode 100644
index 4155569..0000000
--- a/java/com/android/voicemailomtp/mail/Message.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import android.support.annotation.VisibleForTesting;
-import java.util.Date;
-import java.util.HashSet;
-
-public abstract class Message implements Part, Body {
-    public static final Message[] EMPTY_ARRAY = new Message[0];
-
-    public static final String RECIPIENT_TYPE_TO = "to";
-    public static final String RECIPIENT_TYPE_CC = "cc";
-    public static final String RECIPIENT_TYPE_BCC = "bcc";
-    public enum RecipientType {
-        TO, CC, BCC,
-    }
-
-    protected String mUid;
-
-    private HashSet<String> mFlags = null;
-
-    protected Date mInternalDate;
-
-    public String getUid() {
-        return mUid;
-    }
-
-    public void setUid(String uid) {
-        this.mUid = uid;
-    }
-
-    public abstract String getSubject() throws MessagingException;
-
-    public abstract void setSubject(String subject) throws MessagingException;
-
-    public Date getInternalDate() {
-        return mInternalDate;
-    }
-
-    public void setInternalDate(Date internalDate) {
-        this.mInternalDate = internalDate;
-    }
-
-    public abstract Date getReceivedDate() throws MessagingException;
-
-    public abstract Date getSentDate() throws MessagingException;
-
-    public abstract void setSentDate(Date sentDate) throws MessagingException;
-
-    public abstract Address[] getRecipients(String type) throws MessagingException;
-
-    public abstract void setRecipients(String type, Address[] addresses)
-            throws MessagingException;
-
-    public void setRecipient(String type, Address address) throws MessagingException {
-        setRecipients(type, new Address[] {
-            address
-        });
-    }
-
-    public abstract Address[] getFrom() throws MessagingException;
-
-    public abstract void setFrom(Address from) throws MessagingException;
-
-    public abstract Address[] getReplyTo() throws MessagingException;
-
-    public abstract void setReplyTo(Address[] from) throws MessagingException;
-
-    // Always use these instead of getHeader("Message-ID") or setHeader("Message-ID");
-    public abstract void setMessageId(String messageId) throws MessagingException;
-    public abstract String getMessageId() throws MessagingException;
-
-    @Override
-    public boolean isMimeType(String mimeType) throws MessagingException {
-        return getContentType().startsWith(mimeType);
-    }
-
-    private HashSet<String> getFlagSet() {
-        if (mFlags == null) {
-            mFlags = new HashSet<String>();
-        }
-        return mFlags;
-    }
-
-    /*
-     * TODO Refactor Flags at some point to be able to store user defined flags.
-     */
-    public String[] getFlags() {
-        return getFlagSet().toArray(new String[] {});
-    }
-
-    /**
-     * Set/clear a flag directly, without involving overrides of {@link #setFlag} in subclasses.
-     * Only used for testing.
-     */
-    @VisibleForTesting
-    private final void setFlagDirectlyForTest(String flag, boolean set) throws MessagingException {
-        if (set) {
-            getFlagSet().add(flag);
-        } else {
-            getFlagSet().remove(flag);
-        }
-    }
-
-    public void setFlag(String flag, boolean set) throws MessagingException {
-        setFlagDirectlyForTest(flag, set);
-    }
-
-    /**
-     * This method calls setFlag(String, boolean)
-     * @param flags
-     * @param set
-     */
-    public void setFlags(String[] flags, boolean set) throws MessagingException {
-        for (String flag : flags) {
-            setFlag(flag, set);
-        }
-    }
-
-    public boolean isSet(String flag) {
-        return getFlagSet().contains(flag);
-    }
-
-    public abstract void saveChanges() throws MessagingException;
-
-    @Override
-    public String toString() {
-        return getClass().getSimpleName() + ':' + mUid;
-    }
-}
diff --git a/java/com/android/voicemailomtp/mail/MessagingException.java b/java/com/android/voicemailomtp/mail/MessagingException.java
deleted file mode 100644
index 2855052..0000000
--- a/java/com/android/voicemailomtp/mail/MessagingException.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail;
-
-/**
- * This exception is used for most types of failures that occur during server interactions.
- *
- * Data passed through this exception should be considered non-localized.  Any strings should
- * either be internal-only (for debugging) or server-generated.
- *
- * TO DO: Does it make sense to further collapse AuthenticationFailedException and
- * CertificateValidationException and any others into this?
- */
-public class MessagingException extends Exception {
-    public static final long serialVersionUID = -1;
-
-    public static final int NO_ERROR = -1;
-    /** Any exception that does not specify a specific issue */
-    public static final int UNSPECIFIED_EXCEPTION = 0;
-    /** Connection or IO errors */
-    public static final int IOERROR = 1;
-    /** The configuration requested TLS but the server did not support it. */
-    public static final int TLS_REQUIRED = 2;
-    /** Authentication is required but the server did not support it. */
-    public static final int AUTH_REQUIRED = 3;
-    /** General security failures */
-    public static final int GENERAL_SECURITY = 4;
-    /** Authentication failed */
-    public static final int AUTHENTICATION_FAILED = 5;
-    /** Attempt to create duplicate account */
-    public static final int DUPLICATE_ACCOUNT = 6;
-    /** Required security policies reported - advisory only */
-    public static final int SECURITY_POLICIES_REQUIRED = 7;
-   /** Required security policies not supported */
-    public static final int SECURITY_POLICIES_UNSUPPORTED = 8;
-   /** The protocol (or protocol version) isn't supported */
-    public static final int PROTOCOL_VERSION_UNSUPPORTED = 9;
-    /** The server's SSL certificate couldn't be validated */
-    public static final int CERTIFICATE_VALIDATION_ERROR = 10;
-    /** Authentication failed during autodiscover */
-    public static final int AUTODISCOVER_AUTHENTICATION_FAILED = 11;
-    /** Autodiscover completed with a result (non-error) */
-    public static final int AUTODISCOVER_AUTHENTICATION_RESULT = 12;
-    /** Ambiguous failure; server error or bad credentials */
-    public static final int AUTHENTICATION_FAILED_OR_SERVER_ERROR = 13;
-    /** The server refused access */
-    public static final int ACCESS_DENIED = 14;
-    /** The server refused access */
-    public static final int ATTACHMENT_NOT_FOUND = 15;
-    /** A client SSL certificate is required for connections to the server */
-    public static final int CLIENT_CERTIFICATE_REQUIRED = 16;
-    /** The client SSL certificate specified is invalid */
-    public static final int CLIENT_CERTIFICATE_ERROR = 17;
-    /** The server indicates it does not support OAuth authentication */
-    public static final int OAUTH_NOT_SUPPORTED = 18;
-    /** The server indicates it experienced an internal error */
-    public static final int SERVER_ERROR = 19;
-
-    protected int mExceptionType;
-    // Exception type-specific data
-    protected Object mExceptionData;
-
-    public MessagingException(String message, Throwable throwable) {
-        this(UNSPECIFIED_EXCEPTION, message, throwable);
-    }
-
-    public MessagingException(int exceptionType, String message, Throwable throwable) {
-        super(message, throwable);
-        mExceptionType = exceptionType;
-        mExceptionData = null;
-    }
-
-    /**
-     * Constructs a MessagingException with an exceptionType and a null message.
-     * @param exceptionType The exception type to set for this exception.
-     */
-    public MessagingException(int exceptionType) {
-        this(exceptionType, null, null);
-    }
-
-    /**
-     * Constructs a MessagingException with a message.
-     * @param message the message for this exception
-     */
-    public MessagingException(String message) {
-        this(UNSPECIFIED_EXCEPTION, message, null);
-    }
-
-    /**
-     * Constructs a MessagingException with an exceptionType and a message.
-     * @param exceptionType The exception type to set for this exception.
-     */
-    public MessagingException(int exceptionType, String message) {
-        this(exceptionType, message, null);
-    }
-
-    /**
-     * Constructs a MessagingException with an exceptionType, a message, and data
-     * @param exceptionType The exception type to set for this exception.
-     * @param message the message for the exception (or null)
-     * @param data exception-type specific data for the exception (or null)
-     */
-    public MessagingException(int exceptionType, String message, Object data) {
-        super(message);
-        mExceptionType = exceptionType;
-        mExceptionData = data;
-    }
-
-    /**
-     * Return the exception type.  Will be OTHER_EXCEPTION if not explicitly set.
-     *
-     * @return Returns the exception type.
-     */
-    public int getExceptionType() {
-        return mExceptionType;
-    }
-    /**
-     * Return the exception data.  Will be null if not explicitly set.
-     *
-     * @return Returns the exception data.
-     */
-    public Object getExceptionData() {
-        return mExceptionData;
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/Multipart.java b/java/com/android/voicemailomtp/mail/Multipart.java
deleted file mode 100644
index b45ebab..0000000
--- a/java/com/android/voicemailomtp/mail/Multipart.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import java.util.ArrayList;
-
-public abstract class Multipart implements Body {
-    protected Part mParent;
-
-    protected ArrayList<BodyPart> mParts = new ArrayList<BodyPart>();
-
-    protected String mContentType;
-
-    public void addBodyPart(BodyPart part) throws MessagingException {
-        mParts.add(part);
-    }
-
-    public void addBodyPart(BodyPart part, int index) throws MessagingException {
-        mParts.add(index, part);
-    }
-
-    public BodyPart getBodyPart(int index) throws MessagingException {
-        return mParts.get(index);
-    }
-
-    public String getContentType() throws MessagingException {
-        return mContentType;
-    }
-
-    public int getCount() throws MessagingException {
-        return mParts.size();
-    }
-
-    public boolean removeBodyPart(BodyPart part) throws MessagingException {
-        return mParts.remove(part);
-    }
-
-    public void removeBodyPart(int index) throws MessagingException {
-        mParts.remove(index);
-    }
-
-    public Part getParent() throws MessagingException {
-        return mParent;
-    }
-
-    public void setParent(Part parent) throws MessagingException {
-        this.mParent = parent;
-    }
-}
diff --git a/java/com/android/voicemailomtp/mail/PackedString.java b/java/com/android/voicemailomtp/mail/PackedString.java
deleted file mode 100644
index 5857596..0000000
--- a/java/com/android/voicemailomtp/mail/PackedString.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * A utility class for creating and modifying Strings that are tagged and packed together.
- *
- * Uses non-printable (control chars) for internal delimiters;  Intended for regular displayable
- * strings only, so please use base64 or other encoding if you need to hide any binary data here.
- *
- * Binary compatible with Address.pack() format, which should migrate to use this code.
- */
-public class PackedString {
-
-    /**
-     * Packing format is:
-     *   element : [ value ] or [ value TAG-DELIMITER tag ]
-     *   packed-string : [ element ] [ ELEMENT-DELIMITER [ element ] ]*
-     */
-    private static final char DELIMITER_ELEMENT = '\1';
-    private static final char DELIMITER_TAG = '\2';
-
-    private String mString;
-    private HashMap<String, String> mExploded;
-    private static final HashMap<String, String> EMPTY_MAP = new HashMap<String, String>();
-
-    /**
-     * Create a packed string using an already-packed string (e.g. from database)
-     * @param string packed string
-     */
-    public PackedString(String string) {
-        mString = string;
-        mExploded = null;
-    }
-
-    /**
-     * Get the value referred to by a given tag.  If the tag does not exist, return null.
-     * @param tag identifier of string of interest
-     * @return returns value, or null if no string is found
-     */
-    public String get(String tag) {
-        if (mExploded == null) {
-            mExploded = explode(mString);
-        }
-        return mExploded.get(tag);
-    }
-
-    /**
-     * Return a map of all of the values referred to by a given tag.  This is a shallow
-     * copy, don't edit the values.
-     * @return a map of the values in the packed string
-     */
-    public Map<String, String> unpack() {
-        if (mExploded == null) {
-            mExploded = explode(mString);
-        }
-        return new HashMap<String,String>(mExploded);
-    }
-
-    /**
-     * Read out all values into a map.
-     */
-    private static HashMap<String, String> explode(String packed) {
-        if (packed == null || packed.length() == 0) {
-            return EMPTY_MAP;
-        }
-        HashMap<String, String> map = new HashMap<String, String>();
-
-        int length = packed.length();
-        int elementStartIndex = 0;
-        int elementEndIndex = 0;
-        int tagEndIndex = packed.indexOf(DELIMITER_TAG);
-
-        while (elementStartIndex < length) {
-            elementEndIndex = packed.indexOf(DELIMITER_ELEMENT, elementStartIndex);
-            if (elementEndIndex == -1) {
-                elementEndIndex = length;
-            }
-            String tag;
-            String value;
-            if (tagEndIndex == -1 || elementEndIndex <= tagEndIndex) {
-                // in this case the DELIMITER_PERSONAL is in a future pair (or not found)
-                // so synthesize a positional tag for the value, and don't update tagEndIndex
-                value = packed.substring(elementStartIndex, elementEndIndex);
-                tag = Integer.toString(map.size());
-            } else {
-                value = packed.substring(elementStartIndex, tagEndIndex);
-                tag = packed.substring(tagEndIndex + 1, elementEndIndex);
-                // scan forward for next tag, if any
-                tagEndIndex = packed.indexOf(DELIMITER_TAG, elementEndIndex + 1);
-            }
-            map.put(tag, value);
-            elementStartIndex = elementEndIndex + 1;
-        }
-
-        return map;
-    }
-
-    /**
-     * Builder class for creating PackedString values.  Can also be used for editing existing
-     * PackedString representations.
-     */
-    static public class Builder {
-        HashMap<String, String> mMap;
-
-        /**
-         * Create a builder that's empty (for filling)
-         */
-        public Builder() {
-            mMap = new HashMap<String, String>();
-        }
-
-        /**
-         * Create a builder using the values of an existing PackedString (for editing).
-         */
-        public Builder(String packed) {
-            mMap = explode(packed);
-        }
-
-        /**
-         * Add a tagged value
-         * @param tag identifier of string of interest
-         * @param value the value to record in this position.  null to delete entry.
-         */
-        public void put(String tag, String value) {
-            if (value == null) {
-                mMap.remove(tag);
-            } else {
-                mMap.put(tag, value);
-            }
-        }
-
-        /**
-         * Get the value referred to by a given tag.  If the tag does not exist, return null.
-         * @param tag identifier of string of interest
-         * @return returns value, or null if no string is found
-         */
-        public String get(String tag) {
-            return mMap.get(tag);
-        }
-
-        /**
-         * Pack the values and return a single, encoded string
-         */
-        @Override
-        public String toString() {
-            StringBuilder sb = new StringBuilder();
-            for (Map.Entry<String,String> entry : mMap.entrySet()) {
-                if (sb.length() > 0) {
-                    sb.append(DELIMITER_ELEMENT);
-                }
-                sb.append(entry.getValue());
-                sb.append(DELIMITER_TAG);
-                sb.append(entry.getKey());
-            }
-            return sb.toString();
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/mail/Part.java b/java/com/android/voicemailomtp/mail/Part.java
deleted file mode 100644
index 51f8a4c..0000000
--- a/java/com/android/voicemailomtp/mail/Part.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-public interface Part extends Fetchable {
-    public void addHeader(String name, String value) throws MessagingException;
-
-    public void removeHeader(String name) throws MessagingException;
-
-    public void setHeader(String name, String value) throws MessagingException;
-
-    public Body getBody() throws MessagingException;
-
-    public String getContentType() throws MessagingException;
-
-    public String getDisposition() throws MessagingException;
-
-    public String getContentId() throws MessagingException;
-
-    public String[] getHeader(String name) throws MessagingException;
-
-    public void setExtendedHeader(String name, String value) throws MessagingException;
-
-    public String getExtendedHeader(String name) throws MessagingException;
-
-    public int getSize() throws MessagingException;
-
-    public boolean isMimeType(String mimeType) throws MessagingException;
-
-    public String getMimeType() throws MessagingException;
-
-    public void setBody(Body body) throws MessagingException;
-
-    public void writeTo(OutputStream out) throws IOException, MessagingException;
-}
diff --git a/java/com/android/voicemailomtp/mail/PeekableInputStream.java b/java/com/android/voicemailomtp/mail/PeekableInputStream.java
deleted file mode 100644
index c1181d1..0000000
--- a/java/com/android/voicemailomtp/mail/PeekableInputStream.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * A filtering InputStream that allows single byte "peeks" without consuming the byte. The
- * client of this stream can call peek() to see the next available byte in the stream
- * and a subsequent read will still return the peeked byte.
- */
-public class PeekableInputStream extends InputStream {
-    private final InputStream mIn;
-    private boolean mPeeked;
-    private int mPeekedByte;
-
-    public PeekableInputStream(InputStream in) {
-        this.mIn = in;
-    }
-
-    @Override
-    public int read() throws IOException {
-        if (!mPeeked) {
-            return mIn.read();
-        } else {
-            mPeeked = false;
-            return mPeekedByte;
-        }
-    }
-
-    public int peek() throws IOException {
-        if (!mPeeked) {
-            mPeekedByte = read();
-            mPeeked = true;
-        }
-        return mPeekedByte;
-    }
-
-    @Override
-    public int read(byte[] b, int offset, int length) throws IOException {
-        if (!mPeeked) {
-            return mIn.read(b, offset, length);
-        } else {
-            b[0] = (byte)mPeekedByte;
-            mPeeked = false;
-            int r = mIn.read(b, offset + 1, length - 1);
-            if (r == -1) {
-                return 1;
-            } else {
-                return r + 1;
-            }
-        }
-    }
-
-    @Override
-    public int read(byte[] b) throws IOException {
-        return read(b, 0, b.length);
-    }
-
-    @Override
-    public String toString() {
-        return String.format("PeekableInputStream(in=%s, peeked=%b, peekedByte=%d)",
-                mIn.toString(), mPeeked, mPeekedByte);
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/TempDirectory.java b/java/com/android/voicemailomtp/mail/TempDirectory.java
deleted file mode 100644
index dfae360..0000000
--- a/java/com/android/voicemailomtp/mail/TempDirectory.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail;
-
-import android.content.Context;
-
-import java.io.File;
-
-/**
- * TempDirectory caches the directory used for caching file.  It is set up during application
- * initialization.
- */
-public class TempDirectory {
-    private static File sTempDirectory = null;
-
-    public static void setTempDirectory(Context context) {
-        sTempDirectory = context.getCacheDir();
-    }
-
-    public static File getTempDirectory() {
-        if (sTempDirectory == null) {
-            throw new RuntimeException(
-                    "TempDirectory not set.  " +
-                    "If in a unit test, call Email.setTempDirectory(context) in setUp().");
-        }
-        return sTempDirectory;
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java b/java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java
deleted file mode 100644
index 52c43de..0000000
--- a/java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.internet;
-
-import com.android.voicemailomtp.mail.Body;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.TempDirectory;
-
-import org.apache.commons.io.IOUtils;
-
-import android.util.Base64;
-import android.util.Base64OutputStream;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.FilterInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * A Body that is backed by a temp file. The Body exposes a getOutputStream method that allows
- * the user to write to the temp file. After the write the body is available via getInputStream
- * and writeTo one time. After writeTo is called, or the InputStream returned from
- * getInputStream is closed the file is deleted and the Body should be considered disposed of.
- */
-public class BinaryTempFileBody implements Body {
-    private File mFile;
-
-    /**
-     * An alternate way to put data into a BinaryTempFileBody is to simply supply an already-
-     * created file.  Note that this file will be deleted after it is read.
-     * @param filePath The file containing the data to be stored on disk temporarily
-     */
-    public void setFile(String filePath) {
-        mFile = new File(filePath);
-    }
-
-    public OutputStream getOutputStream() throws IOException {
-        mFile = File.createTempFile("body", null, TempDirectory.getTempDirectory());
-        mFile.deleteOnExit();
-        return new FileOutputStream(mFile);
-    }
-
-    @Override
-    public InputStream getInputStream() throws MessagingException {
-        try {
-            return new BinaryTempFileBodyInputStream(new FileInputStream(mFile));
-        }
-        catch (IOException ioe) {
-            throw new MessagingException("Unable to open body", ioe);
-        }
-    }
-
-    @Override
-    public void writeTo(OutputStream out) throws IOException, MessagingException {
-        InputStream in = getInputStream();
-        Base64OutputStream base64Out = new Base64OutputStream(
-            out, Base64.CRLF | Base64.NO_CLOSE);
-        IOUtils.copy(in, base64Out);
-        base64Out.close();
-        mFile.delete();
-        in.close();
-    }
-
-    class BinaryTempFileBodyInputStream extends FilterInputStream {
-        public BinaryTempFileBodyInputStream(InputStream in) {
-            super(in);
-        }
-
-        @Override
-        public void close() throws IOException {
-            super.close();
-            mFile.delete();
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java b/java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java
deleted file mode 100644
index 8a9c45c..0000000
--- a/java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.internet;
-
-import com.android.voicemailomtp.mail.Body;
-import com.android.voicemailomtp.mail.BodyPart;
-import com.android.voicemailomtp.mail.MessagingException;
-
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.util.regex.Pattern;
-
-/**
- * TODO this is a close approximation of Message, need to update along with
- * Message.
- */
-public class MimeBodyPart extends BodyPart {
-    protected MimeHeader mHeader = new MimeHeader();
-    protected MimeHeader mExtendedHeader;
-    protected Body mBody;
-    protected int mSize;
-
-    // regex that matches content id surrounded by "<>" optionally.
-    private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
-    // regex that matches end of line.
-    private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
-
-    public MimeBodyPart() throws MessagingException {
-        this(null);
-    }
-
-    public MimeBodyPart(Body body) throws MessagingException {
-        this(body, null);
-    }
-
-    public MimeBodyPart(Body body, String mimeType) throws MessagingException {
-        if (mimeType != null) {
-            setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType);
-        }
-        setBody(body);
-    }
-
-    protected String getFirstHeader(String name) throws MessagingException {
-        return mHeader.getFirstHeader(name);
-    }
-
-    @Override
-    public void addHeader(String name, String value) throws MessagingException {
-        mHeader.addHeader(name, value);
-    }
-
-    @Override
-    public void setHeader(String name, String value) throws MessagingException {
-        mHeader.setHeader(name, value);
-    }
-
-    @Override
-    public String[] getHeader(String name) throws MessagingException {
-        return mHeader.getHeader(name);
-    }
-
-    @Override
-    public void removeHeader(String name) throws MessagingException {
-        mHeader.removeHeader(name);
-    }
-
-    @Override
-    public Body getBody() throws MessagingException {
-        return mBody;
-    }
-
-    @Override
-    public void setBody(Body body) throws MessagingException {
-        this.mBody = body;
-        if (body instanceof com.android.voicemailomtp.mail.Multipart) {
-            com.android.voicemailomtp.mail.Multipart multipart =
-                ((com.android.voicemailomtp.mail.Multipart)body);
-            multipart.setParent(this);
-            setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
-        }
-        else if (body instanceof TextBody) {
-            String contentType = String.format("%s;\n charset=utf-8", getMimeType());
-            String name = MimeUtility.getHeaderParameter(getContentType(), "name");
-            if (name != null) {
-                contentType += String.format(";\n name=\"%s\"", name);
-            }
-            setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
-            setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
-        }
-    }
-
-    @Override
-    public String getContentType() throws MessagingException {
-        String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
-        if (contentType == null) {
-            return "text/plain";
-        } else {
-            return contentType;
-        }
-    }
-
-    @Override
-    public String getDisposition() throws MessagingException {
-        String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
-        if (contentDisposition == null) {
-            return null;
-        } else {
-            return contentDisposition;
-        }
-    }
-
-    @Override
-    public String getContentId() throws MessagingException {
-        String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
-        if (contentId == null) {
-            return null;
-        } else {
-            // remove optionally surrounding brackets.
-            return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
-        }
-    }
-
-    @Override
-    public String getMimeType() throws MessagingException {
-        return MimeUtility.getHeaderParameter(getContentType(), null);
-    }
-
-    @Override
-    public boolean isMimeType(String mimeType) throws MessagingException {
-        return getMimeType().equals(mimeType);
-    }
-
-    public void setSize(int size) {
-        this.mSize = size;
-    }
-
-    @Override
-    public int getSize() throws MessagingException {
-        return mSize;
-    }
-
-    /**
-     * Set extended header
-     *
-     * @param name Extended header name
-     * @param value header value - flattened by removing CR-NL if any
-     * remove header if value is null
-     * @throws MessagingException
-     */
-    @Override
-    public void setExtendedHeader(String name, String value) throws MessagingException {
-        if (value == null) {
-            if (mExtendedHeader != null) {
-                mExtendedHeader.removeHeader(name);
-            }
-            return;
-        }
-        if (mExtendedHeader == null) {
-            mExtendedHeader = new MimeHeader();
-        }
-        mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
-    }
-
-    /**
-     * Get extended header
-     *
-     * @param name Extended header name
-     * @return header value - null if header does not exist
-     * @throws MessagingException
-     */
-    @Override
-    public String getExtendedHeader(String name) throws MessagingException {
-        if (mExtendedHeader == null) {
-            return null;
-        }
-        return mExtendedHeader.getFirstHeader(name);
-    }
-
-    /**
-     * Write the MimeMessage out in MIME format.
-     */
-    @Override
-    public void writeTo(OutputStream out) throws IOException, MessagingException {
-        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
-        mHeader.writeTo(out);
-        writer.write("\r\n");
-        writer.flush();
-        if (mBody != null) {
-            mBody.writeTo(out);
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/mail/internet/MimeHeader.java b/java/com/android/voicemailomtp/mail/internet/MimeHeader.java
deleted file mode 100644
index 4b0aea7..0000000
--- a/java/com/android/voicemailomtp/mail/internet/MimeHeader.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.internet;
-
-import com.android.voicemailomtp.mail.MessagingException;
-
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.util.ArrayList;
-
-public class MimeHeader {
-    /**
-     * Application specific header that contains Store specific information about an attachment.
-     * In IMAP this contains the IMAP BODYSTRUCTURE part id so that the ImapStore can later
-     * retrieve the attachment at will from the server.
-     * The info is recorded from this header on LocalStore.appendMessage and is put back
-     * into the MIME data by LocalStore.fetch.
-     */
-    public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA = "X-Android-Attachment-StoreData";
-
-    public static final String HEADER_CONTENT_TYPE = "Content-Type";
-    public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
-    public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
-    public static final String HEADER_CONTENT_ID = "Content-ID";
-
-    /**
-     * Fields that should be omitted when writing the header using writeTo()
-     */
-    private static final String[] WRITE_OMIT_FIELDS = {
-//        HEADER_ANDROID_ATTACHMENT_DOWNLOADED,
-//        HEADER_ANDROID_ATTACHMENT_ID,
-        HEADER_ANDROID_ATTACHMENT_STORE_DATA
-    };
-
-    protected final ArrayList<Field> mFields = new ArrayList<Field>();
-
-    public void clear() {
-        mFields.clear();
-    }
-
-    public String getFirstHeader(String name) throws MessagingException {
-        String[] header = getHeader(name);
-        if (header == null) {
-            return null;
-        }
-        return header[0];
-    }
-
-    public void addHeader(String name, String value) throws MessagingException {
-        mFields.add(new Field(name, value));
-    }
-
-    public void setHeader(String name, String value) throws MessagingException {
-        if (name == null || value == null) {
-            return;
-        }
-        removeHeader(name);
-        addHeader(name, value);
-    }
-
-    public String[] getHeader(String name) throws MessagingException {
-        ArrayList<String> values = new ArrayList<String>();
-        for (Field field : mFields) {
-            if (field.name.equalsIgnoreCase(name)) {
-                values.add(field.value);
-            }
-        }
-        if (values.size() == 0) {
-            return null;
-        }
-        return values.toArray(new String[] {});
-    }
-
-    public void removeHeader(String name) throws MessagingException {
-        ArrayList<Field> removeFields = new ArrayList<Field>();
-        for (Field field : mFields) {
-            if (field.name.equalsIgnoreCase(name)) {
-                removeFields.add(field);
-            }
-        }
-        mFields.removeAll(removeFields);
-    }
-
-    /**
-     * Write header into String
-     *
-     * @return CR-NL separated header string except the headers in writeOmitFields
-     * null if header is empty
-     */
-    public String writeToString() {
-        if (mFields.size() == 0) {
-            return null;
-        }
-        StringBuilder builder = new StringBuilder();
-        for (Field field : mFields) {
-            if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) {
-                builder.append(field.name + ": " + field.value + "\r\n");
-            }
-        }
-        return builder.toString();
-    }
-
-    public void writeTo(OutputStream out) throws IOException, MessagingException {
-        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
-        for (Field field : mFields) {
-            if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) {
-                writer.write(field.name + ": " + field.value + "\r\n");
-            }
-        }
-        writer.flush();
-    }
-
-    private static class Field {
-        final String name;
-        final String value;
-
-        public Field(String name, String value) {
-            this.name = name;
-            this.value = value;
-        }
-
-        @Override
-        public String toString() {
-            return name + "=" + value;
-        }
-    }
-
-    @Override
-    public String toString() {
-        return (mFields == null) ? null : mFields.toString();
-    }
-
-    public final static boolean arrayContains(Object[] a, Object o) {
-        int index = arrayIndex(a, o);
-        return (index >= 0);
-    }
-
-    public final static int arrayIndex(Object[] a, Object o) {
-        for (int i = 0, count = a.length; i < count; i++) {
-            if (a[i].equals(o)) {
-                return i;
-            }
-        }
-        return -1;
-    }
-}
diff --git a/java/com/android/voicemailomtp/mail/internet/MimeMessage.java b/java/com/android/voicemailomtp/mail/internet/MimeMessage.java
deleted file mode 100644
index a11cd6d..0000000
--- a/java/com/android/voicemailomtp/mail/internet/MimeMessage.java
+++ /dev/null
@@ -1,675 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.internet;
-
-import com.android.voicemailomtp.mail.Address;
-import com.android.voicemailomtp.mail.Body;
-import com.android.voicemailomtp.mail.BodyPart;
-import com.android.voicemailomtp.mail.Message;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.Multipart;
-import com.android.voicemailomtp.mail.Part;
-import com.android.voicemailomtp.mail.utils.LogUtils;
-
-import org.apache.james.mime4j.BodyDescriptor;
-import org.apache.james.mime4j.ContentHandler;
-import org.apache.james.mime4j.EOLConvertingInputStream;
-import org.apache.james.mime4j.MimeStreamParser;
-import org.apache.james.mime4j.field.DateTimeField;
-import org.apache.james.mime4j.field.Field;
-
-import android.text.TextUtils;
-
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-import java.util.Stack;
-import java.util.regex.Pattern;
-
-/**
- * An implementation of Message that stores all of its metadata in RFC 822 and
- * RFC 2045 style headers.
- *
- * NOTE:  Automatic generation of a local message-id is becoming unwieldy and should be removed.
- * It would be better to simply do it explicitly on local creation of new outgoing messages.
- */
-public class MimeMessage extends Message {
-    private MimeHeader mHeader;
-    private MimeHeader mExtendedHeader;
-
-    // NOTE:  The fields here are transcribed out of headers, and values stored here will supersede
-    // the values found in the headers.  Use caution to prevent any out-of-phase errors.  In
-    // particular, any adds/changes/deletes here must be echoed by changes in the parse() function.
-    private Address[] mFrom;
-    private Address[] mTo;
-    private Address[] mCc;
-    private Address[] mBcc;
-    private Address[] mReplyTo;
-    private Date mSentDate;
-    private Body mBody;
-    protected int mSize;
-    private boolean mInhibitLocalMessageId = false;
-    private boolean mComplete = true;
-
-    // Shared random source for generating local message-id values
-    private static final java.util.Random sRandom = new java.util.Random();
-
-    // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
-    // "Jan", not the other localized format like "Ene" (meaning January in locale es).
-    // This conversion is used when generating outgoing MIME messages. Incoming MIME date
-    // headers are parsed by org.apache.james.mime4j.field.DateTimeField which does not have any
-    // localization code.
-    private static final SimpleDateFormat DATE_FORMAT =
-        new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
-
-    // regex that matches content id surrounded by "<>" optionally.
-    private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
-    // regex that matches end of line.
-    private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
-
-    public MimeMessage() {
-        mHeader = null;
-    }
-
-    /**
-     * Generate a local message id.  This is only used when none has been assigned, and is
-     * installed lazily.  Any remote (typically server-assigned) message id takes precedence.
-     * @return a long, locally-generated message-ID value
-     */
-    private static String generateMessageId() {
-        final StringBuilder sb = new StringBuilder();
-        sb.append("<");
-        for (int i = 0; i < 24; i++) {
-            // We'll use a 5-bit range (0..31)
-            final int value = sRandom.nextInt() & 31;
-            final char c = "0123456789abcdefghijklmnopqrstuv".charAt(value);
-            sb.append(c);
-        }
-        sb.append(".");
-        sb.append(Long.toString(System.currentTimeMillis()));
-        sb.append("@email.android.com>");
-        return sb.toString();
-    }
-
-    /**
-     * Parse the given InputStream using Apache Mime4J to build a MimeMessage.
-     *
-     * @param in InputStream providing message content
-     * @throws IOException
-     * @throws MessagingException
-     */
-    public MimeMessage(InputStream in) throws IOException, MessagingException {
-        parse(in);
-    }
-
-    private MimeStreamParser init() {
-        // Before parsing the input stream, clear all local fields that may be superceded by
-        // the new incoming message.
-        getMimeHeaders().clear();
-        mInhibitLocalMessageId = true;
-        mFrom = null;
-        mTo = null;
-        mCc = null;
-        mBcc = null;
-        mReplyTo = null;
-        mSentDate = null;
-        mBody = null;
-
-        final MimeStreamParser parser = new MimeStreamParser();
-        parser.setContentHandler(new MimeMessageBuilder());
-        return parser;
-    }
-
-    protected void parse(InputStream in) throws IOException, MessagingException {
-        final MimeStreamParser parser = init();
-        parser.parse(new EOLConvertingInputStream(in));
-        mComplete = !parser.getPrematureEof();
-    }
-
-    public void parse(InputStream in, EOLConvertingInputStream.Callback callback)
-            throws IOException, MessagingException {
-        final MimeStreamParser parser = init();
-        parser.parse(new EOLConvertingInputStream(in, getSize(), callback));
-        mComplete = !parser.getPrematureEof();
-    }
-
-    /**
-     * Return the internal mHeader value, with very lazy initialization.
-     * The goal is to save memory by not creating the headers until needed.
-     */
-    private MimeHeader getMimeHeaders() {
-        if (mHeader == null) {
-            mHeader = new MimeHeader();
-        }
-        return mHeader;
-    }
-
-    @Override
-    public Date getReceivedDate() throws MessagingException {
-        return null;
-    }
-
-    @Override
-    public Date getSentDate() throws MessagingException {
-        if (mSentDate == null) {
-            try {
-                DateTimeField field = (DateTimeField)Field.parse("Date: "
-                        + MimeUtility.unfoldAndDecode(getFirstHeader("Date")));
-                mSentDate = field.getDate();
-                // TODO: We should make it more clear what exceptions can be thrown here,
-                // and whether they reflect a normal or error condition.
-            } catch (Exception e) {
-                LogUtils.v(LogUtils.TAG, "Message missing Date header");
-            }
-        }
-        if (mSentDate == null) {
-            // If we still don't have a date, fall back to "Delivery-date"
-            try {
-                DateTimeField field = (DateTimeField)Field.parse("Date: "
-                        + MimeUtility.unfoldAndDecode(getFirstHeader("Delivery-date")));
-                mSentDate = field.getDate();
-                // TODO: We should make it more clear what exceptions can be thrown here,
-                // and whether they reflect a normal or error condition.
-            } catch (Exception e) {
-                LogUtils.v(LogUtils.TAG, "Message also missing Delivery-Date header");
-            }
-        }
-        return mSentDate;
-    }
-
-    @Override
-    public void setSentDate(Date sentDate) throws MessagingException {
-        setHeader("Date", DATE_FORMAT.format(sentDate));
-        this.mSentDate = sentDate;
-    }
-
-    @Override
-    public String getContentType() throws MessagingException {
-        final String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
-        if (contentType == null) {
-            return "text/plain";
-        } else {
-            return contentType;
-        }
-    }
-
-    @Override
-    public String getDisposition() throws MessagingException {
-        return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
-    }
-
-    @Override
-    public String getContentId() throws MessagingException {
-        final String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
-        if (contentId == null) {
-            return null;
-        } else {
-            // remove optionally surrounding brackets.
-            return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
-        }
-    }
-
-    public boolean isComplete() {
-        return mComplete;
-    }
-
-    @Override
-    public String getMimeType() throws MessagingException {
-        return MimeUtility.getHeaderParameter(getContentType(), null);
-    }
-
-    @Override
-    public int getSize() throws MessagingException {
-        return mSize;
-    }
-
-    /**
-     * Returns a list of the given recipient type from this message. If no addresses are
-     * found the method returns an empty array.
-     */
-    @Override
-    public Address[] getRecipients(String type) throws MessagingException {
-        if (type == RECIPIENT_TYPE_TO) {
-            if (mTo == null) {
-                mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
-            }
-            return mTo;
-        } else if (type == RECIPIENT_TYPE_CC) {
-            if (mCc == null) {
-                mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
-            }
-            return mCc;
-        } else if (type == RECIPIENT_TYPE_BCC) {
-            if (mBcc == null) {
-                mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
-            }
-            return mBcc;
-        } else {
-            throw new MessagingException("Unrecognized recipient type.");
-        }
-    }
-
-    @Override
-    public void setRecipients(String type, Address[] addresses) throws MessagingException {
-        final int TO_LENGTH = 4;  // "To: "
-        final int CC_LENGTH = 4;  // "Cc: "
-        final int BCC_LENGTH = 5; // "Bcc: "
-        if (type == RECIPIENT_TYPE_TO) {
-            if (addresses == null || addresses.length == 0) {
-                removeHeader("To");
-                this.mTo = null;
-            } else {
-                setHeader("To", MimeUtility.fold(Address.toHeader(addresses), TO_LENGTH));
-                this.mTo = addresses;
-            }
-        } else if (type == RECIPIENT_TYPE_CC) {
-            if (addresses == null || addresses.length == 0) {
-                removeHeader("CC");
-                this.mCc = null;
-            } else {
-                setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), CC_LENGTH));
-                this.mCc = addresses;
-            }
-        } else if (type == RECIPIENT_TYPE_BCC) {
-            if (addresses == null || addresses.length == 0) {
-                removeHeader("BCC");
-                this.mBcc = null;
-            } else {
-                setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), BCC_LENGTH));
-                this.mBcc = addresses;
-            }
-        } else {
-            throw new MessagingException("Unrecognized recipient type.");
-        }
-    }
-
-    /**
-     * Returns the unfolded, decoded value of the Subject header.
-     */
-    @Override
-    public String getSubject() throws MessagingException {
-        return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
-    }
-
-    @Override
-    public void setSubject(String subject) throws MessagingException {
-        final int HEADER_NAME_LENGTH = 9;     // "Subject: "
-        setHeader("Subject", MimeUtility.foldAndEncode2(subject, HEADER_NAME_LENGTH));
-    }
-
-    @Override
-    public Address[] getFrom() throws MessagingException {
-        if (mFrom == null) {
-            String list = MimeUtility.unfold(getFirstHeader("From"));
-            if (list == null || list.length() == 0) {
-                list = MimeUtility.unfold(getFirstHeader("Sender"));
-            }
-            mFrom = Address.parse(list);
-        }
-        return mFrom;
-    }
-
-    @Override
-    public void setFrom(Address from) throws MessagingException {
-        final int FROM_LENGTH = 6;  // "From: "
-        if (from != null) {
-            setHeader("From", MimeUtility.fold(from.toHeader(), FROM_LENGTH));
-            this.mFrom = new Address[] {
-                    from
-                };
-        } else {
-            this.mFrom = null;
-        }
-    }
-
-    @Override
-    public Address[] getReplyTo() throws MessagingException {
-        if (mReplyTo == null) {
-            mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
-        }
-        return mReplyTo;
-    }
-
-    @Override
-    public void setReplyTo(Address[] replyTo) throws MessagingException {
-        final int REPLY_TO_LENGTH = 10;  // "Reply-to: "
-        if (replyTo == null || replyTo.length == 0) {
-            removeHeader("Reply-to");
-            mReplyTo = null;
-        } else {
-            setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), REPLY_TO_LENGTH));
-            mReplyTo = replyTo;
-        }
-    }
-
-    /**
-     * Set the mime "Message-ID" header
-     * @param messageId the new Message-ID value
-     * @throws MessagingException
-     */
-    @Override
-    public void setMessageId(String messageId) throws MessagingException {
-        setHeader("Message-ID", messageId);
-    }
-
-    /**
-     * Get the mime "Message-ID" header.  This value will be preloaded with a locally-generated
-     * random ID, if the value has not previously been set.  Local generation can be inhibited/
-     * overridden by explicitly clearing the headers, removing the message-id header, etc.
-     * @return the Message-ID header string, or null if explicitly has been set to null
-     */
-    @Override
-    public String getMessageId() throws MessagingException {
-        String messageId = getFirstHeader("Message-ID");
-        if (messageId == null && !mInhibitLocalMessageId) {
-            messageId = generateMessageId();
-            setMessageId(messageId);
-        }
-        return messageId;
-    }
-
-    @Override
-    public void saveChanges() throws MessagingException {
-        throw new MessagingException("saveChanges not yet implemented");
-    }
-
-    @Override
-    public Body getBody() throws MessagingException {
-        return mBody;
-    }
-
-    @Override
-    public void setBody(Body body) throws MessagingException {
-        this.mBody = body;
-        if (body instanceof Multipart) {
-            final Multipart multipart = ((Multipart)body);
-            multipart.setParent(this);
-            setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
-            setHeader("MIME-Version", "1.0");
-        }
-        else if (body instanceof TextBody) {
-            setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8",
-                    getMimeType()));
-            setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
-        }
-    }
-
-    protected String getFirstHeader(String name) throws MessagingException {
-        return getMimeHeaders().getFirstHeader(name);
-    }
-
-    @Override
-    public void addHeader(String name, String value) throws MessagingException {
-        getMimeHeaders().addHeader(name, value);
-    }
-
-    @Override
-    public void setHeader(String name, String value) throws MessagingException {
-        getMimeHeaders().setHeader(name, value);
-    }
-
-    @Override
-    public String[] getHeader(String name) throws MessagingException {
-        return getMimeHeaders().getHeader(name);
-    }
-
-    @Override
-    public void removeHeader(String name) throws MessagingException {
-        getMimeHeaders().removeHeader(name);
-        if ("Message-ID".equalsIgnoreCase(name)) {
-            mInhibitLocalMessageId = true;
-        }
-    }
-
-    /**
-     * Set extended header
-     *
-     * @param name Extended header name
-     * @param value header value - flattened by removing CR-NL if any
-     * remove header if value is null
-     * @throws MessagingException
-     */
-    @Override
-    public void setExtendedHeader(String name, String value) throws MessagingException {
-        if (value == null) {
-            if (mExtendedHeader != null) {
-                mExtendedHeader.removeHeader(name);
-            }
-            return;
-        }
-        if (mExtendedHeader == null) {
-            mExtendedHeader = new MimeHeader();
-        }
-        mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
-    }
-
-    /**
-     * Get extended header
-     *
-     * @param name Extended header name
-     * @return header value - null if header does not exist
-     * @throws MessagingException
-     */
-    @Override
-    public String getExtendedHeader(String name) throws MessagingException {
-        if (mExtendedHeader == null) {
-            return null;
-        }
-        return mExtendedHeader.getFirstHeader(name);
-    }
-
-    /**
-     * Set entire extended headers from String
-     *
-     * @param headers Extended header and its value - "CR-NL-separated pairs
-     * if null or empty, remove entire extended headers
-     * @throws MessagingException
-     */
-    public void setExtendedHeaders(String headers) throws MessagingException {
-        if (TextUtils.isEmpty(headers)) {
-            mExtendedHeader = null;
-        } else {
-            mExtendedHeader = new MimeHeader();
-            for (final String header : END_OF_LINE.split(headers)) {
-                final String[] tokens = header.split(":", 2);
-                if (tokens.length != 2) {
-                    throw new MessagingException("Illegal extended headers: " + headers);
-                }
-                mExtendedHeader.setHeader(tokens[0].trim(), tokens[1].trim());
-            }
-        }
-    }
-
-    /**
-     * Get entire extended headers as String
-     *
-     * @return "CR-NL-separated extended headers - null if extended header does not exist
-     */
-    public String getExtendedHeaders() {
-        if (mExtendedHeader != null) {
-            return mExtendedHeader.writeToString();
-        }
-        return null;
-    }
-
-    /**
-     * Write message header and body to output stream
-     *
-     * @param out Output steam to write message header and body.
-     */
-    @Override
-    public void writeTo(OutputStream out) throws IOException, MessagingException {
-        final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
-        // Force creation of local message-id
-        getMessageId();
-        getMimeHeaders().writeTo(out);
-        // mExtendedHeader will not be write out to external output stream,
-        // because it is intended to internal use.
-        writer.write("\r\n");
-        writer.flush();
-        if (mBody != null) {
-            mBody.writeTo(out);
-        }
-    }
-
-    @Override
-    public InputStream getInputStream() throws MessagingException {
-        return null;
-    }
-
-    class MimeMessageBuilder implements ContentHandler {
-        private final Stack<Object> stack = new Stack<Object>();
-
-        public MimeMessageBuilder() {
-        }
-
-        private void expect(Class<?> c) {
-            if (!c.isInstance(stack.peek())) {
-                throw new IllegalStateException("Internal stack error: " + "Expected '"
-                        + c.getName() + "' found '" + stack.peek().getClass().getName() + "'");
-            }
-        }
-
-        @Override
-        public void startMessage() {
-            if (stack.isEmpty()) {
-                stack.push(MimeMessage.this);
-            } else {
-                expect(Part.class);
-                try {
-                    final MimeMessage m = new MimeMessage();
-                    ((Part)stack.peek()).setBody(m);
-                    stack.push(m);
-                } catch (MessagingException me) {
-                    throw new Error(me);
-                }
-            }
-        }
-
-        @Override
-        public void endMessage() {
-            expect(MimeMessage.class);
-            stack.pop();
-        }
-
-        @Override
-        public void startHeader() {
-            expect(Part.class);
-        }
-
-        @Override
-        public void field(String fieldData) {
-            expect(Part.class);
-            try {
-                final String[] tokens = fieldData.split(":", 2);
-                ((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim());
-            } catch (MessagingException me) {
-                throw new Error(me);
-            }
-        }
-
-        @Override
-        public void endHeader() {
-            expect(Part.class);
-        }
-
-        @Override
-        public void startMultipart(BodyDescriptor bd) {
-            expect(Part.class);
-
-            final Part e = (Part)stack.peek();
-            try {
-                final MimeMultipart multiPart = new MimeMultipart(e.getContentType());
-                e.setBody(multiPart);
-                stack.push(multiPart);
-            } catch (MessagingException me) {
-                throw new Error(me);
-            }
-        }
-
-        @Override
-        public void body(BodyDescriptor bd, InputStream in) throws IOException {
-            expect(Part.class);
-            final Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
-            try {
-                ((Part)stack.peek()).setBody(body);
-            } catch (MessagingException me) {
-                throw new Error(me);
-            }
-        }
-
-        @Override
-        public void endMultipart() {
-            stack.pop();
-        }
-
-        @Override
-        public void startBodyPart() {
-            expect(MimeMultipart.class);
-
-            try {
-                final MimeBodyPart bodyPart = new MimeBodyPart();
-                ((MimeMultipart)stack.peek()).addBodyPart(bodyPart);
-                stack.push(bodyPart);
-            } catch (MessagingException me) {
-                throw new Error(me);
-            }
-        }
-
-        @Override
-        public void endBodyPart() {
-            expect(BodyPart.class);
-            stack.pop();
-        }
-
-        @Override
-        public void epilogue(InputStream is) throws IOException {
-            expect(MimeMultipart.class);
-            final StringBuilder sb = new StringBuilder();
-            int b;
-            while ((b = is.read()) != -1) {
-                sb.append((char)b);
-            }
-            // TODO: why is this commented out?
-            // ((Multipart) stack.peek()).setEpilogue(sb.toString());
-        }
-
-        @Override
-        public void preamble(InputStream is) throws IOException {
-            expect(MimeMultipart.class);
-            final StringBuilder sb = new StringBuilder();
-            int b;
-            while ((b = is.read()) != -1) {
-                sb.append((char)b);
-            }
-            try {
-                ((MimeMultipart)stack.peek()).setPreamble(sb.toString());
-            } catch (MessagingException me) {
-                throw new Error(me);
-            }
-        }
-
-        @Override
-        public void raw(InputStream is) throws IOException {
-            throw new UnsupportedOperationException("Not supported");
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/mail/internet/MimeMultipart.java b/java/com/android/voicemailomtp/mail/internet/MimeMultipart.java
deleted file mode 100644
index 1119243..0000000
--- a/java/com/android/voicemailomtp/mail/internet/MimeMultipart.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.internet;
-
-import com.android.voicemailomtp.mail.BodyPart;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.Multipart;
-
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-
-public class MimeMultipart extends Multipart {
-    protected String mPreamble;
-
-    protected String mContentType;
-
-    protected String mBoundary;
-
-    protected String mSubType;
-
-    public MimeMultipart() throws MessagingException {
-        mBoundary = generateBoundary();
-        setSubType("mixed");
-    }
-
-    public MimeMultipart(String contentType) throws MessagingException {
-        this.mContentType = contentType;
-        try {
-            mSubType = MimeUtility.getHeaderParameter(contentType, null).split("/")[1];
-            mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary");
-            if (mBoundary == null) {
-                throw new MessagingException("MultiPart does not contain boundary: " + contentType);
-            }
-        } catch (Exception e) {
-            throw new MessagingException(
-                    "Invalid MultiPart Content-Type; must contain subtype and boundary. ("
-                            + contentType + ")", e);
-        }
-    }
-
-    public String generateBoundary() {
-        StringBuffer sb = new StringBuffer();
-        sb.append("----");
-        for (int i = 0; i < 30; i++) {
-            sb.append(Integer.toString((int)(Math.random() * 35), 36));
-        }
-        return sb.toString().toUpperCase();
-    }
-
-    public String getPreamble() throws MessagingException {
-        return mPreamble;
-    }
-
-    public void setPreamble(String preamble) throws MessagingException {
-        this.mPreamble = preamble;
-    }
-
-    @Override
-    public String getContentType() throws MessagingException {
-        return mContentType;
-    }
-
-    public void setSubType(String subType) throws MessagingException {
-        this.mSubType = subType;
-        mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary);
-    }
-
-    @Override
-    public void writeTo(OutputStream out) throws IOException, MessagingException {
-        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
-
-        if (mPreamble != null) {
-            writer.write(mPreamble + "\r\n");
-        }
-
-        for (int i = 0, count = mParts.size(); i < count; i++) {
-            BodyPart bodyPart = mParts.get(i);
-            writer.write("--" + mBoundary + "\r\n");
-            writer.flush();
-            bodyPart.writeTo(out);
-            writer.write("\r\n");
-        }
-
-        writer.write("--" + mBoundary + "--\r\n");
-        writer.flush();
-    }
-
-    @Override
-    public InputStream getInputStream() throws MessagingException {
-        return null;
-    }
-
-    public String getSubTypeForTest() {
-        return mSubType;
-    }
-}
diff --git a/java/com/android/voicemailomtp/mail/internet/MimeUtility.java b/java/com/android/voicemailomtp/mail/internet/MimeUtility.java
deleted file mode 100644
index 4d310b0..0000000
--- a/java/com/android/voicemailomtp/mail/internet/MimeUtility.java
+++ /dev/null
@@ -1,416 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.internet;
-
-import android.text.TextUtils;
-import android.util.Base64;
-import android.util.Base64DataException;
-import android.util.Base64InputStream;
-
-import com.android.voicemailomtp.mail.Body;
-import com.android.voicemailomtp.mail.BodyPart;
-import com.android.voicemailomtp.mail.Message;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.Multipart;
-import com.android.voicemailomtp.mail.Part;
-import com.android.voicemailomtp.VvmLog;
-
-import org.apache.commons.io.IOUtils;
-import org.apache.james.mime4j.codec.EncoderUtil;
-import org.apache.james.mime4j.decoder.DecoderUtil;
-import org.apache.james.mime4j.decoder.QuotedPrintableInputStream;
-import org.apache.james.mime4j.util.CharsetUtil;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class MimeUtility {
-    private static final String LOG_TAG = "Email";
-
-    public static final String MIME_TYPE_RFC822 = "message/rfc822";
-    private final static Pattern PATTERN_CR_OR_LF = Pattern.compile("\r|\n");
-
-    /**
-     * Replace sequences of CRLF+WSP with WSP.  Tries to preserve original string
-     * object whenever possible.
-     */
-    public static String unfold(String s) {
-        if (s == null) {
-            return null;
-        }
-        Matcher patternMatcher = PATTERN_CR_OR_LF.matcher(s);
-        if (patternMatcher.find()) {
-            patternMatcher.reset();
-            s = patternMatcher.replaceAll("");
-        }
-        return s;
-    }
-
-    public static String decode(String s) {
-        if (s == null) {
-            return null;
-        }
-        return DecoderUtil.decodeEncodedWords(s);
-    }
-
-    public static String unfoldAndDecode(String s) {
-        return decode(unfold(s));
-    }
-
-    // TODO implement proper foldAndEncode
-    // NOTE: When this really works, we *must* remove all calls to foldAndEncode2() to prevent
-    // duplication of encoding.
-    public static String foldAndEncode(String s) {
-        return s;
-    }
-
-    /**
-     * INTERIM version of foldAndEncode that will be used only by Subject: headers.
-     * This is safer than implementing foldAndEncode() (see above) and risking unknown damage
-     * to other headers.
-     *
-     * TODO: Copy this code to foldAndEncode(), get rid of this function, confirm all working OK.
-     *
-     * @param s original string to encode and fold
-     * @param usedCharacters number of characters already used up by header name
-
-     * @return the String ready to be transmitted
-     */
-    public static String foldAndEncode2(String s, int usedCharacters) {
-        // james.mime4j.codec.EncoderUtil.java
-        // encode:  encodeIfNecessary(text, usage, numUsedInHeaderName)
-        // Usage.TEXT_TOKENlooks like the right thing for subjects
-        // use WORD_ENTITY for address/names
-
-        String encoded = EncoderUtil.encodeIfNecessary(s, EncoderUtil.Usage.TEXT_TOKEN,
-                usedCharacters);
-
-        return fold(encoded, usedCharacters);
-    }
-
-    /**
-     * INTERIM:  From newer version of org.apache.james (but we don't want to import
-     * the entire MimeUtil class).
-     *
-     * Splits the specified string into a multiple-line representation with
-     * lines no longer than 76 characters (because the line might contain
-     * encoded words; see <a href='http://www.faqs.org/rfcs/rfc2047.html'>RFC
-     * 2047</a> section 2). If the string contains non-whitespace sequences
-     * longer than 76 characters a line break is inserted at the whitespace
-     * character following the sequence resulting in a line longer than 76
-     * characters.
-     *
-     * @param s
-     *            string to split.
-     * @param usedCharacters
-     *            number of characters already used up. Usually the number of
-     *            characters for header field name plus colon and one space.
-     * @return a multiple-line representation of the given string.
-     */
-    public static String fold(String s, int usedCharacters) {
-        final int maxCharacters = 76;
-
-        final int length = s.length();
-        if (usedCharacters + length <= maxCharacters)
-            return s;
-
-        StringBuilder sb = new StringBuilder();
-
-        int lastLineBreak = -usedCharacters;
-        int wspIdx = indexOfWsp(s, 0);
-        while (true) {
-            if (wspIdx == length) {
-                sb.append(s.substring(Math.max(0, lastLineBreak)));
-                return sb.toString();
-            }
-
-            int nextWspIdx = indexOfWsp(s, wspIdx + 1);
-
-            if (nextWspIdx - lastLineBreak > maxCharacters) {
-                sb.append(s.substring(Math.max(0, lastLineBreak), wspIdx));
-                sb.append("\r\n");
-                lastLineBreak = wspIdx;
-            }
-
-            wspIdx = nextWspIdx;
-        }
-    }
-
-    /**
-     * INTERIM:  From newer version of org.apache.james (but we don't want to import
-     * the entire MimeUtil class).
-     *
-     * Search for whitespace.
-     */
-    private static int indexOfWsp(String s, int fromIndex) {
-        final int len = s.length();
-        for (int index = fromIndex; index < len; index++) {
-            char c = s.charAt(index);
-            if (c == ' ' || c == '\t')
-                return index;
-        }
-        return len;
-    }
-
-    /**
-     * Returns the named parameter of a header field. If name is null the first
-     * parameter is returned, or if there are no additional parameters in the
-     * field the entire field is returned. Otherwise the named parameter is
-     * searched for in a case insensitive fashion and returned. If the parameter
-     * cannot be found the method returns null.
-     *
-     * TODO: quite inefficient with the inner trimming & splitting.
-     * TODO: Also has a latent bug: uses "startsWith" to match the name, which can false-positive.
-     * TODO: The doc says that for a null name you get the first param, but you get the header.
-     *    Should probably just fix the doc, but if other code assumes that behavior, fix the code.
-     * TODO: Need to decode %-escaped strings, as in: filename="ab%22d".
-     *       ('+' -> ' ' conversion too? check RFC)
-     *
-     * @param header
-     * @param name
-     * @return the entire header (if name=null), the found parameter, or null
-     */
-    public static String getHeaderParameter(String header, String name) {
-        if (header == null) {
-            return null;
-        }
-        String[] parts = unfold(header).split(";");
-        if (name == null) {
-            return parts[0].trim();
-        }
-        String lowerCaseName = name.toLowerCase();
-        for (String part : parts) {
-            if (part.trim().toLowerCase().startsWith(lowerCaseName)) {
-                String[] parameterParts = part.split("=", 2);
-                if (parameterParts.length < 2) {
-                    return null;
-                }
-                String parameter = parameterParts[1].trim();
-                if (parameter.startsWith("\"") && parameter.endsWith("\"")) {
-                    return parameter.substring(1, parameter.length() - 1);
-                } else {
-                    return parameter;
-                }
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Reads the Part's body and returns a String based on any charset conversion that needed
-     * to be done.
-     * @param part The part containing a body
-     * @return a String containing the converted text in the body, or null if there was no text
-     * or an error during conversion.
-     */
-    public static String getTextFromPart(Part part) {
-        try {
-            if (part != null && part.getBody() != null) {
-                InputStream in = part.getBody().getInputStream();
-                String mimeType = part.getMimeType();
-                if (mimeType != null && MimeUtility.mimeTypeMatches(mimeType, "text/*")) {
-                    /*
-                     * Now we read the part into a buffer for further processing. Because
-                     * the stream is now wrapped we'll remove any transfer encoding at this point.
-                     */
-                    ByteArrayOutputStream out = new ByteArrayOutputStream();
-                    IOUtils.copy(in, out);
-                    in.close();
-                    in = null;      // we want all of our memory back, and close might not release
-
-                    /*
-                     * We've got a text part, so let's see if it needs to be processed further.
-                     */
-                    String charset = getHeaderParameter(part.getContentType(), "charset");
-                    if (charset != null) {
-                        /*
-                         * See if there is conversion from the MIME charset to the Java one.
-                         */
-                        charset = CharsetUtil.toJavaCharset(charset);
-                    }
-                    /*
-                     * No encoding, so use us-ascii, which is the standard.
-                     */
-                    if (charset == null) {
-                        charset = "ASCII";
-                    }
-                    /*
-                     * Convert and return as new String
-                     */
-                    String result = out.toString(charset);
-                    out.close();
-                    return result;
-                }
-            }
-
-        }
-        catch (OutOfMemoryError oom) {
-            /*
-             * If we are not able to process the body there's nothing we can do about it. Return
-             * null and let the upper layers handle the missing content.
-             */
-            VvmLog.e(LOG_TAG, "Unable to getTextFromPart " + oom.toString());
-        }
-        catch (Exception e) {
-            /*
-             * If we are not able to process the body there's nothing we can do about it. Return
-             * null and let the upper layers handle the missing content.
-             */
-            VvmLog.e(LOG_TAG, "Unable to getTextFromPart " + e.toString());
-        }
-        return null;
-    }
-
-    /**
-     * Returns true if the given mimeType matches the matchAgainst specification.  The comparison
-     * ignores case and the matchAgainst string may include "*" for a wildcard (e.g. "image/*").
-     *
-     * @param mimeType A MIME type to check.
-     * @param matchAgainst A MIME type to check against. May include wildcards.
-     * @return true if the mimeType matches
-     */
-    public static boolean mimeTypeMatches(String mimeType, String matchAgainst) {
-        Pattern p = Pattern.compile(matchAgainst.replaceAll("\\*", "\\.\\*"),
-                Pattern.CASE_INSENSITIVE);
-        return p.matcher(mimeType).matches();
-    }
-
-    /**
-     * Returns true if the given mimeType matches any of the matchAgainst specifications.  The
-     * comparison ignores case and the matchAgainst strings may include "*" for a wildcard
-     * (e.g. "image/*").
-     *
-     * @param mimeType A MIME type to check.
-     * @param matchAgainst An array of MIME types to check against. May include wildcards.
-     * @return true if the mimeType matches any of the matchAgainst strings
-     */
-    public static boolean mimeTypeMatches(String mimeType, String[] matchAgainst) {
-        for (String matchType : matchAgainst) {
-            if (mimeTypeMatches(mimeType, matchType)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Given an input stream and a transfer encoding, return a wrapped input stream for that
-     * encoding (or the original if none is required)
-     * @param in the input stream
-     * @param contentTransferEncoding the content transfer encoding
-     * @return a properly wrapped stream
-     */
-    public static InputStream getInputStreamForContentTransferEncoding(InputStream in,
-            String contentTransferEncoding) {
-        if (contentTransferEncoding != null) {
-            contentTransferEncoding =
-                MimeUtility.getHeaderParameter(contentTransferEncoding, null);
-            if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) {
-                in = new QuotedPrintableInputStream(in);
-            }
-            else if ("base64".equalsIgnoreCase(contentTransferEncoding)) {
-                in = new Base64InputStream(in, Base64.DEFAULT);
-            }
-        }
-        return in;
-    }
-
-    /**
-     * Removes any content transfer encoding from the stream and returns a Body.
-     */
-    public static Body decodeBody(InputStream in, String contentTransferEncoding)
-            throws IOException {
-        /*
-         * We'll remove any transfer encoding by wrapping the stream.
-         */
-        in = getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
-        BinaryTempFileBody tempBody = new BinaryTempFileBody();
-        OutputStream out = tempBody.getOutputStream();
-        try {
-            IOUtils.copy(in, out);
-        } catch (Base64DataException bde) {
-            // TODO Need to fix this somehow
-            //String warning = "\n\n" + Email.getMessageDecodeErrorString();
-            //out.write(warning.getBytes());
-        } finally {
-            out.close();
-        }
-        return tempBody;
-    }
-
-    /**
-     * Recursively scan a Part (usually a Message) and sort out which of its children will be
-     * "viewable" and which will be attachments.
-     *
-     * @param part The part to be broken down
-     * @param viewables This arraylist will be populated with all parts that appear to be
-     * the "message" (e.g. text/plain & text/html)
-     * @param attachments This arraylist will be populated with all parts that appear to be
-     * attachments (including inlines)
-     * @throws MessagingException
-     */
-    public static void collectParts(Part part, ArrayList<Part> viewables,
-            ArrayList<Part> attachments) throws MessagingException {
-        String disposition = part.getDisposition();
-        String dispositionType = MimeUtility.getHeaderParameter(disposition, null);
-        // If a disposition is not specified, default to "inline"
-        boolean inline =
-                TextUtils.isEmpty(dispositionType) || "inline".equalsIgnoreCase(dispositionType);
-        // The lower-case mime type
-        String mimeType = part.getMimeType().toLowerCase();
-
-        if (part.getBody() instanceof Multipart) {
-            // If the part is Multipart but not alternative it's either mixed or
-            // something we don't know about, which means we treat it as mixed
-            // per the spec. We just process its pieces recursively.
-            MimeMultipart mp = (MimeMultipart)part.getBody();
-            boolean foundHtml = false;
-            if (mp.getSubTypeForTest().equals("alternative")) {
-                for (int i = 0; i < mp.getCount(); i++) {
-                    if (mp.getBodyPart(i).isMimeType("text/html")) {
-                        foundHtml = true;
-                        break;
-                    }
-                }
-            }
-            for (int i = 0; i < mp.getCount(); i++) {
-                // See if we have text and html
-                BodyPart bp = mp.getBodyPart(i);
-                // If there's html, don't bother loading text
-                if (foundHtml && bp.isMimeType("text/plain")) {
-                    continue;
-                }
-                collectParts(bp, viewables, attachments);
-            }
-        } else if (part.getBody() instanceof Message) {
-            // If the part is an embedded message we just continue to process
-            // it, pulling any viewables or attachments into the running list.
-            Message message = (Message)part.getBody();
-            collectParts(message, viewables, attachments);
-        } else if (inline && (mimeType.startsWith("text") || (mimeType.startsWith("image")))) {
-            // We'll treat text and images as viewables
-            viewables.add(part);
-        } else {
-            // Everything else is an attachment.
-            attachments.add(part);
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/mail/internet/TextBody.java b/java/com/android/voicemailomtp/mail/internet/TextBody.java
deleted file mode 100644
index 578193e..0000000
--- a/java/com/android/voicemailomtp/mail/internet/TextBody.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.internet;
-
-import android.util.Base64;
-
-import com.android.voicemailomtp.mail.Body;
-import com.android.voicemailomtp.mail.MessagingException;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-
-public class TextBody implements Body {
-    String mBody;
-
-    public TextBody(String body) {
-        this.mBody = body;
-    }
-
-    @Override
-    public void writeTo(OutputStream out) throws IOException, MessagingException {
-        byte[] bytes = mBody.getBytes("UTF-8");
-        out.write(Base64.encode(bytes, Base64.CRLF));
-    }
-
-    /**
-     * Get the text of the body in it's unencoded format.
-     * @return
-     */
-    public String getText() {
-        return mBody;
-    }
-
-    /**
-     * Returns an InputStream that reads this body's text in UTF-8 format.
-     */
-    @Override
-    public InputStream getInputStream() throws MessagingException {
-        try {
-            byte[] b = mBody.getBytes("UTF-8");
-            return new ByteArrayInputStream(b);
-        }
-        catch (UnsupportedEncodingException usee) {
-            return null;
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/mail/store/ImapConnection.java b/java/com/android/voicemailomtp/mail/store/ImapConnection.java
deleted file mode 100644
index 61dcf12..0000000
--- a/java/com/android/voicemailomtp/mail/store/ImapConnection.java
+++ /dev/null
@@ -1,413 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.store;
-
-import android.util.ArraySet;
-import android.util.Base64;
-import com.android.voicemailomtp.mail.AuthenticationFailedException;
-import com.android.voicemailomtp.mail.CertificateValidationException;
-import com.android.voicemailomtp.mail.MailTransport;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.store.ImapStore.ImapException;
-import com.android.voicemailomtp.mail.store.imap.DigestMd5Utils;
-import com.android.voicemailomtp.mail.store.imap.ImapConstants;
-import com.android.voicemailomtp.mail.store.imap.ImapResponse;
-import com.android.voicemailomtp.mail.store.imap.ImapResponseParser;
-import com.android.voicemailomtp.mail.store.imap.ImapUtility;
-import com.android.voicemailomtp.mail.utils.LogUtils;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.VvmLog;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
-import javax.net.ssl.SSLException;
-
-/**
- * A cacheable class that stores the details for a single IMAP connection.
- */
-public class ImapConnection {
-    private final String TAG = "ImapConnection";
-
-    private String mLoginPhrase;
-    private ImapStore mImapStore;
-    private MailTransport mTransport;
-    private ImapResponseParser mParser;
-    private Set<String> mCapabilities = new ArraySet<>();
-
-    static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
-
-    /**
-     * Next tag to use.  All connections associated to the same ImapStore instance share the same
-     * counter to make tests simpler.
-     * (Some of the tests involve multiple connections but only have a single counter to track the
-     * tag.)
-     */
-    private final AtomicInteger mNextCommandTag = new AtomicInteger(0);
-
-    ImapConnection(ImapStore store) {
-        setStore(store);
-    }
-
-    void setStore(ImapStore store) {
-        // TODO: maybe we should throw an exception if the connection is not closed here,
-        // if it's not currently closed, then we won't reopen it, so if the credentials have
-        // changed, the connection will not be reestablished.
-        mImapStore = store;
-        mLoginPhrase = null;
-    }
-
-    /**
-     * Generates and returns the phrase to be used for authentication. This will be a LOGIN with
-     * username and password.
-     *
-     * @return the login command string to sent to the IMAP server
-     */
-    String getLoginPhrase() {
-        if (mLoginPhrase == null) {
-            if (mImapStore.getUsername() != null && mImapStore.getPassword() != null) {
-                // build the LOGIN string once (instead of over-and-over again.)
-                // apply the quoting here around the built-up password
-                mLoginPhrase = ImapConstants.LOGIN + " " + mImapStore.getUsername() + " "
-                        + ImapUtility.imapQuoted(mImapStore.getPassword());
-            }
-        }
-        return mLoginPhrase;
-    }
-
-    public void open() throws IOException, MessagingException {
-        if (mTransport != null && mTransport.isOpen()) {
-            return;
-        }
-
-        try {
-            // copy configuration into a clean transport, if necessary
-            if (mTransport == null) {
-                mTransport = mImapStore.cloneTransport();
-            }
-
-            mTransport.open();
-
-            createParser();
-
-            // The server should greet us with something like
-            // * OK IMAP4rev1 Server
-            // consume the response before doing anything else.
-            ImapResponse response = mParser.readResponse(false);
-            if (!response.isOk()) {
-                mImapStore.getImapHelper()
-                        .handleEvent(OmtpEvents.DATA_INVALID_INITIAL_SERVER_RESPONSE);
-                throw new MessagingException(
-                        MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR,
-                        "Invalid server initial response");
-            }
-
-            queryCapability();
-
-            maybeDoStartTls();
-
-            // LOGIN
-            doLogin();
-        } catch (SSLException e) {
-            LogUtils.d(TAG, "SSLException ", e);
-            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_SSL_EXCEPTION);
-            throw new CertificateValidationException(e.getMessage(), e);
-        } catch (IOException ioe) {
-            LogUtils.d(TAG, "IOException", ioe);
-            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_IOE_ON_OPEN);
-            throw ioe;
-        } finally {
-            destroyResponses();
-        }
-    }
-
-    void logout() {
-        try {
-            sendCommand(ImapConstants.LOGOUT, false);
-            if (!mParser.readResponse(true).is(0, ImapConstants.BYE)) {
-                VvmLog.e(TAG, "Server did not respond LOGOUT with BYE");
-            }
-            if (!mParser.readResponse(false).isOk()) {
-                VvmLog.e(TAG, "Server did not respond OK after LOGOUT");
-            }
-        } catch (IOException | MessagingException e) {
-            VvmLog.e(TAG, "Error while logging out:" + e);
-        }
-    }
-
-    /**
-     * Closes the connection and releases all resources. This connection can not be used again
-     * until {@link #setStore(ImapStore)} is called.
-     */
-    void close() {
-        if (mTransport != null) {
-            logout();
-            mTransport.close();
-            mTransport = null;
-        }
-        destroyResponses();
-        mParser = null;
-        mImapStore = null;
-    }
-
-    /**
-     * Attempts to convert the connection into secure connection.
-     */
-    private void maybeDoStartTls() throws IOException, MessagingException {
-        // STARTTLS is required in the OMTP standard but not every implementation support it.
-        // Make sure the server does have this capability
-        if (hasCapability(ImapConstants.CAPABILITY_STARTTLS)) {
-            executeSimpleCommand(ImapConstants.STARTTLS);
-            mTransport.reopenTls();
-            createParser();
-            // The cached capabilities should be refreshed after TLS is established.
-            queryCapability();
-        }
-    }
-
-    /**
-     * Logs into the IMAP server
-     */
-    private void doLogin() throws IOException, MessagingException, AuthenticationFailedException {
-        try {
-            if (mCapabilities.contains(ImapConstants.CAPABILITY_AUTH_DIGEST_MD5)) {
-                doDigestMd5Auth();
-            } else {
-                executeSimpleCommand(getLoginPhrase(), true);
-            }
-        } catch (ImapException ie) {
-            LogUtils.d(TAG, "ImapException", ie);
-            String status = ie.getStatus();
-            String statusMessage = ie.getStatusMessage();
-            String alertText = ie.getAlertText();
-
-            if (ImapConstants.NO.equals(status)) {
-                switch (statusMessage) {
-                    case ImapConstants.NO_UNKNOWN_USER:
-                        mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_USER);
-                        break;
-                    case ImapConstants.NO_UNKNOWN_CLIENT:
-                        mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_DEVICE);
-                        break;
-                    case ImapConstants.NO_INVALID_PASSWORD:
-                        mImapStore.getImapHelper()
-                                .handleEvent(OmtpEvents.DATA_AUTH_INVALID_PASSWORD);
-                        break;
-                    case ImapConstants.NO_MAILBOX_NOT_INITIALIZED:
-                        mImapStore.getImapHelper()
-                                .handleEvent(OmtpEvents.DATA_AUTH_MAILBOX_NOT_INITIALIZED);
-                        break;
-                    case ImapConstants.NO_SERVICE_IS_NOT_PROVISIONED:
-                        mImapStore.getImapHelper()
-                                .handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_PROVISIONED);
-                        break;
-                    case ImapConstants.NO_SERVICE_IS_NOT_ACTIVATED:
-                        mImapStore.getImapHelper()
-                                .handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_ACTIVATED);
-                        break;
-                    case ImapConstants.NO_USER_IS_BLOCKED:
-                        mImapStore.getImapHelper()
-                                .handleEvent(OmtpEvents.DATA_AUTH_USER_IS_BLOCKED);
-                        break;
-                    case ImapConstants.NO_APPLICATION_ERROR:
-                        mImapStore.getImapHelper()
-                                .handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE);
-                    default:
-                        mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_BAD_IMAP_CREDENTIAL);
-                }
-                throw new AuthenticationFailedException(alertText, ie);
-            }
-
-            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE);
-            throw new MessagingException(alertText, ie);
-        }
-    }
-
-    private void doDigestMd5Auth() throws IOException, MessagingException {
-
-        //  Initiate the authentication.
-        //  The server will issue us a challenge, asking to run MD5 on the nonce with our password
-        //  and other data, including the cnonce we randomly generated.
-        //
-        //  C: a AUTHENTICATE DIGEST-MD5
-        //  S: (BASE64) realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",qop="auth",
-        //             algorithm=md5-sess,charset=utf-8
-        List<ImapResponse> responses = executeSimpleCommand(
-            ImapConstants.AUTHENTICATE + " " + ImapConstants.AUTH_DIGEST_MD5);
-        String decodedChallenge = decodeBase64(responses.get(0).getStringOrEmpty(0).getString());
-
-        Map<String, String> challenge = DigestMd5Utils.parseDigestMessage(decodedChallenge);
-        DigestMd5Utils.Data data = new DigestMd5Utils.Data(mImapStore, mTransport, challenge);
-
-        String response = data.createResponse();
-        //  Respond to the challenge. If the server accepts it, it will reply a response-auth which
-        //  is the MD5 of our password and the cnonce we've provided, to prove the server does know
-        //  the password.
-        //
-        //  C: (BASE64) charset=utf-8,username="chris",realm="elwood.innosoft.com",
-        //              nonce="OA6MG9tEQGm2hh",nc=00000001,cnonce="OA6MHXh6VqTrRk",
-        //              digest-uri="imap/elwood.innosoft.com",
-        //              response=d388dad90d4bbd760a152321f2143af7,qop=auth
-        //  S: (BASE64) rspauth=ea40f60335c427b5527b84dbabcdfffd
-
-        responses = executeContinuationResponse(encodeBase64(response), true);
-
-        // Verify response-auth.
-        // If failed verifyResponseAuth() will throw a MessagingException, terminating the
-        // connection
-        String decodedResponseAuth = decodeBase64(responses.get(0).getStringOrEmpty(0).getString());
-        data.verifyResponseAuth(decodedResponseAuth);
-
-        //  Send a empty response to indicate we've accepted the response-auth
-        //
-        //  C: (empty)
-        //  S: a OK User logged in
-        executeContinuationResponse("", false);
-
-    }
-
-    private static String decodeBase64(String string) {
-        return new String(Base64.decode(string, Base64.DEFAULT));
-    }
-
-    private static String encodeBase64(String string) {
-        return Base64.encodeToString(string.getBytes(), Base64.NO_WRAP);
-    }
-
-    private void queryCapability() throws IOException, MessagingException {
-        List<ImapResponse> responses = executeSimpleCommand(ImapConstants.CAPABILITY);
-        mCapabilities.clear();
-        Set<String> disabledCapabilities = mImapStore.getImapHelper().getConfig()
-                .getDisabledCapabilities();
-        for (ImapResponse response : responses) {
-            if (response.isTagged()) {
-                continue;
-            }
-            for (int i = 0; i < response.size(); i++) {
-                String capability = response.getStringOrEmpty(i).getString();
-                if (disabledCapabilities != null) {
-                    if (!disabledCapabilities.contains(capability)) {
-                        mCapabilities.add(capability);
-                    }
-                } else {
-                    mCapabilities.add(capability);
-                }
-            }
-        }
-
-        LogUtils.d(TAG, "Capabilities: " + mCapabilities.toString());
-    }
-
-    private boolean hasCapability(String capability) {
-        return mCapabilities.contains(capability);
-    }
-    /**
-     * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and
-     * set it to {@link #mParser}.
-     *
-     * If we already have an {@link ImapResponseParser}, we
-     * {@link #destroyResponses()} and throw it away.
-     */
-    private void createParser() {
-        destroyResponses();
-        mParser = new ImapResponseParser(mTransport.getInputStream());
-    }
-
-
-    public void destroyResponses() {
-        if (mParser != null) {
-            mParser.destroyResponses();
-        }
-    }
-
-    public ImapResponse readResponse() throws IOException, MessagingException {
-        return mParser.readResponse(false);
-    }
-
-    public List<ImapResponse> executeSimpleCommand(String command)
-            throws IOException, MessagingException{
-        return executeSimpleCommand(command, false);
-    }
-
-    /**
-     * Send a single command to the server.  The command will be preceded by an IMAP command
-     * tag and followed by \r\n (caller need not supply them).
-     * Execute a simple command at the server, a simple command being one that is sent in a single
-     * line of text
-     *
-     * @param command the command to send to the server
-     * @param sensitive whether the command should be redacted in logs (used for login)
-     * @return a list of ImapResponses
-     * @throws IOException
-     * @throws MessagingException
-     */
-    public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
-            throws IOException, MessagingException {
-        // TODO: It may be nice to catch IOExceptions and close the connection here.
-        // Currently, we expect callers to do that, but if they fail to we'll be in a broken state.
-        sendCommand(command, sensitive);
-        return getCommandResponses();
-    }
-
-    public String sendCommand(String command, boolean sensitive)
-            throws IOException, MessagingException {
-        open();
-
-        if (mTransport == null) {
-            throw new IOException("Null transport");
-        }
-        String tag = Integer.toString(mNextCommandTag.incrementAndGet());
-        String commandToSend = tag + " " + command;
-        mTransport.writeLine(commandToSend, (sensitive ? IMAP_REDACTED_LOG : command));
-        return tag;
-    }
-
-    List<ImapResponse> executeContinuationResponse(String response, boolean sensitive)
-            throws IOException, MessagingException {
-        mTransport.writeLine(response, (sensitive ? IMAP_REDACTED_LOG : response));
-        return getCommandResponses();
-    }
-
-    /**
-     * Read and return all of the responses from the most recent command sent to the server
-     *
-     * @return a list of ImapResponses
-     * @throws IOException
-     * @throws MessagingException
-     */
-    List<ImapResponse> getCommandResponses()
-            throws IOException, MessagingException {
-        final List<ImapResponse> responses = new ArrayList<ImapResponse>();
-        ImapResponse response;
-        do {
-            response = mParser.readResponse(false);
-            responses.add(response);
-        } while (!(response.isTagged() || response.isContinuationRequest()));
-
-        if (!(response.isOk() || response.isContinuationRequest())) {
-            final String toString = response.toString();
-            final String status = response.getStatusOrEmpty().getString();
-            final String statusMessage = response.getStatusResponseTextOrEmpty().getString();
-            final String alert = response.getAlertTextOrEmpty().getString();
-            final String responseCode = response.getResponseCodeOrEmpty().getString();
-            destroyResponses();
-            throw new ImapException(toString, status, statusMessage, alert, responseCode);
-        }
-        return responses;
-    }
-}
diff --git a/java/com/android/voicemailomtp/mail/store/ImapFolder.java b/java/com/android/voicemailomtp/mail/store/ImapFolder.java
deleted file mode 100644
index eca3498..0000000
--- a/java/com/android/voicemailomtp/mail/store/ImapFolder.java
+++ /dev/null
@@ -1,784 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.store;
-
-import android.content.Context;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.text.TextUtils;
-import android.util.Base64DataException;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.mail.AuthenticationFailedException;
-import com.android.voicemailomtp.mail.Body;
-import com.android.voicemailomtp.mail.FetchProfile;
-import com.android.voicemailomtp.mail.Flag;
-import com.android.voicemailomtp.mail.Message;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.Part;
-import com.android.voicemailomtp.mail.internet.BinaryTempFileBody;
-import com.android.voicemailomtp.mail.internet.MimeBodyPart;
-import com.android.voicemailomtp.mail.internet.MimeHeader;
-import com.android.voicemailomtp.mail.internet.MimeMultipart;
-import com.android.voicemailomtp.mail.internet.MimeUtility;
-import com.android.voicemailomtp.mail.store.ImapStore.ImapException;
-import com.android.voicemailomtp.mail.store.ImapStore.ImapMessage;
-import com.android.voicemailomtp.mail.store.imap.ImapConstants;
-import com.android.voicemailomtp.mail.store.imap.ImapElement;
-import com.android.voicemailomtp.mail.store.imap.ImapList;
-import com.android.voicemailomtp.mail.store.imap.ImapResponse;
-import com.android.voicemailomtp.mail.store.imap.ImapString;
-import com.android.voicemailomtp.mail.utils.LogUtils;
-import com.android.voicemailomtp.mail.utils.Utility;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Locale;
-
-public class ImapFolder {
-    private static final String TAG = "ImapFolder";
-    private final static String[] PERMANENT_FLAGS =
-        { Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED };
-    private static final int COPY_BUFFER_SIZE = 16*1024;
-
-    private final ImapStore mStore;
-    private final String mName;
-    private int mMessageCount = -1;
-    private ImapConnection mConnection;
-    private String mMode;
-    private boolean mExists;
-    /** A set of hashes that can be used to track dirtiness */
-    Object mHash[];
-
-    public static final String MODE_READ_ONLY = "mode_read_only";
-    public static final String MODE_READ_WRITE = "mode_read_write";
-
-    public ImapFolder(ImapStore store, String name) {
-        mStore = store;
-        mName = name;
-    }
-
-    /**
-     * Callback for each message retrieval.
-     */
-    public interface MessageRetrievalListener {
-        public void messageRetrieved(Message message);
-    }
-
-    private void destroyResponses() {
-        if (mConnection != null) {
-            mConnection.destroyResponses();
-        }
-    }
-
-    public void open(String mode) throws MessagingException {
-        try {
-            if (isOpen()) {
-                throw new AssertionError("Duplicated open on ImapFolder");
-            }
-            synchronized (this) {
-                mConnection = mStore.getConnection();
-            }
-            // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
-            // $MDNSent)
-            // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
-            // NonJunk $MDNSent \*)] Flags permitted.
-            // * 23 EXISTS
-            // * 0 RECENT
-            // * OK [UIDVALIDITY 1125022061] UIDs valid
-            // * OK [UIDNEXT 57576] Predicted next UID
-            // 2 OK [READ-WRITE] Select completed.
-            try {
-                doSelect();
-            } catch (IOException ioe) {
-                throw ioExceptionHandler(mConnection, ioe);
-            } finally {
-                destroyResponses();
-            }
-        } catch (AuthenticationFailedException e) {
-            // Don't cache this connection, so we're forced to try connecting/login again
-            mConnection = null;
-            close(false);
-            throw e;
-        } catch (MessagingException e) {
-            mExists = false;
-            close(false);
-            throw e;
-        }
-    }
-
-    public boolean isOpen() {
-        return mExists && mConnection != null;
-    }
-
-    public String getMode() {
-        return mMode;
-    }
-
-    public void close(boolean expunge) {
-        if (expunge) {
-            try {
-                expunge();
-            } catch (MessagingException e) {
-                LogUtils.e(TAG, e, "Messaging Exception");
-            }
-        }
-        mMessageCount = -1;
-        synchronized (this) {
-            mConnection = null;
-        }
-    }
-
-    public int getMessageCount() {
-        return mMessageCount;
-    }
-
-    String[] getSearchUids(List<ImapResponse> responses) {
-        // S: * SEARCH 2 3 6
-        final ArrayList<String> uids = new ArrayList<String>();
-        for (ImapResponse response : responses) {
-            if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
-                continue;
-            }
-            // Found SEARCH response data
-            for (int i = 1; i < response.size(); i++) {
-                ImapString s = response.getStringOrEmpty(i);
-                if (s.isString()) {
-                    uids.add(s.getString());
-                }
-            }
-        }
-        return uids.toArray(Utility.EMPTY_STRINGS);
-    }
-
-    @VisibleForTesting
-    String[] searchForUids(String searchCriteria) throws MessagingException {
-        checkOpen();
-        try {
-            try {
-                final String command = ImapConstants.UID_SEARCH + " " + searchCriteria;
-                final String[] result = getSearchUids(mConnection.executeSimpleCommand(command));
-                LogUtils.d(TAG, "searchForUids '" + searchCriteria + "' results: " +
-                        result.length);
-                return result;
-            } catch (ImapException me) {
-                LogUtils.d(TAG, "ImapException in search: " + searchCriteria, me);
-                return Utility.EMPTY_STRINGS; // Not found
-            } catch (IOException ioe) {
-                LogUtils.d(TAG, "IOException in search: " + searchCriteria, ioe);
-                mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
-                throw ioExceptionHandler(mConnection, ioe);
-            }
-        } finally {
-            destroyResponses();
-        }
-    }
-
-    @Nullable
-    public Message getMessage(String uid) throws MessagingException {
-        checkOpen();
-
-        final String[] uids = searchForUids(ImapConstants.UID + " " + uid);
-        for (int i = 0; i < uids.length; i++) {
-            if (uids[i].equals(uid)) {
-                return new ImapMessage(uid, this);
-            }
-        }
-        LogUtils.e(TAG, "UID " + uid + " not found on server");
-        return null;
-    }
-
-    @VisibleForTesting
-    protected static boolean isAsciiString(String str) {
-        int len = str.length();
-        for (int i = 0; i < len; i++) {
-            char c = str.charAt(i);
-            if (c >= 128) return false;
-        }
-        return true;
-    }
-
-    public Message[] getMessages(String[] uids) throws MessagingException {
-        if (uids == null) {
-            uids = searchForUids("1:* NOT DELETED");
-        }
-        return getMessagesInternal(uids);
-    }
-
-    public Message[] getMessagesInternal(String[] uids) {
-        final ArrayList<Message> messages = new ArrayList<Message>(uids.length);
-        for (int i = 0; i < uids.length; i++) {
-            final String uid = uids[i];
-            final ImapMessage message = new ImapMessage(uid, this);
-            messages.add(message);
-        }
-        return messages.toArray(Message.EMPTY_ARRAY);
-    }
-
-    public void fetch(Message[] messages, FetchProfile fp,
-            MessageRetrievalListener listener) throws MessagingException {
-        try {
-            fetchInternal(messages, fp, listener);
-        } catch (RuntimeException e) { // Probably a parser error.
-            LogUtils.w(TAG, "Exception detected: " + e.getMessage());
-            throw e;
-        }
-    }
-
-    public void fetchInternal(Message[] messages, FetchProfile fp,
-            MessageRetrievalListener listener) throws MessagingException {
-        if (messages.length == 0) {
-            return;
-        }
-        checkOpen();
-        HashMap<String, Message> messageMap = new HashMap<String, Message>();
-        for (Message m : messages) {
-            messageMap.put(m.getUid(), m);
-        }
-
-        /*
-         * Figure out what command we are going to run:
-         * FLAGS     - UID FETCH (FLAGS)
-         * ENVELOPE  - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[
-         *                            HEADER.FIELDS (date subject from content-type to cc)])
-         * STRUCTURE - UID FETCH (BODYSTRUCTURE)
-         * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned
-         * BODY      - UID FETCH (BODY.PEEK[])
-         * Part      - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID
-         */
-
-        final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>();
-
-        fetchFields.add(ImapConstants.UID);
-        if (fp.contains(FetchProfile.Item.FLAGS)) {
-            fetchFields.add(ImapConstants.FLAGS);
-        }
-        if (fp.contains(FetchProfile.Item.ENVELOPE)) {
-            fetchFields.add(ImapConstants.INTERNALDATE);
-            fetchFields.add(ImapConstants.RFC822_SIZE);
-            fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS);
-        }
-        if (fp.contains(FetchProfile.Item.STRUCTURE)) {
-            fetchFields.add(ImapConstants.BODYSTRUCTURE);
-        }
-
-        if (fp.contains(FetchProfile.Item.BODY_SANE)) {
-            fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE);
-        }
-        if (fp.contains(FetchProfile.Item.BODY)) {
-            fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK);
-        }
-
-        // TODO Why are we only fetching the first part given?
-        final Part fetchPart = fp.getFirstPart();
-        if (fetchPart != null) {
-            final String[] partIds =
-                    fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
-            // TODO Why can a single part have more than one Id? And why should we only fetch
-            // the first id if there are more than one?
-            if (partIds != null) {
-                fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE
-                        + "[" + partIds[0] + "]");
-            }
-        }
-
-        try {
-            mConnection.sendCommand(String.format(Locale.US,
-                    ImapConstants.UID_FETCH + " %s (%s)", ImapStore.joinMessageUids(messages),
-                    Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')
-            ), false);
-            ImapResponse response;
-            do {
-                response = null;
-                try {
-                    response = mConnection.readResponse();
-
-                    if (!response.isDataResponse(1, ImapConstants.FETCH)) {
-                        continue; // Ignore
-                    }
-                    final ImapList fetchList = response.getListOrEmpty(2);
-                    final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID)
-                            .getString();
-                    if (TextUtils.isEmpty(uid)) continue;
-
-                    ImapMessage message = (ImapMessage) messageMap.get(uid);
-                    if (message == null) continue;
-
-                    if (fp.contains(FetchProfile.Item.FLAGS)) {
-                        final ImapList flags =
-                            fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS);
-                        for (int i = 0, count = flags.size(); i < count; i++) {
-                            final ImapString flag = flags.getStringOrEmpty(i);
-                            if (flag.is(ImapConstants.FLAG_DELETED)) {
-                                message.setFlagInternal(Flag.DELETED, true);
-                            } else if (flag.is(ImapConstants.FLAG_ANSWERED)) {
-                                message.setFlagInternal(Flag.ANSWERED, true);
-                            } else if (flag.is(ImapConstants.FLAG_SEEN)) {
-                                message.setFlagInternal(Flag.SEEN, true);
-                            } else if (flag.is(ImapConstants.FLAG_FLAGGED)) {
-                                message.setFlagInternal(Flag.FLAGGED, true);
-                            }
-                        }
-                    }
-                    if (fp.contains(FetchProfile.Item.ENVELOPE)) {
-                        final Date internalDate = fetchList.getKeyedStringOrEmpty(
-                                ImapConstants.INTERNALDATE).getDateOrNull();
-                        final int size = fetchList.getKeyedStringOrEmpty(
-                                ImapConstants.RFC822_SIZE).getNumberOrZero();
-                        final String header = fetchList.getKeyedStringOrEmpty(
-                                ImapConstants.BODY_BRACKET_HEADER, true).getString();
-
-                        message.setInternalDate(internalDate);
-                        message.setSize(size);
-                        message.parse(Utility.streamFromAsciiString(header));
-                    }
-                    if (fp.contains(FetchProfile.Item.STRUCTURE)) {
-                        ImapList bs = fetchList.getKeyedListOrEmpty(
-                                ImapConstants.BODYSTRUCTURE);
-                        if (!bs.isEmpty()) {
-                            try {
-                                parseBodyStructure(bs, message, ImapConstants.TEXT);
-                            } catch (MessagingException e) {
-                                LogUtils.v(TAG, e, "Error handling message");
-                                message.setBody(null);
-                            }
-                        }
-                    }
-                    if (fp.contains(FetchProfile.Item.BODY)
-                            || fp.contains(FetchProfile.Item.BODY_SANE)) {
-                        // Body is keyed by "BODY[]...".
-                        // Previously used "BODY[..." but this can be confused with "BODY[HEADER..."
-                        // TODO Should we accept "RFC822" as well??
-                        ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true);
-                        InputStream bodyStream = body.getAsStream();
-                        message.parse(bodyStream);
-                    }
-                    if (fetchPart != null) {
-                        InputStream bodyStream =
-                                fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream();
-                        String encodings[] = fetchPart.getHeader(
-                                MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING);
-
-                        String contentTransferEncoding = null;
-                        if (encodings != null && encodings.length > 0) {
-                            contentTransferEncoding = encodings[0];
-                        } else {
-                            // According to http://tools.ietf.org/html/rfc2045#section-6.1
-                            // "7bit" is the default.
-                            contentTransferEncoding = "7bit";
-                        }
-
-                        try {
-                            // TODO Don't create 2 temp files.
-                            // decodeBody creates BinaryTempFileBody, but we could avoid this
-                            // if we implement ImapStringBody.
-                            // (We'll need to share a temp file.  Protect it with a ref-count.)
-                            message.setBody(decodeBody(mStore.getContext(), bodyStream,
-                                    contentTransferEncoding, fetchPart.getSize(), listener));
-                        } catch(Exception e) {
-                            // TODO: Figure out what kinds of exceptions might actually be thrown
-                            // from here. This blanket catch-all is because we're not sure what to
-                            // do if we don't have a contentTransferEncoding, and we don't have
-                            // time to figure out what exceptions might be thrown.
-                            LogUtils.e(TAG, "Error fetching body %s", e);
-                        }
-                    }
-
-                    if (listener != null) {
-                        listener.messageRetrieved(message);
-                    }
-                } finally {
-                    destroyResponses();
-                }
-            } while (!response.isTagged());
-        } catch (IOException ioe) {
-            mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
-            throw ioExceptionHandler(mConnection, ioe);
-        }
-    }
-
-    /**
-     * Removes any content transfer encoding from the stream and returns a Body.
-     * This code is taken/condensed from MimeUtility.decodeBody
-     */
-    private static Body decodeBody(Context context,InputStream in, String contentTransferEncoding,
-            int size, MessageRetrievalListener listener) throws IOException {
-        // Get a properly wrapped input stream
-        in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
-        BinaryTempFileBody tempBody = new BinaryTempFileBody();
-        OutputStream out = tempBody.getOutputStream();
-        try {
-            byte[] buffer = new byte[COPY_BUFFER_SIZE];
-            int n = 0;
-            int count = 0;
-            while (-1 != (n = in.read(buffer))) {
-                out.write(buffer, 0, n);
-                count += n;
-            }
-        } catch (Base64DataException bde) {
-            String warning = "\n\nThere was an error while decoding the message.";
-            out.write(warning.getBytes());
-        } finally {
-            out.close();
-        }
-        return tempBody;
-    }
-
-    public String[] getPermanentFlags() {
-        return PERMANENT_FLAGS;
-    }
-
-    /**
-     * Handle any untagged responses that the caller doesn't care to handle themselves.
-     * @param responses
-     */
-    private void handleUntaggedResponses(List<ImapResponse> responses) {
-        for (ImapResponse response : responses) {
-            handleUntaggedResponse(response);
-        }
-    }
-
-    /**
-     * Handle an untagged response that the caller doesn't care to handle themselves.
-     * @param response
-     */
-    private void handleUntaggedResponse(ImapResponse response) {
-        if (response.isDataResponse(1, ImapConstants.EXISTS)) {
-            mMessageCount = response.getStringOrEmpty(0).getNumberOrZero();
-        }
-    }
-
-    private static void parseBodyStructure(ImapList bs, Part part, String id)
-            throws MessagingException {
-        if (bs.getElementOrNone(0).isList()) {
-            /*
-             * This is a multipart/*
-             */
-            MimeMultipart mp = new MimeMultipart();
-            for (int i = 0, count = bs.size(); i < count; i++) {
-                ImapElement e = bs.getElementOrNone(i);
-                if (e.isList()) {
-                    /*
-                     * For each part in the message we're going to add a new BodyPart and parse
-                     * into it.
-                     */
-                    MimeBodyPart bp = new MimeBodyPart();
-                    if (id.equals(ImapConstants.TEXT)) {
-                        parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1));
-
-                    } else {
-                        parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1));
-                    }
-                    mp.addBodyPart(bp);
-
-                } else {
-                    if (e.isString()) {
-                        mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US));
-                    }
-                    break; // Ignore the rest of the list.
-                }
-            }
-            part.setBody(mp);
-        } else {
-            /*
-             * This is a body. We need to add as much information as we can find out about
-             * it to the Part.
-             */
-
-            /*
-             body type
-             body subtype
-             body parameter parenthesized list
-             body id
-             body description
-             body encoding
-             body size
-             */
-
-            final ImapString type = bs.getStringOrEmpty(0);
-            final ImapString subType = bs.getStringOrEmpty(1);
-            final String mimeType =
-                    (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US);
-
-            final ImapList bodyParams = bs.getListOrEmpty(2);
-            final ImapString cid = bs.getStringOrEmpty(3);
-            final ImapString encoding = bs.getStringOrEmpty(5);
-            final int size = bs.getStringOrEmpty(6).getNumberOrZero();
-
-            if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) {
-                // A body type of type MESSAGE and subtype RFC822
-                // contains, immediately after the basic fields, the
-                // envelope structure, body structure, and size in
-                // text lines of the encapsulated message.
-                // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL,
-                //     [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL]
-                /*
-                 * This will be caught by fetch and handled appropriately.
-                 */
-                throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822
-                        + " not yet supported.");
-            }
-
-            /*
-             * Set the content type with as much information as we know right now.
-             */
-            final StringBuilder contentType = new StringBuilder(mimeType);
-
-            /*
-             * If there are body params we might be able to get some more information out
-             * of them.
-             */
-            for (int i = 1, count = bodyParams.size(); i < count; i += 2) {
-
-                // TODO We need to convert " into %22, but
-                // because MimeUtility.getHeaderParameter doesn't recognize it,
-                // we can't fix it for now.
-                contentType.append(String.format(";\n %s=\"%s\"",
-                        bodyParams.getStringOrEmpty(i - 1).getString(),
-                        bodyParams.getStringOrEmpty(i).getString()));
-            }
-
-            part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
-
-            // Extension items
-            final ImapList bodyDisposition;
-
-            if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) {
-                // If media-type is TEXT, 9th element might be: [body-fld-lines] := number
-                // So, if it's not a list, use 10th element.
-                // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.)
-                bodyDisposition = bs.getListOrEmpty(9);
-            } else {
-                bodyDisposition = bs.getListOrEmpty(8);
-            }
-
-            final StringBuilder contentDisposition = new StringBuilder();
-
-            if (bodyDisposition.size() > 0) {
-                final String bodyDisposition0Str =
-                        bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US);
-                if (!TextUtils.isEmpty(bodyDisposition0Str)) {
-                    contentDisposition.append(bodyDisposition0Str);
-                }
-
-                final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1);
-                if (!bodyDispositionParams.isEmpty()) {
-                    /*
-                     * If there is body disposition information we can pull some more
-                     * information about the attachment out.
-                     */
-                    for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) {
-
-                        // TODO We need to convert " into %22.  See above.
-                        contentDisposition.append(String.format(Locale.US, ";\n %s=\"%s\"",
-                                bodyDispositionParams.getStringOrEmpty(i - 1)
-                                        .getString().toLowerCase(Locale.US),
-                                bodyDispositionParams.getStringOrEmpty(i).getString()));
-                    }
-                }
-            }
-
-            if ((size > 0)
-                    && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size")
-                            == null)) {
-                contentDisposition.append(String.format(Locale.US, ";\n size=%d", size));
-            }
-
-            if (contentDisposition.length() > 0) {
-                /*
-                 * Set the content disposition containing at least the size. Attachment
-                 * handling code will use this down the road.
-                 */
-                part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
-                        contentDisposition.toString());
-            }
-
-            /*
-             * Set the Content-Transfer-Encoding header. Attachment code will use this
-             * to parse the body.
-             */
-            if (!encoding.isEmpty()) {
-                part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING,
-                        encoding.getString());
-            }
-
-            /*
-             * Set the Content-ID header.
-             */
-            if (!cid.isEmpty()) {
-                part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString());
-            }
-
-            if (size > 0) {
-                if (part instanceof ImapMessage) {
-                    ((ImapMessage) part).setSize(size);
-                } else if (part instanceof MimeBodyPart) {
-                    ((MimeBodyPart) part).setSize(size);
-                } else {
-                    throw new MessagingException("Unknown part type " + part.toString());
-                }
-            }
-            part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
-        }
-
-    }
-
-    public Message[] expunge() throws MessagingException {
-        checkOpen();
-        try {
-            handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE));
-        } catch (IOException ioe) {
-            mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
-            throw ioExceptionHandler(mConnection, ioe);
-        } finally {
-            destroyResponses();
-        }
-        return null;
-    }
-
-    public void setFlags(Message[] messages, String[] flags, boolean value)
-            throws MessagingException {
-        checkOpen();
-
-        String allFlags = "";
-        if (flags.length > 0) {
-            StringBuilder flagList = new StringBuilder();
-            for (int i = 0, count = flags.length; i < count; i++) {
-                String flag = flags[i];
-                if (flag == Flag.SEEN) {
-                    flagList.append(" " + ImapConstants.FLAG_SEEN);
-                } else if (flag == Flag.DELETED) {
-                    flagList.append(" " + ImapConstants.FLAG_DELETED);
-                } else if (flag == Flag.FLAGGED) {
-                    flagList.append(" " + ImapConstants.FLAG_FLAGGED);
-                } else if (flag == Flag.ANSWERED) {
-                    flagList.append(" " + ImapConstants.FLAG_ANSWERED);
-                }
-            }
-            allFlags = flagList.substring(1);
-        }
-        try {
-            mConnection.executeSimpleCommand(String.format(Locale.US,
-                    ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)",
-                    ImapStore.joinMessageUids(messages),
-                    value ? "+" : "-",
-                    allFlags));
-
-        } catch (IOException ioe) {
-            mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
-            throw ioExceptionHandler(mConnection, ioe);
-        } finally {
-            destroyResponses();
-        }
-    }
-
-    /**
-     * Selects the folder for use. Before performing any operations on this folder, it
-     * must be selected.
-     */
-    private void doSelect() throws IOException, MessagingException {
-        final List<ImapResponse> responses = mConnection.executeSimpleCommand(
-                String.format(Locale.US, ImapConstants.SELECT + " \"%s\"", mName));
-
-        // Assume the folder is opened read-write; unless we are notified otherwise
-        mMode = MODE_READ_WRITE;
-        int messageCount = -1;
-        for (ImapResponse response : responses) {
-            if (response.isDataResponse(1, ImapConstants.EXISTS)) {
-                messageCount = response.getStringOrEmpty(0).getNumberOrZero();
-            } else if (response.isOk()) {
-                final ImapString responseCode = response.getResponseCodeOrEmpty();
-                if (responseCode.is(ImapConstants.READ_ONLY)) {
-                    mMode = MODE_READ_ONLY;
-                } else if (responseCode.is(ImapConstants.READ_WRITE)) {
-                    mMode = MODE_READ_WRITE;
-                }
-            } else if (response.isTagged()) { // Not OK
-                mStore.getImapHelper().handleEvent(OmtpEvents.DATA_MAILBOX_OPEN_FAILED);
-                throw new MessagingException("Can't open mailbox: "
-                        + response.getStatusResponseTextOrEmpty());
-            }
-        }
-        if (messageCount == -1) {
-            throw new MessagingException("Did not find message count during select");
-        }
-        mMessageCount = messageCount;
-        mExists = true;
-    }
-
-    public class Quota {
-
-        public final int occupied;
-        public final int total;
-
-        public Quota(int occupied, int total) {
-            this.occupied = occupied;
-            this.total = total;
-        }
-    }
-
-    public Quota getQuota() throws MessagingException {
-        try {
-            final List<ImapResponse> responses = mConnection.executeSimpleCommand(
-                    String.format(Locale.US, ImapConstants.GETQUOTAROOT + " \"%s\"", mName));
-
-            for (ImapResponse response : responses) {
-                if (!response.isDataResponse(0, ImapConstants.QUOTA)) {
-                    continue;
-                }
-                ImapList list = response.getListOrEmpty(2);
-                for (int i = 0; i < list.size(); i += 3) {
-                    if (!list.getStringOrEmpty(i).is("voice")) {
-                        continue;
-                    }
-                    return new Quota(
-                            list.getStringOrEmpty(i + 1).getNumber(-1),
-                            list.getStringOrEmpty(i + 2).getNumber(-1));
-                }
-            }
-        } catch (IOException ioe) {
-            mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
-            throw ioExceptionHandler(mConnection, ioe);
-        } finally {
-            destroyResponses();
-        }
-        return null;
-    }
-
-    private void checkOpen() throws MessagingException {
-        if (!isOpen()) {
-            throw new MessagingException("Folder " + mName + " is not open.");
-        }
-    }
-
-    private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
-        LogUtils.d(TAG, "IO Exception detected: ", ioe);
-        connection.close();
-        if (connection == mConnection) {
-            mConnection = null; // To prevent close() from returning the connection to the pool.
-            close(false);
-        }
-        return new MessagingException(MessagingException.IOERROR, "IO Error", ioe);
-    }
-
-    public Message createMessage(String uid) {
-        return new ImapMessage(uid, this);
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/ImapStore.java b/java/com/android/voicemailomtp/mail/store/ImapStore.java
deleted file mode 100644
index f3e0c09..0000000
--- a/java/com/android/voicemailomtp/mail/store/ImapStore.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store;
-
-import android.content.Context;
-import android.net.Network;
-
-import com.android.voicemailomtp.mail.MailTransport;
-import com.android.voicemailomtp.mail.Message;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.internet.MimeMessage;
-import com.android.voicemailomtp.imap.ImapHelper;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-public class ImapStore {
-    /**
-     * A global suggestion to Store implementors on how much of the body
-     * should be returned on FetchProfile.Item.BODY_SANE requests. We'll use 125k now.
-     */
-    public static final int FETCH_BODY_SANE_SUGGESTED_SIZE = (125 * 1024);
-    private final Context mContext;
-    private final ImapHelper mHelper;
-    private final String mUsername;
-    private final String mPassword;
-    private final MailTransport mTransport;
-    private ImapConnection mConnection;
-
-    public static final int FLAG_NONE         = 0x00;    // No flags
-    public static final int FLAG_SSL          = 0x01;    // Use SSL
-    public static final int FLAG_TLS          = 0x02;    // Use TLS
-    public static final int FLAG_AUTHENTICATE = 0x04;    // Use name/password for authentication
-    public static final int FLAG_TRUST_ALL    = 0x08;    // Trust all certificates
-    public static final int FLAG_OAUTH        = 0x10;    // Use OAuth for authentication
-
-    /**
-     * Contains all the information necessary to log into an imap server
-     */
-    public ImapStore(Context context, ImapHelper helper, String username, String password, int port,
-            String serverName, int flags, Network network) {
-        mContext = context;
-        mHelper = helper;
-        mUsername = username;
-        mPassword = password;
-        mTransport = new MailTransport(context, this.getImapHelper(),
-                network, serverName, port, flags);
-    }
-
-    public Context getContext() {
-        return mContext;
-    }
-
-    public ImapHelper getImapHelper() {
-        return mHelper;
-    }
-
-    public String getUsername() {
-        return mUsername;
-    }
-
-    public String getPassword() {
-        return mPassword;
-    }
-
-    /** Returns a clone of the transport associated with this store. */
-    MailTransport cloneTransport() {
-        return mTransport.clone();
-    }
-
-    /**
-     * Returns UIDs of Messages joined with "," as the separator.
-     */
-    static String joinMessageUids(Message[] messages) {
-        StringBuilder sb = new StringBuilder();
-        boolean notFirst = false;
-        for (Message m : messages) {
-            if (notFirst) {
-                sb.append(',');
-            }
-            sb.append(m.getUid());
-            notFirst = true;
-        }
-        return sb.toString();
-    }
-
-    static class ImapMessage extends MimeMessage {
-        private ImapFolder mFolder;
-
-        ImapMessage(String uid, ImapFolder folder) {
-            mUid = uid;
-            mFolder = folder;
-        }
-
-        public void setSize(int size) {
-            mSize = size;
-        }
-
-        @Override
-        public void parse(InputStream in) throws IOException, MessagingException {
-            super.parse(in);
-        }
-
-        public void setFlagInternal(String flag, boolean set) throws MessagingException {
-            super.setFlag(flag, set);
-        }
-
-        @Override
-        public void setFlag(String flag, boolean set) throws MessagingException {
-            super.setFlag(flag, set);
-            mFolder.setFlags(new Message[] { this }, new String[] { flag }, set);
-        }
-    }
-
-    static class ImapException extends MessagingException {
-        private static final long serialVersionUID = 1L;
-
-        private final String mStatus;
-        private final String mStatusMessage;
-        private final String mAlertText;
-        private final String mResponseCode;
-
-        public ImapException(String message, String status, String statusMessage, String alertText,
-                String responseCode) {
-            super(message);
-            mStatus = status;
-            mStatusMessage = statusMessage;
-            mAlertText = alertText;
-            mResponseCode = responseCode;
-        }
-
-        public String getStatus() {
-            return mStatus;
-        }
-
-        public String getStatusMessage() {
-            return mStatusMessage;
-        }
-
-        public String getAlertText() {
-            return mAlertText;
-        }
-
-        public String getResponseCode() {
-            return mResponseCode;
-        }
-    }
-
-    public void closeConnection() {
-        if (mConnection != null) {
-            mConnection.close();
-            mConnection = null;
-        }
-    }
-
-    public ImapConnection getConnection() {
-        if (mConnection == null) {
-            mConnection = new ImapConnection(this);
-        }
-        return mConnection;
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java b/java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java
deleted file mode 100644
index b78f552..0000000
--- a/java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java
+++ /dev/null
@@ -1,335 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-import android.annotation.TargetApi;
-import android.os.Build.VERSION_CODES;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.util.ArrayMap;
-import android.util.Base64;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.mail.MailTransport;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.store.ImapStore;
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Map;
-
-@SuppressWarnings("AndroidApiChecker") // Map.getOrDefault() is java8
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class DigestMd5Utils {
-
-    private static final String TAG = "DigestMd5Utils";
-
-    private static final String DIGEST_CHARSET = "CHARSET";
-    private static final String DIGEST_USERNAME = "username";
-    private static final String DIGEST_REALM = "realm";
-    private static final String DIGEST_NONCE = "nonce";
-    private static final String DIGEST_NC = "nc";
-    private static final String DIGEST_CNONCE = "cnonce";
-    private static final String DIGEST_URI = "digest-uri";
-    private static final String DIGEST_RESPONSE = "response";
-    private static final String DIGEST_QOP = "qop";
-
-    private static final String RESPONSE_AUTH_HEADER = "rspauth=";
-    private static final String HEX_CHARS = "0123456789abcdef";
-
-    /**
-     * Represents the set of data we need to generate the DIGEST-MD5 response.
-     */
-    public static class Data {
-
-        private static final String CHARSET = "utf-8";
-
-        public String username;
-        public String password;
-        public String realm;
-        public String nonce;
-        public String nc;
-        public String cnonce;
-        public String digestUri;
-        public String qop;
-
-        @VisibleForTesting
-        Data() {
-            // Do nothing
-        }
-
-        public Data(ImapStore imapStore, MailTransport transport, Map<String, String> challenge) {
-            username = imapStore.getUsername();
-            password = imapStore.getPassword();
-            realm = challenge.getOrDefault(DIGEST_REALM, "");
-            nonce = challenge.get(DIGEST_NONCE);
-            cnonce = createCnonce();
-            nc = "00000001"; // Subsequent Authentication not supported, nounce count always 1.
-            qop = "auth"; // Other config not supported
-            digestUri = "imap/" + transport.getHost();
-        }
-
-        private static String createCnonce() {
-            SecureRandom generator = new SecureRandom();
-
-            // At least 64 bits of entropy is required
-            byte[] rawBytes = new byte[8];
-            generator.nextBytes(rawBytes);
-
-            return Base64.encodeToString(rawBytes, Base64.NO_WRAP);
-        }
-
-        /**
-         * Verify the response-auth returned by the server is correct.
-         */
-        public void verifyResponseAuth(String response)
-                throws MessagingException {
-            if (!response.startsWith(RESPONSE_AUTH_HEADER)) {
-                throw new MessagingException("response-auth expected");
-            }
-            if (!response.substring(RESPONSE_AUTH_HEADER.length())
-                    .equals(DigestMd5Utils.getResponse(this, true))) {
-                throw new MessagingException("invalid response-auth return from the server.");
-            }
-        }
-
-        public String createResponse() {
-            String response = getResponse(this, false);
-            ResponseBuilder builder = new ResponseBuilder();
-            builder
-                    .append(DIGEST_CHARSET, CHARSET)
-                    .appendQuoted(DIGEST_USERNAME, username)
-                    .appendQuoted(DIGEST_REALM, realm)
-                    .appendQuoted(DIGEST_NONCE, nonce)
-                    .append(DIGEST_NC, nc)
-                    .appendQuoted(DIGEST_CNONCE, cnonce)
-                    .appendQuoted(DIGEST_URI, digestUri)
-                    .append(DIGEST_RESPONSE, response)
-                    .append(DIGEST_QOP, qop);
-            return builder.toString();
-        }
-
-        private static class ResponseBuilder {
-
-            private StringBuilder mBuilder = new StringBuilder();
-
-            public ResponseBuilder appendQuoted(String key, String value) {
-                if (mBuilder.length() != 0) {
-                    mBuilder.append(",");
-                }
-                mBuilder.append(key).append("=\"").append(value).append("\"");
-                return this;
-            }
-
-            public ResponseBuilder append(String key, String value) {
-                if (mBuilder.length() != 0) {
-                    mBuilder.append(",");
-                }
-                mBuilder.append(key).append("=").append(value);
-                return this;
-            }
-
-            @Override
-            public String toString() {
-                return mBuilder.toString();
-            }
-        }
-    }
-
-    /*
-        response-value  =
-            toHex( getKeyDigest ( toHex(getMd5(a1)),
-            { nonce-value, ":" nc-value, ":",
-              cnonce-value, ":", qop-value, ":", toHex(getMd5(a2)) }))
-     * @param isResponseAuth is the response the one the server is returning us. response-auth has
-     * different a2 format.
-     */
-    @VisibleForTesting
-    static String getResponse(Data data, boolean isResponseAuth) {
-        StringBuilder a1 = new StringBuilder();
-        a1.append(new String(
-                getMd5(data.username + ":" + data.realm + ":" + data.password),
-                StandardCharsets.ISO_8859_1));
-        a1.append(":").append(data.nonce).append(":").append(data.cnonce);
-
-        StringBuilder a2 = new StringBuilder();
-        if (!isResponseAuth) {
-            a2.append("AUTHENTICATE");
-        }
-        a2.append(":").append(data.digestUri);
-
-        return toHex(getKeyDigest(
-                toHex(getMd5(a1.toString())),
-                data.nonce + ":" + data.nc + ":" + data.cnonce + ":" + data.qop + ":" + toHex(
-                        getMd5(a2.toString()))
-        ));
-    }
-
-    /**
-     * Let getMd5(s) be the 16 octet MD5 hash [RFC 1321] of the octet string s.
-     */
-    private static byte[] getMd5(String s) {
-        try {
-            MessageDigest digester = MessageDigest.getInstance("MD5");
-            digester.update(s.getBytes(StandardCharsets.ISO_8859_1));
-            return digester.digest();
-        } catch (NoSuchAlgorithmException e) {
-            throw new AssertionError(e);
-        }
-    }
-
-    /**
-     * Let getKeyDigest(k, s) be getMd5({k, ":", s}), i.e., the 16 octet hash of the string k, a colon and the
-     * string s.
-     */
-    private static byte[] getKeyDigest(String k, String s) {
-        StringBuilder builder = new StringBuilder(k).append(":").append(s);
-        return getMd5(builder.toString());
-    }
-
-    /**
-     * Let toHex(n) be the representation of the 16 octet MD5 hash n as a string of 32 hex digits
-     * (with alphabetic characters always in lower case, since MD5 is case sensitive).
-     */
-    private static String toHex(byte[] n) {
-        StringBuilder result = new StringBuilder();
-        for (byte b : n) {
-            int unsignedByte = b & 0xFF;
-            result.append(HEX_CHARS.charAt(unsignedByte / 16))
-                    .append(HEX_CHARS.charAt(unsignedByte % 16));
-        }
-        return result.toString();
-    }
-
-    public static Map<String, String> parseDigestMessage(String message) throws MessagingException {
-        Map<String, String> result = new DigestMessageParser(message).parse();
-        if (!result.containsKey(DIGEST_NONCE)) {
-            throw new MessagingException("nonce missing from server DIGEST-MD5 challenge");
-        }
-        return result;
-    }
-
-    /**
-     * Parse the key-value pair returned by the server.
-     */
-    private static class DigestMessageParser {
-
-        private final String mMessage;
-        private int mPosition = 0;
-        private Map<String, String> mResult = new ArrayMap<>();
-
-        public DigestMessageParser(String message) {
-            mMessage = message;
-        }
-
-        @Nullable
-        public Map<String, String> parse() {
-            try {
-                while (mPosition < mMessage.length()) {
-                    parsePair();
-                    if (mPosition != mMessage.length()) {
-                        expect(',');
-                    }
-                }
-            } catch (IndexOutOfBoundsException e) {
-                VvmLog.e(TAG, e.toString());
-                return null;
-            }
-            return mResult;
-        }
-
-        private void parsePair() {
-            String key = parseKey();
-            expect('=');
-            String value = parseValue();
-            mResult.put(key, value);
-        }
-
-        private void expect(char c) {
-            if (pop() != c) {
-                throw new IllegalStateException(
-                        "unexpected character " + mMessage.charAt(mPosition));
-            }
-        }
-
-        private char pop() {
-            char result = peek();
-            mPosition++;
-            return result;
-        }
-
-        private char peek() {
-            return mMessage.charAt(mPosition);
-        }
-
-        private void goToNext(char c) {
-            while (peek() != c) {
-                mPosition++;
-            }
-        }
-
-        private String parseKey() {
-            int start = mPosition;
-            goToNext('=');
-            return mMessage.substring(start, mPosition);
-        }
-
-        private String parseValue() {
-            if (peek() == '"') {
-                return parseQuotedValue();
-            } else {
-                return parseUnquotedValue();
-            }
-        }
-
-        private String parseQuotedValue() {
-            expect('"');
-            StringBuilder result = new StringBuilder();
-            while (true) {
-                char c = pop();
-                if (c == '\\') {
-                    result.append(pop());
-                } else if (c == '"') {
-                    break;
-                } else {
-                    result.append(c);
-                }
-            }
-            return result.toString();
-        }
-
-        private String parseUnquotedValue() {
-            StringBuilder result = new StringBuilder();
-            while (true) {
-                char c = pop();
-                if (c == '\\') {
-                    result.append(pop());
-                } else if (c == ',') {
-                    mPosition--;
-                    break;
-                } else {
-                    result.append(c);
-                }
-
-                if (mPosition == mMessage.length()) {
-                    break;
-                }
-            }
-            return result.toString();
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java b/java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java
deleted file mode 100644
index d8e7575..0000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-import com.android.voicemailomtp.mail.store.ImapStore;
-
-import java.util.Locale;
-
-public final class ImapConstants {
-    private ImapConstants() {}
-
-    public static final String FETCH_FIELD_BODY_PEEK_BARE = "BODY.PEEK";
-    public static final String FETCH_FIELD_BODY_PEEK = FETCH_FIELD_BODY_PEEK_BARE + "[]";
-    public static final String FETCH_FIELD_BODY_PEEK_SANE = String.format(
-            Locale.US, "BODY.PEEK[]<0.%d>", ImapStore.FETCH_BODY_SANE_SUGGESTED_SIZE);
-    public static final String FETCH_FIELD_HEADERS =
-            "BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc message-id)]";
-
-    public static final String ALERT = "ALERT";
-    public static final String APPEND = "APPEND";
-    public static final String AUTHENTICATE = "AUTHENTICATE";
-    public static final String BAD = "BAD";
-    public static final String BADCHARSET = "BADCHARSET";
-    public static final String BODY = "BODY";
-    public static final String BODY_BRACKET_HEADER = "BODY[HEADER";
-    public static final String BODYSTRUCTURE = "BODYSTRUCTURE";
-    public static final String BYE = "BYE";
-    public static final String CAPABILITY = "CAPABILITY";
-    public static final String CHECK = "CHECK";
-    public static final String CLOSE = "CLOSE";
-    public static final String COPY = "COPY";
-    public static final String COPYUID = "COPYUID";
-    public static final String CREATE = "CREATE";
-    public static final String DELETE = "DELETE";
-    public static final String EXAMINE = "EXAMINE";
-    public static final String EXISTS = "EXISTS";
-    public static final String EXPUNGE = "EXPUNGE";
-    public static final String FETCH = "FETCH";
-    public static final String FLAG_ANSWERED = "\\ANSWERED";
-    public static final String FLAG_DELETED = "\\DELETED";
-    public static final String FLAG_FLAGGED = "\\FLAGGED";
-    public static final String FLAG_NO_SELECT = "\\NOSELECT";
-    public static final String FLAG_SEEN = "\\SEEN";
-    public static final String FLAGS = "FLAGS";
-    public static final String FLAGS_SILENT = "FLAGS.SILENT";
-    public static final String ID = "ID";
-    public static final String INBOX = "INBOX";
-    public static final String INTERNALDATE = "INTERNALDATE";
-    public static final String LIST = "LIST";
-    public static final String LOGIN = "LOGIN";
-    public static final String LOGOUT = "LOGOUT";
-    public static final String LSUB = "LSUB";
-    public static final String NAMESPACE = "NAMESPACE";
-    public static final String NO = "NO";
-    public static final String NOOP = "NOOP";
-    public static final String OK = "OK";
-    public static final String PARSE = "PARSE";
-    public static final String PERMANENTFLAGS = "PERMANENTFLAGS";
-    public static final String PREAUTH = "PREAUTH";
-    public static final String READ_ONLY = "READ-ONLY";
-    public static final String READ_WRITE = "READ-WRITE";
-    public static final String RENAME = "RENAME";
-    public static final String RFC822_SIZE = "RFC822.SIZE";
-    public static final String SEARCH = "SEARCH";
-    public static final String SELECT = "SELECT";
-    public static final String STARTTLS = "STARTTLS";
-    public static final String STATUS = "STATUS";
-    public static final String STORE = "STORE";
-    public static final String SUBSCRIBE = "SUBSCRIBE";
-    public static final String TEXT = "TEXT";
-    public static final String TRYCREATE = "TRYCREATE";
-    public static final String UID = "UID";
-    public static final String UID_COPY = "UID COPY";
-    public static final String UID_FETCH = "UID FETCH";
-    public static final String UID_SEARCH = "UID SEARCH";
-    public static final String UID_STORE = "UID STORE";
-    public static final String UIDNEXT = "UIDNEXT";
-    public static final String UIDPLUS = "UIDPLUS";
-    public static final String UIDVALIDITY = "UIDVALIDITY";
-    public static final String UNSEEN = "UNSEEN";
-    public static final String UNSUBSCRIBE = "UNSUBSCRIBE";
-    public static final String XOAUTH2 = "XOAUTH2";
-    public static final String APPENDUID = "APPENDUID";
-    public static final String NIL = "NIL";
-
-    /**
-     * NO responses
-     */
-    public static final String NO_COMMAND_NOT_ALLOWED = "command not allowed";
-    public static final String NO_RESERVATION_FAILED = "reservation failed";
-    public static final String NO_APPLICATION_ERROR = "application error";
-    public static final String NO_INVALID_PARAMETER = "invalid parameter";
-    public static final String NO_INVALID_COMMAND = "invalid command";
-    public static final String NO_UNKNOWN_COMMAND = "unknown command";
-    // AUTHENTICATE
-    // The subscriber can not be located in the system.
-    public static final String NO_UNKNOWN_USER = "unknown user";
-    // The Client Type or Protocol Version is unknown.
-    public static final String NO_UNKNOWN_CLIENT = "unknown client";
-    // The password received from the client does not match the password defined in the subscriber's profile.
-    public static final String NO_INVALID_PASSWORD = "invalid password";
-    // The subscriber's mailbox has not yet been initialised via the TUI
-    public static final String NO_MAILBOX_NOT_INITIALIZED = "mailbox not initialized";
-    // The subscriber has not been provisioned for the VVM service.
-    public static final String NO_SERVICE_IS_NOT_PROVISIONED =
-            "service is not provisioned";
-    // The subscriber is provisioned for the VVM service but the VVM service is currently not active
-    public static final String NO_SERVICE_IS_NOT_ACTIVATED = "service is not activated";
-    // The Voice Mail Blocked flag in the subscriber's profile is set to YES.
-    public static final String NO_USER_IS_BLOCKED = "user is blocked";
-
-    /**
-     * extensions
-     */
-    public static final String GETQUOTA = "GETQUOTA";
-    public static final String GETQUOTAROOT = "GETQUOTAROOT";
-    public static final String QUOTAROOT = "QUOTAROOT";
-    public static final String QUOTA = "QUOTA";
-
-    /**
-     * capabilities
-     */
-    public static final String CAPABILITY_AUTH_DIGEST_MD5 = "AUTH=DIGEST-MD5";
-    public static final String CAPABILITY_STARTTLS = "STARTTLS";
-
-    /**
-     * authentication
-     */
-    public static final String AUTH_DIGEST_MD5 = "DIGEST-MD5";
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapElement.java b/java/com/android/voicemailomtp/mail/store/imap/ImapElement.java
deleted file mode 100644
index 9f272e3..0000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapElement.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-/**
- * Class representing "element"s in IMAP responses.
- *
- * <p>Class hierarchy:
- * <pre>
- * ImapElement
- *   |
- *   |-- ImapElement.NONE (for 'index out of range')
- *   |
- *   |-- ImapList (isList() == true)
- *   |   |
- *   |   |-- ImapList.EMPTY
- *   |   |
- *   |   --- ImapResponse
- *   |
- *   --- ImapString (isString() == true)
- *       |
- *       |-- ImapString.EMPTY
- *       |
- *       |-- ImapSimpleString
- *       |
- *       |-- ImapMemoryLiteral
- *       |
- *       --- ImapTempFileLiteral
- * </pre>
- */
-public abstract class ImapElement {
-    /**
-     * An element that is returned by {@link ImapList#getElementOrNone} to indicate an index
-     * is out of range.
-     */
-    public static final ImapElement NONE = new ImapElement() {
-        @Override public void destroy() {
-            // Don't call super.destroy().
-            // It's a shared object.  We don't want the mDestroyed to be set on this.
-        }
-
-        @Override public boolean isList() {
-            return false;
-        }
-
-        @Override public boolean isString() {
-            return false;
-        }
-
-        @Override public String toString() {
-            return "[NO ELEMENT]";
-        }
-
-        @Override
-        public boolean equalsForTest(ImapElement that) {
-            return super.equalsForTest(that);
-        }
-    };
-
-    private boolean mDestroyed = false;
-
-    public abstract boolean isList();
-
-    public abstract boolean isString();
-
-    protected boolean isDestroyed() {
-        return mDestroyed;
-    }
-
-    /**
-     * Clean up the resources used by the instance.
-     * It's for removing a temp file used by {@link ImapTempFileLiteral}.
-     */
-    public void destroy() {
-        mDestroyed = true;
-    }
-
-    /**
-     * Throws {@link RuntimeException} if it's already destroyed.
-     */
-    protected final void checkNotDestroyed() {
-        if (mDestroyed) {
-            throw new RuntimeException("Already destroyed");
-        }
-    }
-
-    /**
-     * Return a string that represents this object; it's purely for the debug purpose.  Don't
-     * mistake it for {@link ImapString#getString}.
-     *
-     * Abstract to force subclasses to implement it.
-     */
-    @Override
-    public abstract String toString();
-
-    /**
-     * The equals implementation that is intended to be used only for unit testing.
-     * (Because it may be heavy and has a special sense of "equal" for testing.)
-     */
-    public boolean equalsForTest(ImapElement that) {
-        if (that == null) {
-            return false;
-        }
-        return this.getClass() == that.getClass(); // Has to be the same class.
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapList.java b/java/com/android/voicemailomtp/mail/store/imap/ImapList.java
deleted file mode 100644
index 970423c..0000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapList.java
+++ /dev/null
@@ -1,235 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-import java.util.ArrayList;
-
-/**
- * Class represents an IMAP list.
- */
-public class ImapList extends ImapElement {
-    /**
-     * {@link ImapList} representing an empty list.
-     */
-    public static final ImapList EMPTY = new ImapList() {
-        @Override public void destroy() {
-            // Don't call super.destroy().
-            // It's a shared object.  We don't want the mDestroyed to be set on this.
-        }
-
-        @Override void add(ImapElement e) {
-            throw new RuntimeException();
-        }
-    };
-
-    private ArrayList<ImapElement> mList = new ArrayList<ImapElement>();
-
-    /* package */ void add(ImapElement e) {
-        if (e == null) {
-            throw new RuntimeException("Can't add null");
-        }
-        mList.add(e);
-    }
-
-    @Override
-    public final boolean isString() {
-        return false;
-    }
-
-    @Override
-    public final boolean isList() {
-        return true;
-    }
-
-    public final int size() {
-        return mList.size();
-    }
-
-    public final boolean isEmpty() {
-        return size() == 0;
-    }
-
-    /**
-     * Return true if the element at {@code index} exists, is string, and equals to {@code s}.
-     * (case insensitive)
-     */
-    public final boolean is(int index, String s) {
-        return is(index, s, false);
-    }
-
-    /**
-     * Same as {@link #is(int, String)}, but does the prefix match if {@code prefixMatch}.
-     */
-    public final boolean is(int index, String s, boolean prefixMatch) {
-        if (!prefixMatch) {
-            return getStringOrEmpty(index).is(s);
-        } else {
-            return getStringOrEmpty(index).startsWith(s);
-        }
-    }
-
-    /**
-     * Return the element at {@code index}.
-     * If {@code index} is out of range, returns {@link ImapElement#NONE}.
-     */
-    public final ImapElement getElementOrNone(int index) {
-        return (index >= mList.size()) ? ImapElement.NONE : mList.get(index);
-    }
-
-    /**
-     * Return the element at {@code index} if it's a list.
-     * If {@code index} is out of range or not a list, returns {@link ImapList#EMPTY}.
-     */
-    public final ImapList getListOrEmpty(int index) {
-        ImapElement el = getElementOrNone(index);
-        return el.isList() ? (ImapList) el : EMPTY;
-    }
-
-    /**
-     * Return the element at {@code index} if it's a string.
-     * If {@code index} is out of range or not a string, returns {@link ImapString#EMPTY}.
-     */
-    public final ImapString getStringOrEmpty(int index) {
-        ImapElement el = getElementOrNone(index);
-        return el.isString() ? (ImapString) el : ImapString.EMPTY;
-    }
-
-    /**
-     * Return an element keyed by {@code key}.  Return null if not found.  {@code key} has to be
-     * at an even index.
-     */
-    /* package */ final ImapElement getKeyedElementOrNull(String key, boolean prefixMatch) {
-        for (int i = 1; i < size(); i += 2) {
-            if (is(i-1, key, prefixMatch)) {
-                return mList.get(i);
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Return an {@link ImapList} keyed by {@code key}.
-     * Return {@link ImapList#EMPTY} if not found.
-     */
-    public final ImapList getKeyedListOrEmpty(String key) {
-        return getKeyedListOrEmpty(key, false);
-    }
-
-    /**
-     * Return an {@link ImapList} keyed by {@code key}.
-     * Return {@link ImapList#EMPTY} if not found.
-     */
-    public final ImapList getKeyedListOrEmpty(String key, boolean prefixMatch) {
-        ImapElement e = getKeyedElementOrNull(key, prefixMatch);
-        return (e != null) ? ((ImapList) e) : ImapList.EMPTY;
-    }
-
-    /**
-     * Return an {@link ImapString} keyed by {@code key}.
-     * Return {@link ImapString#EMPTY} if not found.
-     */
-    public final ImapString getKeyedStringOrEmpty(String key) {
-        return getKeyedStringOrEmpty(key, false);
-    }
-
-    /**
-     * Return an {@link ImapString} keyed by {@code key}.
-     * Return {@link ImapString#EMPTY} if not found.
-     */
-    public final ImapString getKeyedStringOrEmpty(String key, boolean prefixMatch) {
-        ImapElement e = getKeyedElementOrNull(key, prefixMatch);
-        return (e != null) ? ((ImapString) e) : ImapString.EMPTY;
-    }
-
-    /**
-     * Return true if it contains {@code s}.
-     */
-    public final boolean contains(String s) {
-        for (int i = 0; i < size(); i++) {
-            if (getStringOrEmpty(i).is(s)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    @Override
-    public void destroy() {
-        if (mList != null) {
-            for (ImapElement e : mList) {
-                e.destroy();
-            }
-            mList = null;
-        }
-        super.destroy();
-    }
-
-    @Override
-    public String toString() {
-        return mList.toString();
-    }
-
-    /**
-     * Return the text representations of the contents concatenated with ",".
-     */
-    public final String flatten() {
-        return flatten(new StringBuilder()).toString();
-    }
-
-    /**
-     * Returns text representations (i.e. getString()) of contents joined together with
-     * "," as the separator.
-     *
-     * Only used for building the capability string passed to vendor policies.
-     *
-     * We can't use toString(), because it's for debugging (meaning the format may change any time),
-     * and it won't expand literals.
-     */
-    private final StringBuilder flatten(StringBuilder sb) {
-        sb.append('[');
-        for (int i = 0; i < mList.size(); i++) {
-            if (i > 0) {
-                sb.append(',');
-            }
-            final ImapElement e = getElementOrNone(i);
-            if (e.isList()) {
-                getListOrEmpty(i).flatten(sb);
-            } else if (e.isString()) {
-                sb.append(getStringOrEmpty(i).getString());
-            }
-        }
-        sb.append(']');
-        return sb;
-    }
-
-    @Override
-    public boolean equalsForTest(ImapElement that) {
-        if (!super.equalsForTest(that)) {
-            return false;
-        }
-        ImapList thatList = (ImapList) that;
-        if (size() != thatList.size()) {
-            return false;
-        }
-        for (int i = 0; i < size(); i++) {
-            if (!mList.get(i).equalsForTest(thatList.getElementOrNone(i))) {
-                return false;
-            }
-        }
-        return true;
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.java b/java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.java
deleted file mode 100644
index ad60ca7..0000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-import com.android.voicemailomtp.mail.FixedLengthInputStream;
-import com.android.voicemailomtp.VvmLog;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-
-/**
- * Subclass of {@link ImapString} used for literals backed by an in-memory byte array.
- */
-public class ImapMemoryLiteral extends ImapString {
-    private final String TAG = "ImapMemoryLiteral";
-    private byte[] mData;
-
-    /* package */ ImapMemoryLiteral(FixedLengthInputStream in) throws IOException {
-        // We could use ByteArrayOutputStream and IOUtils.copy, but it'd perform an unnecessary
-        // copy....
-        mData = new byte[in.getLength()];
-        int pos = 0;
-        while (pos < mData.length) {
-            int read = in.read(mData, pos, mData.length - pos);
-            if (read < 0) {
-                break;
-            }
-            pos += read;
-        }
-        if (pos != mData.length) {
-            VvmLog.w(TAG, "length mismatch");
-        }
-    }
-
-    @Override
-    public void destroy() {
-        mData = null;
-        super.destroy();
-    }
-
-    @Override
-    public String getString() {
-        try {
-            return new String(mData, "US-ASCII");
-        } catch (UnsupportedEncodingException e) {
-            VvmLog.e(TAG, "Unsupported encoding: ", e);
-        }
-        return null;
-    }
-
-    @Override
-    public InputStream getAsStream() {
-        return new ByteArrayInputStream(mData);
-    }
-
-    @Override
-    public String toString() {
-        return String.format("{%d byte literal(memory)}", mData.length);
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java b/java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java
deleted file mode 100644
index 412f16d..0000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-/**
- * Class represents an IMAP response.
- */
-public class ImapResponse extends ImapList {
-    private final String mTag;
-    private final boolean mIsContinuationRequest;
-
-    /* package */ ImapResponse(String tag, boolean isContinuationRequest) {
-        mTag = tag;
-        mIsContinuationRequest = isContinuationRequest;
-    }
-
-    /* package */ static boolean isStatusResponse(String symbol) {
-        return     ImapConstants.OK.equalsIgnoreCase(symbol)
-                || ImapConstants.NO.equalsIgnoreCase(symbol)
-                || ImapConstants.BAD.equalsIgnoreCase(symbol)
-                || ImapConstants.PREAUTH.equalsIgnoreCase(symbol)
-                || ImapConstants.BYE.equalsIgnoreCase(symbol);
-    }
-
-    /**
-     * @return whether it's a tagged response.
-     */
-    public boolean isTagged() {
-        return mTag != null;
-    }
-
-    /**
-     * @return whether it's a continuation request.
-     */
-    public boolean isContinuationRequest() {
-        return mIsContinuationRequest;
-    }
-
-    public boolean isStatusResponse() {
-        return isStatusResponse(getStringOrEmpty(0).getString());
-    }
-
-    /**
-     * @return whether it's an OK response.
-     */
-    public boolean isOk() {
-        return is(0, ImapConstants.OK);
-    }
-
-    /**
-     * @return whether it's an BAD response.
-     */
-    public boolean isBad() {
-        return is(0, ImapConstants.BAD);
-    }
-
-    /**
-     * @return whether it's an NO response.
-     */
-    public boolean isNo() {
-        return is(0, ImapConstants.NO);
-    }
-
-    /**
-     * @return whether it's an {@code responseType} data response.  (i.e. not tagged).
-     * @param index where {@code responseType} should appear.  e.g. 1 for "FETCH"
-     * @param responseType e.g. "FETCH"
-     */
-    public final boolean isDataResponse(int index, String responseType) {
-        return !isTagged() && getStringOrEmpty(index).is(responseType);
-    }
-
-    /**
-     * @return Response code (RFC 3501 7.1) if it's a status response.
-     *
-     * e.g. "ALERT" for "* OK [ALERT] System shutdown in 10 minutes"
-     */
-    public ImapString getResponseCodeOrEmpty() {
-        if (!isStatusResponse()) {
-            return ImapString.EMPTY; // Not a status response.
-        }
-        return getListOrEmpty(1).getStringOrEmpty(0);
-    }
-
-    /**
-     * @return Alert message it it has ALERT response code.
-     *
-     * e.g. "System shutdown in 10 minutes" for "* OK [ALERT] System shutdown in 10 minutes"
-     */
-    public ImapString getAlertTextOrEmpty() {
-        if (!getResponseCodeOrEmpty().is(ImapConstants.ALERT)) {
-            return ImapString.EMPTY; // Not an ALERT
-        }
-        // The 3rd element contains all the rest of line.
-        return getStringOrEmpty(2);
-    }
-
-    /**
-     * @return Response text in a status response.
-     */
-    public ImapString getStatusResponseTextOrEmpty() {
-        if (!isStatusResponse()) {
-            return ImapString.EMPTY;
-        }
-        return getStringOrEmpty(getElementOrNone(1).isList() ? 2 : 1);
-    }
-
-    public ImapString getStatusOrEmpty() {
-        if (!isStatusResponse()) {
-            return ImapString.EMPTY;
-        }
-        return getStringOrEmpty(0);
-    }
-
-    @Override
-    public String toString() {
-        String tag = mTag;
-        if (isContinuationRequest()) {
-            tag = "+";
-        }
-        return "#" + tag + "# " + super.toString();
-    }
-
-    @Override
-    public boolean equalsForTest(ImapElement that) {
-        if (!super.equalsForTest(that)) {
-            return false;
-        }
-        final ImapResponse thatResponse = (ImapResponse) that;
-        if (mTag == null) {
-            if (thatResponse.mTag != null) {
-                return false;
-            }
-        } else {
-            if (!mTag.equals(thatResponse.mTag)) {
-                return false;
-            }
-        }
-        if (mIsContinuationRequest != thatResponse.mIsContinuationRequest) {
-            return false;
-        }
-        return true;
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java b/java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java
deleted file mode 100644
index 692596f..0000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java
+++ /dev/null
@@ -1,432 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.voicemailomtp.mail.FixedLengthInputStream;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.mail.PeekableInputStream;
-import com.android.voicemailomtp.VvmLog;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-
-/**
- * IMAP response parser.
- */
-public class ImapResponseParser {
-    private static final String TAG = "ImapResponseParser";
-
-    /**
-     * Literal larger than this will be stored in temp file.
-     */
-    public static final int LITERAL_KEEP_IN_MEMORY_THRESHOLD = 2 * 1024 * 1024;
-
-    /** Input stream */
-    private final PeekableInputStream mIn;
-
-    private final int mLiteralKeepInMemoryThreshold;
-
-    /** StringBuilder used by readUntil() */
-    private final StringBuilder mBufferReadUntil = new StringBuilder();
-
-    /** StringBuilder used by parseBareString() */
-    private final StringBuilder mParseBareString = new StringBuilder();
-
-    /**
-     * We store all {@link ImapResponse} in it.  {@link #destroyResponses()} must be called from
-     * time to time to destroy them and clear it.
-     */
-    private final ArrayList<ImapResponse> mResponsesToDestroy = new ArrayList<ImapResponse>();
-
-    /**
-     * Exception thrown when we receive BYE.  It derives from IOException, so it'll be treated
-     * in the same way EOF does.
-     */
-    public static class ByeException extends IOException {
-        public static final String MESSAGE = "Received BYE";
-        public ByeException() {
-            super(MESSAGE);
-        }
-    }
-
-    /**
-     * Public constructor for normal use.
-     */
-    public ImapResponseParser(InputStream in) {
-        this(in, LITERAL_KEEP_IN_MEMORY_THRESHOLD);
-    }
-
-    /**
-     * Constructor for testing to override the literal size threshold.
-     */
-    /* package for test */ ImapResponseParser(InputStream in, int literalKeepInMemoryThreshold) {
-        mIn = new PeekableInputStream(in);
-        mLiteralKeepInMemoryThreshold = literalKeepInMemoryThreshold;
-    }
-
-    private static IOException newEOSException() {
-        final String message = "End of stream reached";
-        VvmLog.d(TAG, message);
-        return new IOException(message);
-    }
-
-    /**
-     * Peek next one byte.
-     *
-     * Throws IOException() if reaches EOF.  As long as logical response lines end with \r\n,
-     * we shouldn't see EOF during parsing.
-     */
-    private int peek() throws IOException {
-        final int next = mIn.peek();
-        if (next == -1) {
-            throw newEOSException();
-        }
-        return next;
-    }
-
-    /**
-     * Read and return one byte from {@link #mIn}, and put it in {@link #mDiscourseLogger}.
-     *
-     * Throws IOException() if reaches EOF.  As long as logical response lines end with \r\n,
-     * we shouldn't see EOF during parsing.
-     */
-    private int readByte() throws IOException {
-        int next = mIn.read();
-        if (next == -1) {
-            throw newEOSException();
-        }
-        return next;
-    }
-
-    /**
-     * Destroy all the {@link ImapResponse}s stored in the internal storage and clear it.
-     *
-     * @see #readResponse()
-     */
-    public void destroyResponses() {
-        for (ImapResponse r : mResponsesToDestroy) {
-            r.destroy();
-        }
-        mResponsesToDestroy.clear();
-    }
-
-    /**
-     * Reads the next response available on the stream and returns an
-     * {@link ImapResponse} object that represents it.
-     *
-     * <p>When this method successfully returns an {@link ImapResponse}, the {@link ImapResponse}
-     * is stored in the internal storage.  When the {@link ImapResponse} is no longer used
-     * {@link #destroyResponses} should be called to destroy all the responses in the array.
-     *
-     * @param byeExpected is a untagged BYE response expected? If not proper cleanup will be done
-     * and {@link ByeException} will be thrown.
-     * @return the parsed {@link ImapResponse} object.
-     * @exception ByeException when detects BYE and <code>byeExpected</code> is false.
-     */
-    public ImapResponse readResponse(boolean byeExpected) throws IOException, MessagingException {
-        ImapResponse response = null;
-        try {
-            response = parseResponse();
-        } catch (RuntimeException e) {
-            // Parser crash -- log network activities.
-            onParseError(e);
-            throw e;
-        } catch (IOException e) {
-            // Network error, or received an unexpected char.
-            onParseError(e);
-            throw e;
-        }
-
-        // Handle this outside of try-catch.  We don't have to dump protocol log when getting BYE.
-        if (!byeExpected && response.is(0, ImapConstants.BYE)) {
-            Log.w(TAG, ByeException.MESSAGE);
-            response.destroy();
-            throw new ByeException();
-        }
-        mResponsesToDestroy.add(response);
-        return response;
-    }
-
-    private void onParseError(Exception e) {
-        // Read a few more bytes, so that the log will contain some more context, even if the parser
-        // crashes in the middle of a response.
-        // This also makes sure the byte in question will be logged, no matter where it crashes.
-        // e.g. when parseAtom() peeks and finds at an unexpected char, it throws an exception
-        // before actually reading it.
-        // However, we don't want to read too much, because then it may get into an email message.
-        try {
-            for (int i = 0; i < 4; i++) {
-                int b = readByte();
-                if (b == -1 || b == '\n') {
-                    break;
-                }
-            }
-        } catch (IOException ignore) {
-        }
-        VvmLog.w(TAG, "Exception detected: " + e.getMessage());
-    }
-
-    /**
-     * Read next byte from stream and throw it away.  If the byte is different from {@code expected}
-     * throw {@link MessagingException}.
-     */
-    /* package for test */ void expect(char expected) throws IOException {
-        final int next = readByte();
-        if (expected != next) {
-            throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)",
-                    (int) expected, expected, next, (char) next));
-        }
-    }
-
-    /**
-     * Read bytes until we find {@code end}, and return all as string.
-     * The {@code end} will be read (rather than peeked) and won't be included in the result.
-     */
-    /* package for test */ String readUntil(char end) throws IOException {
-        mBufferReadUntil.setLength(0);
-        for (;;) {
-            final int ch = readByte();
-            if (ch != end) {
-                mBufferReadUntil.append((char) ch);
-            } else {
-                return mBufferReadUntil.toString();
-            }
-        }
-    }
-
-    /**
-     * Read all bytes until \r\n.
-     */
-    /* package */ String readUntilEol() throws IOException {
-        String ret = readUntil('\r');
-        expect('\n'); // TODO Should this really be error?
-        return ret;
-    }
-
-    /**
-     * Parse and return the response line.
-     */
-    private ImapResponse parseResponse() throws IOException, MessagingException {
-        // We need to destroy the response if we get an exception.
-        // So, we first store the response that's being built in responseToDestroy, until it's
-        // completely built, at which point we copy it into responseToReturn and null out
-        // responseToDestroyt.
-        // If responseToDestroy is not null in finally, we destroy it because that means
-        // we got an exception somewhere.
-        ImapResponse responseToDestroy = null;
-        final ImapResponse responseToReturn;
-
-        try {
-            final int ch = peek();
-            if (ch == '+') { // Continuation request
-                readByte(); // skip +
-                expect(' ');
-                responseToDestroy = new ImapResponse(null, true);
-
-                // If it's continuation request, we don't really care what's in it.
-                responseToDestroy.add(new ImapSimpleString(readUntilEol()));
-
-                // Response has successfully been built.  Let's return it.
-                responseToReturn = responseToDestroy;
-                responseToDestroy = null;
-            } else {
-                // Status response or response data
-                final String tag;
-                if (ch == '*') {
-                    tag = null;
-                    readByte(); // skip *
-                    expect(' ');
-                } else {
-                    tag = readUntil(' ');
-                }
-                responseToDestroy = new ImapResponse(tag, false);
-
-                final ImapString firstString = parseBareString();
-                responseToDestroy.add(firstString);
-
-                // parseBareString won't eat a space after the string, so we need to skip it,
-                // if exists.
-                // If the next char is not ' ', it should be EOL.
-                if (peek() == ' ') {
-                    readByte(); // skip ' '
-
-                    if (responseToDestroy.isStatusResponse()) { // It's a status response
-
-                        // Is there a response code?
-                        final int next = peek();
-                        if (next == '[') {
-                            responseToDestroy.add(parseList('[', ']'));
-                            if (peek() == ' ') { // Skip following space
-                                readByte();
-                            }
-                        }
-
-                        String rest = readUntilEol();
-                        if (!TextUtils.isEmpty(rest)) {
-                            // The rest is free-form text.
-                            responseToDestroy.add(new ImapSimpleString(rest));
-                        }
-                    } else { // It's a response data.
-                        parseElements(responseToDestroy, '\0');
-                    }
-                } else {
-                    expect('\r');
-                    expect('\n');
-                }
-
-                // Response has successfully been built.  Let's return it.
-                responseToReturn = responseToDestroy;
-                responseToDestroy = null;
-            }
-        } finally {
-            if (responseToDestroy != null) {
-                // We get an exception.
-                responseToDestroy.destroy();
-            }
-        }
-
-        return responseToReturn;
-    }
-
-    private ImapElement parseElement() throws IOException, MessagingException {
-        final int next = peek();
-        switch (next) {
-            case '(':
-                return parseList('(', ')');
-            case '[':
-                return parseList('[', ']');
-            case '"':
-                readByte(); // Skip "
-                return new ImapSimpleString(readUntil('"'));
-            case '{':
-                return parseLiteral();
-            case '\r':  // CR
-                readByte(); // Consume \r
-                expect('\n'); // Should be followed by LF.
-                return null;
-            case '\n': // LF // There shouldn't be a bare LF, but just in case.
-                readByte(); // Consume \n
-                return null;
-            default:
-                return parseBareString();
-        }
-    }
-
-    /**
-     * Parses an atom.
-     *
-     * Special case: If an atom contains '[', everything until the next ']' will be considered
-     * a part of the atom.
-     * (e.g. "BODY[HEADER.FIELDS ("DATE" ...)]" will become a single ImapString)
-     *
-     * If the value is "NIL", returns an empty string.
-     */
-    private ImapString parseBareString() throws IOException, MessagingException {
-        mParseBareString.setLength(0);
-        for (;;) {
-            final int ch = peek();
-
-            // TODO Can we clean this up?  (This condition is from the old parser.)
-            if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' ||
-                    // ']' is not part of atom (it's in resp-specials)
-                    ch == ']' ||
-                    // docs claim that flags are \ atom but atom isn't supposed to
-                    // contain
-                    // * and some flags contain *
-                    // ch == '%' || ch == '*' ||
-                    ch == '%' ||
-                    // TODO probably should not allow \ and should recognize
-                    // it as a flag instead
-                    // ch == '"' || ch == '\' ||
-                    ch == '"' || (0x00 <= ch && ch <= 0x1f) || ch == 0x7f) {
-                if (mParseBareString.length() == 0) {
-                    throw new MessagingException("Expected string, none found.");
-                }
-                String s = mParseBareString.toString();
-
-                // NIL will be always converted into the empty string.
-                if (ImapConstants.NIL.equalsIgnoreCase(s)) {
-                    return ImapString.EMPTY;
-                }
-                return new ImapSimpleString(s);
-            } else if (ch == '[') {
-                // Eat all until next ']'
-                mParseBareString.append((char) readByte());
-                mParseBareString.append(readUntil(']'));
-                mParseBareString.append(']'); // readUntil won't include the end char.
-            } else {
-                mParseBareString.append((char) readByte());
-            }
-        }
-    }
-
-    private void parseElements(ImapList list, char end)
-            throws IOException, MessagingException {
-        for (;;) {
-            for (;;) {
-                final int next = peek();
-                if (next == end) {
-                    return;
-                }
-                if (next != ' ') {
-                    break;
-                }
-                // Skip space
-                readByte();
-            }
-            final ImapElement el = parseElement();
-            if (el == null) { // EOL
-                return;
-            }
-            list.add(el);
-        }
-    }
-
-    private ImapList parseList(char opening, char closing)
-            throws IOException, MessagingException {
-        expect(opening);
-        final ImapList list = new ImapList();
-        parseElements(list, closing);
-        expect(closing);
-        return list;
-    }
-
-    private ImapString parseLiteral() throws IOException, MessagingException {
-        expect('{');
-        final int size;
-        try {
-            size = Integer.parseInt(readUntil('}'));
-        } catch (NumberFormatException nfe) {
-            throw new MessagingException("Invalid length in literal");
-        }
-        if (size < 0) {
-            throw new MessagingException("Invalid negative length in literal");
-        }
-        expect('\r');
-        expect('\n');
-        FixedLengthInputStream in = new FixedLengthInputStream(mIn, size);
-        if (size > mLiteralKeepInMemoryThreshold) {
-            return new ImapTempFileLiteral(in);
-        } else {
-            return new ImapMemoryLiteral(in);
-        }
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java b/java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java
deleted file mode 100644
index 22d8141..0000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-import com.android.voicemailomtp.VvmLog;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-
-/**
- * Subclass of {@link ImapString} used for non literals.
- */
-public class ImapSimpleString extends ImapString {
-    private final String TAG = "ImapSimpleString";
-    private String mString;
-
-    /* package */  ImapSimpleString(String string) {
-        mString = (string != null) ? string : "";
-    }
-
-    @Override
-    public void destroy() {
-        mString = null;
-        super.destroy();
-    }
-
-    @Override
-    public String getString() {
-        return mString;
-    }
-
-    @Override
-    public InputStream getAsStream() {
-        try {
-            return new ByteArrayInputStream(mString.getBytes("US-ASCII"));
-        } catch (UnsupportedEncodingException e) {
-            VvmLog.e(TAG, "Unsupported encoding: ", e);
-        }
-        return null;
-    }
-
-    @Override
-    public String toString() {
-        // Purposefully not return just mString, in order to prevent using it instead of getString.
-        return "\"" + mString + "\"";
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapString.java b/java/com/android/voicemailomtp/mail/store/imap/ImapString.java
deleted file mode 100644
index 83efb64..0000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapString.java
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.mail.store.imap;
-
-import com.android.voicemailomtp.VvmLog;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-
-/**
- * Class represents an IMAP "element" that is not a list.
- *
- * An atom, quoted string, literal, are all represented by this.  Values like OK, STATUS are too.
- * Also, this class class may contain more arbitrary value like "BODY[HEADER.FIELDS ("DATE")]".
- * See {@link ImapResponseParser}.
- */
-public abstract class ImapString extends ImapElement {
-    private static final byte[] EMPTY_BYTES = new byte[0];
-
-    public static final ImapString EMPTY = new ImapString() {
-        @Override public void destroy() {
-            // Don't call super.destroy().
-            // It's a shared object.  We don't want the mDestroyed to be set on this.
-        }
-
-        @Override public String getString() {
-            return "";
-        }
-
-        @Override public InputStream getAsStream() {
-            return new ByteArrayInputStream(EMPTY_BYTES);
-        }
-
-        @Override public String toString() {
-            return "";
-        }
-    };
-
-    // This is used only for parsing IMAP's FETCH ENVELOPE command, in which
-    // en_US-like date format is used like "01-Jan-2009 11:20:39 -0800", so this should be
-    // handled by Locale.US
-    private final static SimpleDateFormat DATE_TIME_FORMAT =
-            new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.US);
-
-    private boolean mIsInteger;
-    private int mParsedInteger;
-    private Date mParsedDate;
-
-    @Override
-    public final boolean isList() {
-        return false;
-    }
-
-    @Override
-    public final boolean isString() {
-        return true;
-    }
-
-    /**
-     * @return true if and only if the length of the string is larger than 0.
-     *
-     * Note: IMAP NIL is considered an empty string. See {@link ImapResponseParser
-     * #parseBareString}.
-     * On the other hand, a quoted/literal string with value NIL (i.e. "NIL" and {3}\r\nNIL) is
-     * treated literally.
-     */
-    public final boolean isEmpty() {
-        return getString().length() == 0;
-    }
-
-    public abstract String getString();
-
-    public abstract InputStream getAsStream();
-
-    /**
-     * @return whether it can be parsed as a number.
-     */
-    public final boolean isNumber() {
-        if (mIsInteger) {
-            return true;
-        }
-        try {
-            mParsedInteger = Integer.parseInt(getString());
-            mIsInteger = true;
-            return true;
-        } catch (NumberFormatException e) {
-            return false;
-        }
-    }
-
-    /**
-     * @return value parsed as a number, or 0 if the string is not a number.
-     */
-    public final int getNumberOrZero() {
-        return getNumber(0);
-    }
-
-    /**
-     * @return value parsed as a number, or {@code defaultValue} if the string is not a number.
-     */
-    public final int getNumber(int defaultValue) {
-        if (!isNumber()) {
-            return defaultValue;
-        }
-        return mParsedInteger;
-    }
-
-    /**
-     * @return whether it can be parsed as a date using {@link #DATE_TIME_FORMAT}.
-     */
-    public final boolean isDate() {
-        if (mParsedDate != null) {
-            return true;
-        }
-        if (isEmpty()) {
-            return false;
-        }
-        try {
-            mParsedDate = DATE_TIME_FORMAT.parse(getString());
-            return true;
-        } catch (ParseException e) {
-            VvmLog.w("ImapString", getString() + " can't be parsed as a date.");
-            return false;
-        }
-    }
-
-    /**
-     * @return value it can be parsed as a {@link Date}, or null otherwise.
-     */
-    public final Date getDateOrNull() {
-        if (!isDate()) {
-            return null;
-        }
-        return mParsedDate;
-    }
-
-    /**
-     * @return whether the value case-insensitively equals to {@code s}.
-     */
-    public final boolean is(String s) {
-        if (s == null) {
-            return false;
-        }
-        return getString().equalsIgnoreCase(s);
-    }
-
-
-    /**
-     * @return whether the value case-insensitively starts with {@code s}.
-     */
-    public final boolean startsWith(String prefix) {
-        if (prefix == null) {
-            return false;
-        }
-        final String me = this.getString();
-        if (me.length() < prefix.length()) {
-            return false;
-        }
-        return me.substring(0, prefix.length()).equalsIgnoreCase(prefix);
-    }
-
-    // To force subclasses to implement it.
-    @Override
-    public abstract String toString();
-
-    @Override
-    public final boolean equalsForTest(ImapElement that) {
-        if (!super.equalsForTest(that)) {
-            return false;
-        }
-        ImapString thatString = (ImapString) that;
-        return getString().equals(thatString.getString());
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java b/java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java
deleted file mode 100644
index efe5c38..0000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.store.imap;
-
-import com.android.voicemailomtp.mail.FixedLengthInputStream;
-import com.android.voicemailomtp.mail.TempDirectory;
-import com.android.voicemailomtp.mail.utils.Utility;
-import com.android.voicemailomtp.mail.utils.LogUtils;
-
-import org.apache.commons.io.IOUtils;
-
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * Subclass of {@link ImapString} used for literals backed by a temp file.
- */
-public class ImapTempFileLiteral extends ImapString {
-    private final String TAG = "ImapTempFileLiteral";
-
-    /* package for test */ final File mFile;
-
-    /** Size is purely for toString() */
-    private final int mSize;
-
-    /* package */  ImapTempFileLiteral(FixedLengthInputStream stream) throws IOException {
-        mSize = stream.getLength();
-        mFile = File.createTempFile("imap", ".tmp", TempDirectory.getTempDirectory());
-
-        // Unfortunately, we can't really use deleteOnExit(), because temp filenames are random
-        // so it'd simply cause a memory leak.
-        // deleteOnExit() simply adds filenames to a static list and the list will never shrink.
-        // mFile.deleteOnExit();
-        OutputStream out = new FileOutputStream(mFile);
-        IOUtils.copy(stream, out);
-        out.close();
-    }
-
-    /**
-     * Make sure we delete the temp file.
-     *
-     * We should always be calling {@link ImapResponse#destroy()}, but it's here as a last resort.
-     */
-    @Override
-    protected void finalize() throws Throwable {
-        try {
-            destroy();
-        } finally {
-            super.finalize();
-        }
-    }
-
-    @Override
-    public InputStream getAsStream() {
-        checkNotDestroyed();
-        try {
-            return new FileInputStream(mFile);
-        } catch (FileNotFoundException e) {
-            // It's probably possible if we're low on storage and the system clears the cache dir.
-            LogUtils.w(TAG, "ImapTempFileLiteral: Temp file not found");
-
-            // Return 0 byte stream as a dummy...
-            return new ByteArrayInputStream(new byte[0]);
-        }
-    }
-
-    @Override
-    public String getString() {
-        checkNotDestroyed();
-        try {
-            byte[] bytes = IOUtils.toByteArray(getAsStream());
-            // Prevent crash from OOM; we've seen this, but only rarely and not reproducibly
-            if (bytes.length > ImapResponseParser.LITERAL_KEEP_IN_MEMORY_THRESHOLD) {
-                throw new IOException();
-            }
-            return Utility.fromAscii(bytes);
-        } catch (IOException e) {
-            LogUtils.w(TAG, "ImapTempFileLiteral: Error while reading temp file", e);
-            return "";
-        }
-    }
-
-    @Override
-    public void destroy() {
-        try {
-            if (!isDestroyed() && mFile.exists()) {
-                mFile.delete();
-            }
-        } catch (RuntimeException re) {
-            // Just log and ignore.
-            LogUtils.w(TAG, "Failed to remove temp file: " + re.getMessage());
-        }
-        super.destroy();
-    }
-
-    @Override
-    public String toString() {
-        return String.format("{%d byte literal(file)}", mSize);
-    }
-
-    public boolean tempFileExistsForTest() {
-        return mFile.exists();
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java b/java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java
deleted file mode 100644
index b045eb3..0000000
--- a/java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.store.imap;
-
-import com.android.voicemailomtp.mail.utils.LogUtils;
-
-import java.util.ArrayList;
-
-/**
- * Utility methods for use with IMAP.
- */
-public class ImapUtility {
-    public static final String TAG = "ImapUtility";
-    /**
-     * Apply quoting rules per IMAP RFC,
-     * quoted          = DQUOTE *QUOTED-CHAR DQUOTE
-     * QUOTED-CHAR     = <any TEXT-CHAR except quoted-specials> / "\" quoted-specials
-     * quoted-specials = DQUOTE / "\"
-     *
-     * This is used primarily for IMAP login, but might be useful elsewhere.
-     *
-     * NOTE:  Not very efficient - you may wish to preflight this, or perhaps it should check
-     * for trouble chars before calling the replace functions.
-     *
-     * @param s The string to be quoted.
-     * @return A copy of the string, having undergone quoting as described above
-     */
-    public static String imapQuoted(String s) {
-
-        // First, quote any backslashes by replacing \ with \\
-        // regex Pattern:  \\    (Java string const = \\\\)
-        // Substitute:     \\\\  (Java string const = \\\\\\\\)
-        String result = s.replaceAll("\\\\", "\\\\\\\\");
-
-        // Then, quote any double-quotes by replacing " with \"
-        // regex Pattern:  "    (Java string const = \")
-        // Substitute:     \\"  (Java string const = \\\\\")
-        result = result.replaceAll("\"", "\\\\\"");
-
-        // return string with quotes around it
-        return "\"" + result + "\"";
-    }
-
-    /**
-     * Gets all of the values in a sequence set per RFC 3501. Any ranges are expanded into a
-     * list of individual numbers. If the set is invalid, an empty array is returned.
-     * <pre>
-     * sequence-number = nz-number / "*"
-     * sequence-range  = sequence-number ":" sequence-number
-     * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
-     * </pre>
-     */
-    public static String[] getImapSequenceValues(String set) {
-        ArrayList<String> list = new ArrayList<String>();
-        if (set != null) {
-            String[] setItems = set.split(",");
-            for (String item : setItems) {
-                if (item.indexOf(':') == -1) {
-                    // simple item
-                    try {
-                        Integer.parseInt(item); // Don't need the value; just ensure it's valid
-                        list.add(item);
-                    } catch (NumberFormatException e) {
-                        LogUtils.d(TAG, "Invalid UID value", e);
-                    }
-                } else {
-                    // range
-                    for (String rangeItem : getImapRangeValues(item)) {
-                        list.add(rangeItem);
-                    }
-                }
-            }
-        }
-        String[] stringList = new String[list.size()];
-        return list.toArray(stringList);
-    }
-
-    /**
-     * Expand the given number range into a list of individual numbers. If the range is not valid,
-     * an empty array is returned.
-     * <pre>
-     * sequence-number = nz-number / "*"
-     * sequence-range  = sequence-number ":" sequence-number
-     * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
-     * </pre>
-     */
-    public static String[] getImapRangeValues(String range) {
-        ArrayList<String> list = new ArrayList<String>();
-        try {
-            if (range != null) {
-                int colonPos = range.indexOf(':');
-                if (colonPos > 0) {
-                    int first  = Integer.parseInt(range.substring(0, colonPos));
-                    int second = Integer.parseInt(range.substring(colonPos + 1));
-                    if (first < second) {
-                        for (int i = first; i <= second; i++) {
-                            list.add(Integer.toString(i));
-                        }
-                    } else {
-                        for (int i = first; i >= second; i--) {
-                            list.add(Integer.toString(i));
-                        }
-                    }
-                }
-            }
-        } catch (NumberFormatException e) {
-            LogUtils.d(TAG, "Invalid range value", e);
-        }
-        String[] stringList = new String[list.size()];
-        return list.toArray(stringList);
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/utility/CountingOutputStream.java b/java/com/android/voicemailomtp/mail/utility/CountingOutputStream.java
deleted file mode 100644
index fdf81d4..0000000
--- a/java/com/android/voicemailomtp/mail/utility/CountingOutputStream.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.utility;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-/**
- * A simple pass-thru OutputStream that also counts how many bytes are written to it and
- * makes that count available to callers.
- */
-public class CountingOutputStream extends OutputStream {
-    private long mCount;
-    private final OutputStream mOutputStream;
-
-    public CountingOutputStream(OutputStream outputStream) {
-        mOutputStream = outputStream;
-    }
-
-    public long getCount() {
-        return mCount;
-    }
-
-    @Override
-    public void write(byte[] buffer, int offset, int count) throws IOException {
-        mOutputStream.write(buffer, offset, count);
-        mCount += count;
-    }
-
-    @Override
-    public void write(int oneByte) throws IOException {
-        mOutputStream.write(oneByte);
-        mCount++;
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/utility/EOLConvertingOutputStream.java b/java/com/android/voicemailomtp/mail/utility/EOLConvertingOutputStream.java
deleted file mode 100644
index 5b93a92..0000000
--- a/java/com/android/voicemailomtp/mail/utility/EOLConvertingOutputStream.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.utility;
-
-import java.io.FilterOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-
-public class EOLConvertingOutputStream extends FilterOutputStream {
-    int lastChar;
-
-    public EOLConvertingOutputStream(OutputStream out) {
-        super(out);
-    }
-
-    @Override
-    public void write(int oneByte) throws IOException {
-        if (oneByte == '\n') {
-            if (lastChar != '\r') {
-                super.write('\r');
-            }
-        }
-        super.write(oneByte);
-        lastChar = oneByte;
-    }
-
-    @Override
-    public void flush() throws IOException {
-        if (lastChar == '\r') {
-            super.write('\n');
-            lastChar = '\n';
-        }
-        super.flush();
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/utils/LogUtils.java b/java/com/android/voicemailomtp/mail/utils/LogUtils.java
deleted file mode 100644
index a213a83..0000000
--- a/java/com/android/voicemailomtp/mail/utils/LogUtils.java
+++ /dev/null
@@ -1,413 +0,0 @@
-/**
- * Copyright (c) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.utils;
-
-import android.net.Uri;
-import android.support.annotation.VisibleForTesting;
-import android.text.TextUtils;
-import android.util.Log;
-import com.android.voicemailomtp.VvmLog;
-import java.util.List;
-import java.util.regex.Pattern;
-
-public class LogUtils {
-    public static final String TAG = "Email Log";
-
-    // "GMT" + "+" or "-" + 4 digits
-    private static final Pattern DATE_CLEANUP_PATTERN_WRONG_TIMEZONE =
-            Pattern.compile("GMT([-+]\\d{4})$");
-
-    private static final String ACCOUNT_PREFIX = "account:";
-
-    /**
-     * Priority constant for the println method; use LogUtils.v.
-     */
-    public static final int VERBOSE = Log.VERBOSE;
-
-    /**
-     * Priority constant for the println method; use LogUtils.d.
-     */
-    public static final int DEBUG = Log.DEBUG;
-
-    /**
-     * Priority constant for the println method; use LogUtils.i.
-     */
-    public static final int INFO = Log.INFO;
-
-    /**
-     * Priority constant for the println method; use LogUtils.w.
-     */
-    public static final int WARN = Log.WARN;
-
-    /**
-     * Priority constant for the println method; use LogUtils.e.
-     */
-    public static final int ERROR = Log.ERROR;
-
-  /**
-   * Used to enable/disable logging that we don't want included in production releases. This should
-   * be set to DEBUG for production releases, and VERBOSE for internal builds.
-   */
-  private static final int MAX_ENABLED_LOG_LEVEL = DEBUG;
-
-    private static Boolean sDebugLoggingEnabledForTests = null;
-
-    /**
-     * Enable debug logging for unit tests.
-     */
-    @VisibleForTesting
-    public static void setDebugLoggingEnabledForTests(boolean enabled) {
-        setDebugLoggingEnabledForTestsInternal(enabled);
-    }
-
-    protected static void setDebugLoggingEnabledForTestsInternal(boolean enabled) {
-        sDebugLoggingEnabledForTests = Boolean.valueOf(enabled);
-    }
-
-    /**
-     * Returns true if the build configuration prevents debug logging.
-     */
-    @VisibleForTesting
-    public static boolean buildPreventsDebugLogging() {
-        return MAX_ENABLED_LOG_LEVEL > VERBOSE;
-    }
-
-    /**
-     * Returns a boolean indicating whether debug logging is enabled.
-     */
-    protected static boolean isDebugLoggingEnabled(String tag) {
-        if (buildPreventsDebugLogging()) {
-            return false;
-        }
-        if (sDebugLoggingEnabledForTests != null) {
-            return sDebugLoggingEnabledForTests.booleanValue();
-        }
-        return Log.isLoggable(tag, Log.DEBUG) || Log.isLoggable(TAG, Log.DEBUG);
-    }
-
-    /**
-     * Returns a String for the specified content provider uri.  This will do
-     * sanitation of the uri to remove PII if debug logging is not enabled.
-     */
-    public static String contentUriToString(final Uri uri) {
-        return contentUriToString(TAG, uri);
-    }
-
-    /**
-     * Returns a String for the specified content provider uri.  This will do
-     * sanitation of the uri to remove PII if debug logging is not enabled.
-     */
-    public static String contentUriToString(String tag, Uri uri) {
-        if (isDebugLoggingEnabled(tag)) {
-            // Debug logging has been enabled, so log the uri as is
-            return uri.toString();
-        } else {
-            // Debug logging is not enabled, we want to remove the email address from the uri.
-            List<String> pathSegments = uri.getPathSegments();
-
-            Uri.Builder builder = new Uri.Builder()
-                    .scheme(uri.getScheme())
-                    .authority(uri.getAuthority())
-                    .query(uri.getQuery())
-                    .fragment(uri.getFragment());
-
-            // This assumes that the first path segment is the account
-            final String account = pathSegments.get(0);
-
-            builder = builder.appendPath(sanitizeAccountName(account));
-            for (int i = 1; i < pathSegments.size(); i++) {
-                builder.appendPath(pathSegments.get(i));
-            }
-            return builder.toString();
-        }
-    }
-
-    /**
-     * Sanitizes an account name.  If debug logging is not enabled, a sanitized name
-     * is returned.
-     */
-    public static String sanitizeAccountName(String accountName) {
-        if (TextUtils.isEmpty(accountName)) {
-            return "";
-        }
-
-        return ACCOUNT_PREFIX + sanitizeName(TAG, accountName);
-    }
-
-    public static String sanitizeName(final String tag, final String name) {
-        if (TextUtils.isEmpty(name)) {
-            return "";
-        }
-
-        if (isDebugLoggingEnabled(tag)) {
-            return name;
-        }
-
-        return String.valueOf(name.hashCode());
-    }
-
-    /**
-     * Checks to see whether or not a log for the specified tag is loggable at the specified level.
-     */
-    public static boolean isLoggable(String tag, int level) {
-        if (MAX_ENABLED_LOG_LEVEL > level) {
-            return false;
-        }
-        return Log.isLoggable(tag, level) || Log.isLoggable(TAG, level);
-    }
-
-    /**
-     * Send a {@link #VERBOSE} log message.
-     * @param tag Used to identify the source of a log message.  It usually identifies
-     *        the class or activity where the log call occurs.
-     * @param format the format string (see {@link java.util.Formatter#format})
-     * @param args
-     *            the list of arguments passed to the formatter. If there are
-     *            more arguments than required by {@code format},
-     *            additional arguments are ignored.
-     */
-    public static int v(String tag, String format, Object... args) {
-        if (isLoggable(tag, VERBOSE)) {
-            return VvmLog.v(tag, String.format(format, args));
-        }
-        return 0;
-    }
-
-    /**
-     * Send a {@link #VERBOSE} log message.
-     * @param tag Used to identify the source of a log message.  It usually identifies
-     *        the class or activity where the log call occurs.
-     * @param tr An exception to log
-     * @param format the format string (see {@link java.util.Formatter#format})
-     * @param args
-     *            the list of arguments passed to the formatter. If there are
-     *            more arguments than required by {@code format},
-     *            additional arguments are ignored.
-     */
-    public static int v(String tag, Throwable tr, String format, Object... args) {
-        if (isLoggable(tag, VERBOSE)) {
-            return VvmLog.v(tag, String.format(format, args), tr);
-        }
-        return 0;
-    }
-
-    /**
-     * Send a {@link #DEBUG} log message.
-     * @param tag Used to identify the source of a log message.  It usually identifies
-     *        the class or activity where the log call occurs.
-     * @param format the format string (see {@link java.util.Formatter#format})
-     * @param args
-     *            the list of arguments passed to the formatter. If there are
-     *            more arguments than required by {@code format},
-     *            additional arguments are ignored.
-     */
-    public static int d(String tag, String format, Object... args) {
-        if (isLoggable(tag, DEBUG)) {
-            return VvmLog.d(tag, String.format(format, args));
-        }
-        return 0;
-    }
-
-    /**
-     * Send a {@link #DEBUG} log message.
-     * @param tag Used to identify the source of a log message.  It usually identifies
-     *        the class or activity where the log call occurs.
-     * @param tr An exception to log
-     * @param format the format string (see {@link java.util.Formatter#format})
-     * @param args
-     *            the list of arguments passed to the formatter. If there are
-     *            more arguments than required by {@code format},
-     *            additional arguments are ignored.
-     */
-    public static int d(String tag, Throwable tr, String format, Object... args) {
-        if (isLoggable(tag, DEBUG)) {
-            return VvmLog.d(tag, String.format(format, args), tr);
-        }
-        return 0;
-    }
-
-    /**
-     * Send a {@link #INFO} log message.
-     * @param tag Used to identify the source of a log message.  It usually identifies
-     *        the class or activity where the log call occurs.
-     * @param format the format string (see {@link java.util.Formatter#format})
-     * @param args
-     *            the list of arguments passed to the formatter. If there are
-     *            more arguments than required by {@code format},
-     *            additional arguments are ignored.
-     */
-    public static int i(String tag, String format, Object... args) {
-        if (isLoggable(tag, INFO)) {
-            return VvmLog.i(tag, String.format(format, args));
-        }
-        return 0;
-    }
-
-    /**
-     * Send a {@link #INFO} log message.
-     * @param tag Used to identify the source of a log message.  It usually identifies
-     *        the class or activity where the log call occurs.
-     * @param tr An exception to log
-     * @param format the format string (see {@link java.util.Formatter#format})
-     * @param args
-     *            the list of arguments passed to the formatter. If there are
-     *            more arguments than required by {@code format},
-     *            additional arguments are ignored.
-     */
-    public static int i(String tag, Throwable tr, String format, Object... args) {
-        if (isLoggable(tag, INFO)) {
-            return VvmLog.i(tag, String.format(format, args), tr);
-        }
-        return 0;
-    }
-
-    /**
-     * Send a {@link #WARN} log message.
-     * @param tag Used to identify the source of a log message.  It usually identifies
-     *        the class or activity where the log call occurs.
-     * @param format the format string (see {@link java.util.Formatter#format})
-     * @param args
-     *            the list of arguments passed to the formatter. If there are
-     *            more arguments than required by {@code format},
-     *            additional arguments are ignored.
-     */
-    public static int w(String tag, String format, Object... args) {
-        if (isLoggable(tag, WARN)) {
-            return VvmLog.w(tag, String.format(format, args));
-        }
-        return 0;
-    }
-
-    /**
-     * Send a {@link #WARN} log message.
-     * @param tag Used to identify the source of a log message.  It usually identifies
-     *        the class or activity where the log call occurs.
-     * @param tr An exception to log
-     * @param format the format string (see {@link java.util.Formatter#format})
-     * @param args
-     *            the list of arguments passed to the formatter. If there are
-     *            more arguments than required by {@code format},
-     *            additional arguments are ignored.
-     */
-    public static int w(String tag, Throwable tr, String format, Object... args) {
-        if (isLoggable(tag, WARN)) {
-            return VvmLog.w(tag, String.format(format, args), tr);
-        }
-        return 0;
-    }
-
-    /**
-     * Send a {@link #ERROR} log message.
-     * @param tag Used to identify the source of a log message.  It usually identifies
-     *        the class or activity where the log call occurs.
-     * @param format the format string (see {@link java.util.Formatter#format})
-     * @param args
-     *            the list of arguments passed to the formatter. If there are
-     *            more arguments than required by {@code format},
-     *            additional arguments are ignored.
-     */
-    public static int e(String tag, String format, Object... args) {
-        if (isLoggable(tag, ERROR)) {
-            return VvmLog.e(tag, String.format(format, args));
-        }
-        return 0;
-    }
-
-    /**
-     * Send a {@link #ERROR} log message.
-     * @param tag Used to identify the source of a log message.  It usually identifies
-     *        the class or activity where the log call occurs.
-     * @param tr An exception to log
-     * @param format the format string (see {@link java.util.Formatter#format})
-     * @param args
-     *            the list of arguments passed to the formatter. If there are
-     *            more arguments than required by {@code format},
-     *            additional arguments are ignored.
-     */
-    public static int e(String tag, Throwable tr, String format, Object... args) {
-        if (isLoggable(tag, ERROR)) {
-            return VvmLog.e(tag, String.format(format, args), tr);
-        }
-        return 0;
-    }
-
-    /**
-     * What a Terrible Failure: Report a condition that should never happen.
-     * The error will always be logged at level ASSERT with the call stack.
-     * Depending on system configuration, a report may be added to the
-     * {@link android.os.DropBoxManager} and/or the process may be terminated
-     * immediately with an error dialog.
-     * @param tag Used to identify the source of a log message.  It usually identifies
-     *        the class or activity where the log call occurs.
-     * @param format the format string (see {@link java.util.Formatter#format})
-     * @param args
-     *            the list of arguments passed to the formatter. If there are
-     *            more arguments than required by {@code format},
-     *            additional arguments are ignored.
-     */
-    public static int wtf(String tag, String format, Object... args) {
-        return VvmLog.wtf(tag, String.format(format, args), new Error());
-    }
-
-    /**
-     * What a Terrible Failure: Report a condition that should never happen.
-     * The error will always be logged at level ASSERT with the call stack.
-     * Depending on system configuration, a report may be added to the
-     * {@link android.os.DropBoxManager} and/or the process may be terminated
-     * immediately with an error dialog.
-     * @param tag Used to identify the source of a log message.  It usually identifies
-     *        the class or activity where the log call occurs.
-     * @param tr An exception to log
-     * @param format the format string (see {@link java.util.Formatter#format})
-     * @param args
-     *            the list of arguments passed to the formatter. If there are
-     *            more arguments than required by {@code format},
-     *            additional arguments are ignored.
-     */
-    public static int wtf(String tag, Throwable tr, String format, Object... args) {
-        return VvmLog.wtf(tag, String.format(format, args), tr);
-    }
-
-
-    /**
-     * Try to make a date MIME(RFC 2822/5322)-compliant.
-     *
-     * It fixes:
-     * - "Thu, 10 Dec 09 15:08:08 GMT-0700" to "Thu, 10 Dec 09 15:08:08 -0700"
-     *   (4 digit zone value can't be preceded by "GMT")
-     *   We got a report saying eBay sends a date in this format
-     */
-    public static String cleanUpMimeDate(String date) {
-        if (TextUtils.isEmpty(date)) {
-            return date;
-        }
-        date = DATE_CLEANUP_PATTERN_WRONG_TIMEZONE.matcher(date).replaceFirst("$1");
-        return date;
-    }
-
-
-    public static String byteToHex(int b) {
-        return byteToHex(new StringBuilder(), b).toString();
-    }
-
-    public static StringBuilder byteToHex(StringBuilder sb, int b) {
-        b &= 0xFF;
-        sb.append("0123456789ABCDEF".charAt(b >> 4));
-        sb.append("0123456789ABCDEF".charAt(b & 0xF));
-        return sb;
-    }
-
-}
diff --git a/java/com/android/voicemailomtp/mail/utils/Utility.java b/java/com/android/voicemailomtp/mail/utils/Utility.java
deleted file mode 100644
index c7286fa..0000000
--- a/java/com/android/voicemailomtp/mail/utils/Utility.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- * Copyright (c) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.voicemailomtp.mail.utils;
-
-import java.io.ByteArrayInputStream;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.charset.Charset;
-
-/**
- * Simple utility methods used in email functions.
- */
-public class Utility {
-    public static final Charset ASCII = Charset.forName("US-ASCII");
-
-    public static final String[] EMPTY_STRINGS = new String[0];
-
-    /**
-     * Returns a concatenated string containing the output of every Object's
-     * toString() method, each separated by the given separator character.
-     */
-    public static String combine(Object[] parts, char separator) {
-        if (parts == null) {
-            return null;
-        }
-        StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < parts.length; i++) {
-            sb.append(parts[i].toString());
-            if (i < parts.length - 1) {
-                sb.append(separator);
-            }
-        }
-        return sb.toString();
-    }
-
-    /** Converts a String to ASCII bytes */
-    public static byte[] toAscii(String s) {
-        return encode(ASCII, s);
-    }
-
-    /** Builds a String from ASCII bytes */
-    public static String fromAscii(byte[] b) {
-        return decode(ASCII, b);
-    }
-
-    private static byte[] encode(Charset charset, String s) {
-        if (s == null) {
-            return null;
-        }
-        final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s));
-        final byte[] bytes = new byte[buffer.limit()];
-        buffer.get(bytes);
-        return bytes;
-    }
-
-    private static String decode(Charset charset, byte[] b) {
-        if (b == null) {
-            return null;
-        }
-        final CharBuffer cb = charset.decode(ByteBuffer.wrap(b));
-        return new String(cb.array(), 0, cb.length());
-    }
-
-    public static ByteArrayInputStream streamFromAsciiString(String ascii) {
-        return new ByteArrayInputStream(toAscii(ascii));
-    }
-}
diff --git a/java/com/android/voicemailomtp/protocol/CvvmProtocol.java b/java/com/android/voicemailomtp/protocol/CvvmProtocol.java
deleted file mode 100644
index 48ed997..0000000
--- a/java/com/android/voicemailomtp/protocol/CvvmProtocol.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.protocol;
-
-import android.content.Context;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SmsManager;
-
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.sms.OmtpCvvmMessageSender;
-import com.android.voicemailomtp.sms.OmtpMessageSender;
-
-/**
- * A flavor of OMTP protocol with a different mobile originated (MO) format
- *
- * Used by carriers such as T-Mobile
- */
-public class CvvmProtocol extends VisualVoicemailProtocol {
-
-    private static String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
-    private static String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s";
-    private static String IMAP_CLOSE_NUT = "CLOSE_NUT";
-
-    @Override
-    public OmtpMessageSender createMessageSender(Context context,
-            PhoneAccountHandle phoneAccountHandle, short applicationPort,
-            String destinationNumber) {
-        return new OmtpCvvmMessageSender(context, phoneAccountHandle, applicationPort,
-                destinationNumber);
-    }
-
-    @Override
-    public String getCommand(String command) {
-        if (command == OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT) {
-            return IMAP_CHANGE_TUI_PWD_FORMAT;
-        }
-        if (command == OmtpConstants.IMAP_CLOSE_NUT) {
-            return IMAP_CLOSE_NUT;
-        }
-        if (command == OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT) {
-            return IMAP_CHANGE_VM_LANG_FORMAT;
-        }
-        return super.getCommand(command);
-    }
-}
diff --git a/java/com/android/voicemailomtp/protocol/OmtpProtocol.java b/java/com/android/voicemailomtp/protocol/OmtpProtocol.java
deleted file mode 100644
index d88a232..0000000
--- a/java/com/android/voicemailomtp/protocol/OmtpProtocol.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.protocol;
-
-import android.content.Context;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SmsManager;
-
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.sms.OmtpMessageSender;
-import com.android.voicemailomtp.sms.OmtpStandardMessageSender;
-
-public class OmtpProtocol extends VisualVoicemailProtocol {
-
-    @Override
-    public OmtpMessageSender createMessageSender(Context context,
-            PhoneAccountHandle phoneAccountHandle, short applicationPort,
-            String destinationNumber) {
-        return new OmtpStandardMessageSender(context, phoneAccountHandle, applicationPort,
-                destinationNumber,
-                null, OmtpConstants.PROTOCOL_VERSION1_1, null);
-    }
-}
diff --git a/java/com/android/voicemailomtp/protocol/ProtocolHelper.java b/java/com/android/voicemailomtp/protocol/ProtocolHelper.java
deleted file mode 100644
index 4fca199..0000000
--- a/java/com/android/voicemailomtp/protocol/ProtocolHelper.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.protocol;
-
-import android.telephony.SmsManager;
-import android.text.TextUtils;
-
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.sms.OmtpMessageSender;
-
-public class ProtocolHelper {
-
-    private static final String TAG = "ProtocolHelper";
-
-    public static OmtpMessageSender getMessageSender(VisualVoicemailProtocol protocol,
-            OmtpVvmCarrierConfigHelper config) {
-
-        int applicationPort = config.getApplicationPort();
-        String destinationNumber = config.getDestinationNumber();
-        if (TextUtils.isEmpty(destinationNumber)) {
-            VvmLog.w(TAG, "No destination number for this carrier.");
-            return null;
-        }
-
-        return protocol.createMessageSender(config.getContext(), config.getPhoneAccountHandle(),
-                (short) applicationPort, destinationNumber);
-    }
-}
diff --git a/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java b/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java
deleted file mode 100644
index 9ff2ed1..0000000
--- a/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.protocol;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SmsManager;
-import com.android.voicemailomtp.ActivationTask;
-import com.android.voicemailomtp.DefaultOmtpEventHandler;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.sms.OmtpMessageSender;
-import com.android.voicemailomtp.sms.StatusMessage;
-
-public abstract class VisualVoicemailProtocol {
-
-    /**
-     * Activation should cause the carrier to respond with a STATUS SMS.
-     */
-    public void startActivation(OmtpVvmCarrierConfigHelper config, PendingIntent sentIntent) {
-        OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
-        if (messageSender != null) {
-            messageSender.requestVvmActivation(sentIntent);
-        }
-    }
-
-    public void startDeactivation(OmtpVvmCarrierConfigHelper config) {
-        OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
-        if (messageSender != null) {
-            messageSender.requestVvmDeactivation(null);
-        }
-    }
-
-    public boolean supportsProvisioning() {
-        return false;
-    }
-
-    public void startProvisioning(ActivationTask task, PhoneAccountHandle handle,
-        OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor editor, StatusMessage message,
-        Bundle data) {
-        // Do nothing
-    }
-
-    public void requestStatus(OmtpVvmCarrierConfigHelper config,
-            @Nullable PendingIntent sentIntent) {
-        OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
-        if (messageSender != null) {
-            messageSender.requestVvmStatus(sentIntent);
-        }
-    }
-
-    public abstract OmtpMessageSender createMessageSender(Context context,
-            PhoneAccountHandle phoneAccountHandle,
-            short applicationPort, String destinationNumber);
-
-    /**
-     * Translate an OMTP IMAP command to the protocol specific one. For example, changing the TUI
-     * password on OMTP is XCHANGE_TUI_PWD, but on CVVM and VVM3 it is CHANGE_TUI_PWD.
-     *
-     * @param command A String command in {@link com.android.voicemailomtp.OmtpConstants}, the exact
-     * instance should be used instead of its' value.
-     * @returns Translated command, or {@code null} if not available in this protocol
-     */
-    public String getCommand(String command) {
-        return command;
-    }
-
-    public void handleEvent(Context context, OmtpVvmCarrierConfigHelper config,
-        VoicemailStatus.Editor status, OmtpEvents event) {
-        DefaultOmtpEventHandler.handleEvent(context, config, status, event);
-    }
-
-    /**
-     * Given an VVM SMS with an unknown {@code event}, let the protocol attempt to translate it into
-     * an equivalent STATUS SMS. Returns {@code null} if it cannot be translated.
-     */
-    @Nullable
-    public Bundle translateStatusSmsBundle(OmtpVvmCarrierConfigHelper config, String event,
-            Bundle data) {
-        return null;
-    }
-}
diff --git a/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java b/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java
deleted file mode 100644
index b74f503..0000000
--- a/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.protocol;
-
-import android.content.res.Resources;
-import android.support.annotation.Nullable;
-import android.telephony.TelephonyManager;
-import com.android.voicemailomtp.VvmLog;
-
-public class VisualVoicemailProtocolFactory {
-
-    private static final String TAG = "VvmProtocolFactory";
-
-    private static final String VVM_TYPE_VVM3 = "vvm_type_vvm3";
-
-    @Nullable
-    public static VisualVoicemailProtocol create(Resources resources, String type) {
-        if (type == null) {
-            return null;
-        }
-        switch (type) {
-            case TelephonyManager.VVM_TYPE_OMTP:
-                return new OmtpProtocol();
-            case TelephonyManager.VVM_TYPE_CVVM:
-                return new CvvmProtocol();
-            case VVM_TYPE_VVM3:
-                return new Vvm3Protocol();
-            default:
-                VvmLog.e(TAG, "Unexpected visual voicemail type: " + type);
-        }
-        return null;
-    }
-}
diff --git a/java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java b/java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java
deleted file mode 100644
index 7264638..0000000
--- a/java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.protocol;
-
-import android.content.Context;
-import android.support.annotation.IntDef;
-import android.util.Log;
-import com.android.voicemailomtp.DefaultOmtpEventHandler;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.OmtpEvents.Type;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.settings.VoicemailChangePinActivity;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Handles {@link OmtpEvents} when {@link Vvm3Protocol} is being used. This handler writes custom
- * error codes into the voicemail status table so support on the dialer side is required.
- *
- * TODO(b/29577838) disable VVM3 by default so support on system dialer can be ensured.
- */
-public class Vvm3EventHandler {
-
-    private static final String TAG = "Vvm3EventHandler";
-
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef({VMS_DNS_FAILURE, VMG_DNS_FAILURE, SPG_DNS_FAILURE, VMS_NO_CELLULAR, VMG_NO_CELLULAR,
-            SPG_NO_CELLULAR, VMS_TIMEOUT, VMG_TIMEOUT, STATUS_SMS_TIMEOUT, SUBSCRIBER_BLOCKED,
-            UNKNOWN_USER, UNKNOWN_DEVICE, INVALID_PASSWORD, MAILBOX_NOT_INITIALIZED,
-            SERVICE_NOT_PROVISIONED, SERVICE_NOT_ACTIVATED, USER_BLOCKED, IMAP_GETQUOTA_ERROR,
-            IMAP_SELECT_ERROR, IMAP_ERROR, VMG_INTERNAL_ERROR, VMG_DB_ERROR,
-            VMG_COMMUNICATION_ERROR, SPG_URL_NOT_FOUND, VMG_UNKNOWN_ERROR, PIN_NOT_SET})
-    public @interface ErrorCode {
-
-    }
-
-    public static final int VMS_DNS_FAILURE = -9001;
-    public static final int VMG_DNS_FAILURE = -9002;
-    public static final int SPG_DNS_FAILURE = -9003;
-    public static final int VMS_NO_CELLULAR = -9004;
-    public static final int VMG_NO_CELLULAR = -9005;
-    public static final int SPG_NO_CELLULAR = -9006;
-    public static final int VMS_TIMEOUT = -9007;
-    public static final int VMG_TIMEOUT = -9008;
-    public static final int STATUS_SMS_TIMEOUT = -9009;
-
-    public static final int SUBSCRIBER_BLOCKED = -9990;
-    public static final int UNKNOWN_USER = -9991;
-    public static final int UNKNOWN_DEVICE = -9992;
-    public static final int INVALID_PASSWORD = -9993;
-    public static final int MAILBOX_NOT_INITIALIZED = -9994;
-    public static final int SERVICE_NOT_PROVISIONED = -9995;
-    public static final int SERVICE_NOT_ACTIVATED = -9996;
-    public static final int USER_BLOCKED = -9998;
-    public static final int IMAP_GETQUOTA_ERROR = -9997;
-    public static final int IMAP_SELECT_ERROR = -9989;
-    public static final int IMAP_ERROR = -9999;
-
-    public static final int VMG_INTERNAL_ERROR = -101;
-    public static final int VMG_DB_ERROR = -102;
-    public static final int VMG_COMMUNICATION_ERROR = -103;
-    public static final int SPG_URL_NOT_FOUND = -301;
-
-    // Non VVM3 codes:
-    public static final int VMG_UNKNOWN_ERROR = -1;
-    public static final int PIN_NOT_SET = -100;
-    // STATUS SMS returned st=U and rc!=2. The user cannot be provisioned and must contact customer
-    // support.
-    public static final int SUBSCRIBER_UNKNOWN = -99;
-
-
-    public static void handleEvent(Context context, OmtpVvmCarrierConfigHelper config,
-        VoicemailStatus.Editor status, OmtpEvents event) {
-        boolean handled = false;
-        switch (event.getType()) {
-            case Type.CONFIGURATION:
-                handled = handleConfigurationEvent(context, status, event);
-                break;
-            case Type.DATA_CHANNEL:
-                handled = handleDataChannelEvent(status, event);
-                break;
-            case Type.NOTIFICATION_CHANNEL:
-                handled = handleNotificationChannelEvent(status, event);
-                break;
-            case Type.OTHER:
-                handled = handleOtherEvent(status, event);
-                break;
-            default:
-                Log.wtf(TAG, "invalid event type " + event.getType() + " for " + event);
-        }
-        if (!handled) {
-            DefaultOmtpEventHandler.handleEvent(context, config, status, event);
-        }
-    }
-
-    private static boolean handleConfigurationEvent(Context context, VoicemailStatus.Editor status,
-        OmtpEvents event) {
-        switch (event) {
-            case CONFIG_REQUEST_STATUS_SUCCESS:
-                if (status.getPhoneAccountHandle() == null) {
-                    // This should never happen.
-                    Log.e(TAG, "status editor has null phone account handle");
-                    return true;
-                }
-
-                if (!VoicemailChangePinActivity
-                    .isDefaultOldPinSet(context, status.getPhoneAccountHandle())) {
-                    return false;
-                } else {
-                    postError(status, PIN_NOT_SET);
-                }
-                break;
-            case CONFIG_DEFAULT_PIN_REPLACED:
-                postError(status, PIN_NOT_SET);
-                break;
-            case CONFIG_STATUS_SMS_TIME_OUT:
-                postError(status, STATUS_SMS_TIMEOUT);
-                break;
-            default:
-                return false;
-        }
-        return true;
-    }
-
-    private static boolean handleDataChannelEvent(VoicemailStatus.Editor status, OmtpEvents event) {
-        switch (event) {
-            case DATA_NO_CONNECTION:
-            case DATA_NO_CONNECTION_CELLULAR_REQUIRED:
-            case DATA_ALL_SOCKET_CONNECTION_FAILED:
-                postError(status, VMS_NO_CELLULAR);
-                break;
-            case DATA_SSL_INVALID_HOST_NAME:
-            case DATA_CANNOT_ESTABLISH_SSL_SESSION:
-            case DATA_IOE_ON_OPEN:
-                postError(status, VMS_TIMEOUT);
-                break;
-            case DATA_CANNOT_RESOLVE_HOST_ON_NETWORK:
-                postError(status, VMS_DNS_FAILURE);
-                break;
-            case DATA_BAD_IMAP_CREDENTIAL:
-                postError(status, IMAP_ERROR);
-                break;
-            case DATA_AUTH_UNKNOWN_USER:
-                postError(status, UNKNOWN_USER);
-                break;
-            case DATA_AUTH_UNKNOWN_DEVICE:
-                postError(status, UNKNOWN_DEVICE);
-                break;
-            case DATA_AUTH_INVALID_PASSWORD:
-                postError(status, INVALID_PASSWORD);
-                break;
-            case DATA_AUTH_MAILBOX_NOT_INITIALIZED:
-                postError(status, MAILBOX_NOT_INITIALIZED);
-                break;
-            case DATA_AUTH_SERVICE_NOT_PROVISIONED:
-                postError(status, SERVICE_NOT_PROVISIONED);
-                break;
-            case DATA_AUTH_SERVICE_NOT_ACTIVATED:
-                postError(status, SERVICE_NOT_ACTIVATED);
-                break;
-            case DATA_AUTH_USER_IS_BLOCKED:
-                postError(status, USER_BLOCKED);
-                break;
-            case DATA_REJECTED_SERVER_RESPONSE:
-            case DATA_INVALID_INITIAL_SERVER_RESPONSE:
-            case DATA_SSL_EXCEPTION:
-                postError(status, IMAP_ERROR);
-                break;
-            default:
-                return false;
-        }
-        return true;
-    }
-
-    private static boolean handleNotificationChannelEvent(VoicemailStatus.Editor status,
-        OmtpEvents event) {
-        return false;
-    }
-
-    private static boolean handleOtherEvent(VoicemailStatus.Editor status,
-            OmtpEvents event) {
-        switch (event) {
-            case VVM3_NEW_USER_SETUP_FAILED:
-                postError(status, MAILBOX_NOT_INITIALIZED);
-                break;
-            case VVM3_VMG_DNS_FAILURE:
-                postError(status, VMG_DNS_FAILURE);
-                break;
-            case VVM3_SPG_DNS_FAILURE:
-                postError(status, SPG_DNS_FAILURE);
-                break;
-            case VVM3_VMG_CONNECTION_FAILED:
-                postError(status, VMG_NO_CELLULAR);
-                break;
-            case VVM3_SPG_CONNECTION_FAILED:
-                postError(status, SPG_NO_CELLULAR);
-                break;
-            case VVM3_VMG_TIMEOUT:
-                postError(status, VMG_TIMEOUT);
-                break;
-            case VVM3_SUBSCRIBER_PROVISIONED:
-                postError(status, SERVICE_NOT_ACTIVATED);
-                break;
-            case VVM3_SUBSCRIBER_BLOCKED:
-                postError(status, SUBSCRIBER_BLOCKED);
-                break;
-            case VVM3_SUBSCRIBER_UNKNOWN:
-                postError(status, SUBSCRIBER_UNKNOWN);
-                break;
-            default:
-                return false;
-        }
-        return true;
-    }
-
-    private static void postError(VoicemailStatus.Editor editor, @ErrorCode int errorCode) {
-        switch (errorCode) {
-            case VMG_DNS_FAILURE:
-            case SPG_DNS_FAILURE:
-            case VMG_NO_CELLULAR:
-            case SPG_NO_CELLULAR:
-            case VMG_TIMEOUT:
-            case SUBSCRIBER_BLOCKED:
-            case UNKNOWN_USER:
-            case UNKNOWN_DEVICE:
-            case INVALID_PASSWORD:
-            case MAILBOX_NOT_INITIALIZED:
-            case SERVICE_NOT_PROVISIONED:
-            case SERVICE_NOT_ACTIVATED:
-            case USER_BLOCKED:
-            case VMG_UNKNOWN_ERROR:
-            case SPG_URL_NOT_FOUND:
-            case VMG_INTERNAL_ERROR:
-            case VMG_DB_ERROR:
-            case VMG_COMMUNICATION_ERROR:
-            case PIN_NOT_SET:
-            case SUBSCRIBER_UNKNOWN:
-                editor.setConfigurationState(errorCode);
-                break;
-            case VMS_NO_CELLULAR:
-            case VMS_DNS_FAILURE:
-            case VMS_TIMEOUT:
-            case IMAP_GETQUOTA_ERROR:
-            case IMAP_SELECT_ERROR:
-            case IMAP_ERROR:
-                editor.setDataChannelState(errorCode);
-                break;
-            case STATUS_SMS_TIMEOUT:
-                editor.setNotificationChannelState(errorCode);
-                break;
-            default:
-                Log.wtf(TAG, "unknown error code: " + errorCode);
-        }
-        editor.apply();
-    }
-}
diff --git a/java/com/android/voicemailomtp/protocol/Vvm3Protocol.java b/java/com/android/voicemailomtp/protocol/Vvm3Protocol.java
deleted file mode 100644
index 652d101..0000000
--- a/java/com/android/voicemailomtp/protocol/Vvm3Protocol.java
+++ /dev/null
@@ -1,301 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.protocol;
-
-import android.annotation.TargetApi;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.net.Network;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import android.text.TextUtils;
-import com.android.voicemailomtp.ActivationTask;
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VisualVoicemailPreferences;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.imap.ImapHelper;
-import com.android.voicemailomtp.imap.ImapHelper.InitializingException;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil;
-import com.android.voicemailomtp.settings.VoicemailChangePinActivity;
-import com.android.voicemailomtp.sms.OmtpMessageSender;
-import com.android.voicemailomtp.sms.StatusMessage;
-import com.android.voicemailomtp.sms.Vvm3MessageSender;
-import com.android.voicemailomtp.sync.VvmNetworkRequest;
-import com.android.voicemailomtp.sync.VvmNetworkRequest.NetworkWrapper;
-import com.android.voicemailomtp.sync.VvmNetworkRequest.RequestFailedException;
-import java.io.IOException;
-import java.security.SecureRandom;
-import java.util.Locale;
-
-/**
- * A flavor of OMTP protocol with a different provisioning process
- *
- * <p>Used by carriers such as Verizon Wireless
- */
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class Vvm3Protocol extends VisualVoicemailProtocol {
-
-    private static final String TAG = "Vvm3Protocol";
-
-    private static final String SMS_EVENT_UNRECOGNIZED = "UNRECOGNIZED";
-    private static final String SMS_EVENT_UNRECOGNIZED_CMD = "cmd";
-    private static final String SMS_EVENT_UNRECOGNIZED_STATUS = "STATUS";
-    private static final String DEFAULT_VMG_URL_KEY = "default_vmg_url";
-
-    private static final String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
-    private static final String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s";
-    private static final String IMAP_CLOSE_NUT = "CLOSE_NUT";
-
-    private static final String ISO639_Spanish = "es";
-
-    /**
-     * For VVM3, if the STATUS SMS returns {@link StatusMessage#getProvisioningStatus()} of {@link
-     * OmtpConstants#SUBSCRIBER_UNKNOWN} and {@link StatusMessage#getReturnCode()} of this value,
-     * the user can self-provision visual voicemail service. For other response codes, the user must
-     * contact customer support to resolve the issue.
-     */
-    private static final String VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE = "2";
-
-    // Default prompt level when using the telephone user interface.
-    // Standard prompt when the user call into the voicemail, and no prompts when someone else is
-    // leaving a voicemail.
-    private static final String VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS = "5";
-    private static final String VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS = "6";
-
-    private static final int DEFAULT_PIN_LENGTH = 6;
-
-    @Override
-    public void startActivation(OmtpVvmCarrierConfigHelper config,
-            @Nullable PendingIntent sentIntent) {
-        // VVM3 does not support activation SMS.
-        // Send a status request which will start the provisioning process if the user is not
-        // provisioned.
-        VvmLog.i(TAG, "Activating");
-        config.requestStatus(sentIntent);
-    }
-
-    @Override
-    public void startDeactivation(OmtpVvmCarrierConfigHelper config) {
-        // VVM3 does not support deactivation.
-        // do nothing.
-    }
-
-    @Override
-    public boolean supportsProvisioning() {
-        return true;
-    }
-
-    @Override
-    public void startProvisioning(ActivationTask task, PhoneAccountHandle phoneAccountHandle,
-            OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, StatusMessage message,
-            Bundle data) {
-        VvmLog.i(TAG, "start vvm3 provisioning");
-        if (OmtpConstants.SUBSCRIBER_UNKNOWN.equals(message.getProvisioningStatus())) {
-            VvmLog.i(TAG, "Provisioning status: Unknown");
-            if (VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE
-                    .equals(message.getReturnCode())) {
-                VvmLog.i(TAG, "Self provisioning available, subscribing");
-                new Vvm3Subscriber(task, phoneAccountHandle, config, status, data).subscribe();
-            } else {
-                config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_UNKNOWN);
-            }
-        } else if (OmtpConstants.SUBSCRIBER_NEW.equals(message.getProvisioningStatus())) {
-            VvmLog.i(TAG, "setting up new user");
-            // Save the IMAP credentials in preferences so they are persistent and can be retrieved.
-            VisualVoicemailPreferences prefs =
-                    new VisualVoicemailPreferences(config.getContext(), phoneAccountHandle);
-            message.putStatus(prefs.edit()).apply();
-
-            startProvisionNewUser(task, phoneAccountHandle, config, status, message);
-        } else if (OmtpConstants.SUBSCRIBER_PROVISIONED.equals(message.getProvisioningStatus())) {
-            VvmLog.i(TAG, "User provisioned but not activated, disabling VVM");
-            VisualVoicemailSettingsUtil
-                    .setEnabled(config.getContext(), phoneAccountHandle, false);
-        } else if (OmtpConstants.SUBSCRIBER_BLOCKED.equals(message.getProvisioningStatus())) {
-            VvmLog.i(TAG, "User blocked");
-            config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_BLOCKED);
-        }
-    }
-
-    @Override
-    public OmtpMessageSender createMessageSender(Context context,
-            PhoneAccountHandle phoneAccountHandle, short applicationPort,
-            String destinationNumber) {
-        return new Vvm3MessageSender(context, phoneAccountHandle, applicationPort,
-                destinationNumber);
-    }
-
-    @Override
-    public void handleEvent(Context context, OmtpVvmCarrierConfigHelper config,
-            VoicemailStatus.Editor status, OmtpEvents event) {
-        Vvm3EventHandler.handleEvent(context, config, status, event);
-    }
-
-    @Override
-    public String getCommand(String command) {
-        if (command == OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT) {
-            return IMAP_CHANGE_TUI_PWD_FORMAT;
-        }
-        if (command == OmtpConstants.IMAP_CLOSE_NUT) {
-            return IMAP_CLOSE_NUT;
-        }
-        if (command == OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT) {
-            return IMAP_CHANGE_VM_LANG_FORMAT;
-        }
-        return super.getCommand(command);
-    }
-
-    @Override
-    public Bundle translateStatusSmsBundle(OmtpVvmCarrierConfigHelper config, String event,
-            Bundle data) {
-        // UNRECOGNIZED?cmd=STATUS is the response of a STATUS request when the user is provisioned
-        // with iPhone visual voicemail without VoLTE. Translate it into an unprovisioned status
-        // so provisioning can be done.
-        if (!SMS_EVENT_UNRECOGNIZED.equals(event)) {
-            return null;
-        }
-        if (!SMS_EVENT_UNRECOGNIZED_STATUS.equals(data.getString(SMS_EVENT_UNRECOGNIZED_CMD))) {
-            return null;
-        }
-        Bundle bundle = new Bundle();
-        bundle.putString(OmtpConstants.PROVISIONING_STATUS, OmtpConstants.SUBSCRIBER_UNKNOWN);
-        bundle.putString(OmtpConstants.RETURN_CODE,
-                VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE);
-        String vmgUrl = config.getString(DEFAULT_VMG_URL_KEY);
-        if (TextUtils.isEmpty(vmgUrl)) {
-            VvmLog.e(TAG, "Unable to translate STATUS SMS: VMG URL is not set in config");
-            return null;
-        }
-        bundle.putString(Vvm3Subscriber.VMG_URL_KEY, vmgUrl);
-        VvmLog.i(TAG, "UNRECOGNIZED?cmd=STATUS translated into unprovisioned STATUS SMS");
-        return bundle;
-    }
-
-    private void startProvisionNewUser(ActivationTask task, PhoneAccountHandle phoneAccountHandle,
-            OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status,
-            StatusMessage message) {
-        try (NetworkWrapper wrapper = VvmNetworkRequest
-                .getNetwork(config, phoneAccountHandle, status)) {
-            Network network = wrapper.get();
-
-            VvmLog.i(TAG, "new user: network available");
-            try (ImapHelper helper = new ImapHelper(config.getContext(), phoneAccountHandle,
-                    network, status)) {
-                // VVM3 has inconsistent error language code to OMTP. Just issue a raw command
-                // here.
-                // TODO(b/29082671): use LocaleList
-                if (Locale.getDefault().getLanguage()
-                        .equals(new Locale(ISO639_Spanish).getLanguage())) {
-                    // Spanish
-                    helper.changeVoicemailTuiLanguage(
-                            VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS);
-                } else {
-                    // English
-                    helper.changeVoicemailTuiLanguage(
-                            VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS);
-                }
-                VvmLog.i(TAG, "new user: language set");
-
-                if (setPin(config.getContext(), phoneAccountHandle, helper, message)) {
-                    // Only close new user tutorial if the PIN has been changed.
-                    helper.closeNewUserTutorial();
-                    VvmLog.i(TAG, "new user: NUT closed");
-
-                    config.requestStatus(null);
-                }
-            } catch (InitializingException | MessagingException | IOException e) {
-                config.handleEvent(status, OmtpEvents.VVM3_NEW_USER_SETUP_FAILED);
-                task.fail();
-                VvmLog.e(TAG, e.toString());
-            }
-        } catch (RequestFailedException e) {
-            config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
-            task.fail();
-        }
-
-    }
-
-
-    private static boolean setPin(Context context, PhoneAccountHandle phoneAccountHandle,
-            ImapHelper helper, StatusMessage message)
-            throws IOException, MessagingException {
-        String defaultPin = getDefaultPin(message);
-        if (defaultPin == null) {
-            VvmLog.i(TAG, "cannot generate default PIN");
-            return false;
-        }
-
-        if (VoicemailChangePinActivity.isDefaultOldPinSet(context, phoneAccountHandle)) {
-            // The pin was already set
-            VvmLog.i(TAG, "PIN already set");
-            return true;
-        }
-        String newPin = generatePin(getMinimumPinLength(context, phoneAccountHandle));
-        if (helper.changePin(defaultPin, newPin) == OmtpConstants.CHANGE_PIN_SUCCESS) {
-            VoicemailChangePinActivity.setDefaultOldPIN(context, phoneAccountHandle, newPin);
-            helper.handleEvent(OmtpEvents.CONFIG_DEFAULT_PIN_REPLACED);
-        }
-        VvmLog.i(TAG, "new user: PIN set");
-        return true;
-    }
-
-    @Nullable
-    private static String getDefaultPin(StatusMessage message) {
-        // The IMAP username is [phone number]@example.com
-        String username = message.getImapUserName();
-        try {
-            String number = username.substring(0, username.indexOf('@'));
-            if (number.length() < 4) {
-                VvmLog.e(TAG, "unable to extract number from IMAP username");
-                return null;
-            }
-            return "1" + number.substring(number.length() - 4);
-        } catch (StringIndexOutOfBoundsException e) {
-            VvmLog.e(TAG, "unable to extract number from IMAP username");
-            return null;
-        }
-
-    }
-
-    private static int getMinimumPinLength(Context context, PhoneAccountHandle phoneAccountHandle) {
-        VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(context,
-                phoneAccountHandle);
-        // The OMTP pin length format is {min}-{max}
-        String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-");
-        if (lengths.length == 2) {
-            try {
-                return Integer.parseInt(lengths[0]);
-            } catch (NumberFormatException e) {
-                return DEFAULT_PIN_LENGTH;
-            }
-        }
-        return DEFAULT_PIN_LENGTH;
-    }
-
-    private static String generatePin(int length) {
-        SecureRandom random = new SecureRandom();
-        return String.format(Locale.US, "%010d", Math.abs(random.nextLong()))
-                .substring(0, length);
-
-    }
-}
diff --git a/java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java b/java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java
deleted file mode 100644
index 0a4d792..0000000
--- a/java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java
+++ /dev/null
@@ -1,326 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.protocol;
-
-import android.annotation.TargetApi;
-import android.net.Network;
-import android.os.Build;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.support.annotation.WorkerThread;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.TelephonyManager;
-import android.text.Html;
-import android.text.Spanned;
-import android.text.style.URLSpan;
-import android.util.ArrayMap;
-import com.android.voicemailomtp.ActivationTask;
-import com.android.voicemailomtp.Assert;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.sync.VvmNetworkRequest;
-import com.android.voicemailomtp.sync.VvmNetworkRequest.NetworkWrapper;
-import com.android.voicemailomtp.sync.VvmNetworkRequest.RequestFailedException;
-import com.android.volley.AuthFailureError;
-import com.android.volley.Request;
-import com.android.volley.RequestQueue;
-import com.android.volley.toolbox.HurlStack;
-import com.android.volley.toolbox.RequestFuture;
-import com.android.volley.toolbox.StringRequest;
-import com.android.volley.toolbox.Volley;
-import java.io.IOException;
-import java.net.CookieHandler;
-import java.net.CookieManager;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Random;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Class to subscribe to basic VVM3 visual voicemail, for example, Verizon. Subscription is required
- * when the user is unprovisioned. This could happen when the user is on a legacy service, or
- * switched over from devices that used other type of visual voicemail.
- *
- * <p>The STATUS SMS will come with a URL to the voicemail management gateway. From it we can find
- * the self provisioning gateway URL that we can modify voicemail services.
- *
- * <p>A request to the self provisioning gateway to activate basic visual voicemail will return us
- * with a web page. If the user hasn't subscribe to it yet it will contain a link to confirm the
- * subscription. This link should be clicked through cellular network, and have cookies enabled.
- *
- * <p>After the process is completed, the carrier should send us another STATUS SMS with a new or
- * ready user.
- */
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class Vvm3Subscriber {
-
-    private static final String TAG = "Vvm3Subscriber";
-
-    private static final String OPERATION_GET_SPG_URL = "retrieveSPGURL";
-    private static final String SPG_URL_TAG = "spgurl";
-    private static final String TRANSACTION_ID_TAG = "transactionid";
-    //language=XML
-    private static final String VMG_XML_REQUEST_FORMAT = ""
-            + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
-            + "<VMGVVMRequest>"
-            + "  <MessageHeader>"
-            + "    <transactionid>%1$s</transactionid>"
-            + "  </MessageHeader>"
-            + "  <MessageBody>"
-            + "    <mdn>%2$s</mdn>"
-            + "    <operation>%3$s</operation>"
-            + "    <source>Device</source>"
-            + "    <devicemodel>%4$s</devicemodel>"
-            + "  </MessageBody>"
-            + "</VMGVVMRequest>";
-
-    static final String VMG_URL_KEY = "vmg_url";
-
-    // Self provisioning POST key/values. VVM3 API 2.1.0 12.3
-    private static final String SPG_VZW_MDN_PARAM = "VZW_MDN";
-    private static final String SPG_VZW_SERVICE_PARAM = "VZW_SERVICE";
-    private static final String SPG_VZW_SERVICE_BASIC = "BVVM";
-    private static final String SPG_DEVICE_MODEL_PARAM = "DEVICE_MODEL";
-    // Value for all android device
-    private static final String SPG_DEVICE_MODEL_ANDROID = "DROID_4G";
-    private static final String SPG_APP_TOKEN_PARAM = "APP_TOKEN";
-    private static final String SPG_APP_TOKEN = "q8e3t5u2o1";
-    private static final String SPG_LANGUAGE_PARAM = "SPG_LANGUAGE_PARAM";
-    private static final String SPG_LANGUAGE_EN = "ENGLISH";
-
-    private static final String BASIC_SUBSCRIBE_LINK_TEXT = "Subscribe to Basic Visual Voice Mail";
-
-    private static final int REQUEST_TIMEOUT_SECONDS = 30;
-
-    private final ActivationTask mTask;
-    private final PhoneAccountHandle mHandle;
-    private final OmtpVvmCarrierConfigHelper mHelper;
-    private final VoicemailStatus.Editor mStatus;
-    private final Bundle mData;
-
-    private final String mNumber;
-
-    private RequestQueue mRequestQueue;
-
-    private static class ProvisioningException extends Exception {
-
-        public ProvisioningException(String message) {
-            super(message);
-        }
-    }
-
-    static {
-        // Set the default cookie handler to retain session data for the self provisioning gateway.
-        // Note; this is not ideal as it is application-wide, and can easily get clobbered.
-        // But it seems to be the preferred way to manage cookie for HttpURLConnection, and manually
-        // managing cookies will greatly increase complexity.
-        CookieManager cookieManager = new CookieManager();
-        CookieHandler.setDefault(cookieManager);
-    }
-
-    @WorkerThread
-    public Vvm3Subscriber(ActivationTask task, PhoneAccountHandle handle,
-            OmtpVvmCarrierConfigHelper helper, VoicemailStatus.Editor status, Bundle data) {
-        Assert.isNotMainThread();
-        mTask = task;
-        mHandle = handle;
-        mHelper = helper;
-        mStatus = status;
-        mData = data;
-
-        // Assuming getLine1Number() will work with VVM3. For unprovisioned users the IMAP username
-        // is not included in the status SMS, thus no other way to get the current phone number.
-        mNumber = mHelper.getContext().getSystemService(TelephonyManager.class)
-                .createForPhoneAccountHandle(mHandle).getLine1Number();
-    }
-
-    @WorkerThread
-    public void subscribe() {
-        Assert.isNotMainThread();
-        // Cellular data is required to subscribe.
-        // processSubscription() is called after network is available.
-        VvmLog.i(TAG, "Subscribing");
-
-        try (NetworkWrapper wrapper = VvmNetworkRequest.getNetwork(mHelper, mHandle, mStatus)) {
-            Network network = wrapper.get();
-            VvmLog.d(TAG, "provisioning: network available");
-            mRequestQueue = Volley
-                    .newRequestQueue(mHelper.getContext(), new NetworkSpecifiedHurlStack(network));
-            processSubscription();
-        } catch (RequestFailedException e) {
-            mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
-            mTask.fail();
-        }
-    }
-
-    private void processSubscription() {
-        try {
-            String gatewayUrl = getSelfProvisioningGateway();
-            String selfProvisionResponse = getSelfProvisionResponse(gatewayUrl);
-            String subscribeLink = findSubscribeLink(selfProvisionResponse);
-            clickSubscribeLink(subscribeLink);
-        } catch (ProvisioningException e) {
-            VvmLog.e(TAG, e.toString());
-            mTask.fail();
-        }
-    }
-
-    /**
-     * Get the URL to perform self-provisioning from the voicemail management gateway.
-     */
-    private String getSelfProvisioningGateway() throws ProvisioningException {
-        VvmLog.i(TAG, "retrieving SPG URL");
-        String response = vvm3XmlRequest(OPERATION_GET_SPG_URL);
-        return extractText(response, SPG_URL_TAG);
-    }
-
-    /**
-     * Sent a request to the self-provisioning gateway, which will return us with a webpage. The
-     * page might contain a "Subscribe to Basic Visual Voice Mail" link to complete the
-     * subscription. The cookie from this response and cellular data is required to click the link.
-     */
-    private String getSelfProvisionResponse(String url) throws ProvisioningException {
-        VvmLog.i(TAG, "Retrieving self provisioning response");
-
-        RequestFuture<String> future = RequestFuture.newFuture();
-
-        StringRequest stringRequest = new StringRequest(Request.Method.POST, url, future, future) {
-            @Override
-            protected Map<String, String> getParams() {
-                Map<String, String> params = new ArrayMap<>();
-                params.put(SPG_VZW_MDN_PARAM, mNumber);
-                params.put(SPG_VZW_SERVICE_PARAM, SPG_VZW_SERVICE_BASIC);
-                params.put(SPG_DEVICE_MODEL_PARAM, SPG_DEVICE_MODEL_ANDROID);
-                params.put(SPG_APP_TOKEN_PARAM, SPG_APP_TOKEN);
-                // Language to display the subscription page. The page is never shown to the user
-                // so just use English.
-                params.put(SPG_LANGUAGE_PARAM, SPG_LANGUAGE_EN);
-                return params;
-            }
-        };
-
-        mRequestQueue.add(stringRequest);
-        try {
-            return future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
-        } catch (InterruptedException | ExecutionException | TimeoutException e) {
-            mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
-            throw new ProvisioningException(e.toString());
-        }
-    }
-
-    private void clickSubscribeLink(String subscribeLink) throws ProvisioningException {
-        VvmLog.i(TAG, "Clicking subscribe link");
-        RequestFuture<String> future = RequestFuture.newFuture();
-
-        StringRequest stringRequest = new StringRequest(Request.Method.POST,
-                subscribeLink, future, future);
-        mRequestQueue.add(stringRequest);
-        try {
-            // A new STATUS SMS will be sent after this request.
-            future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
-        } catch (TimeoutException | ExecutionException | InterruptedException e) {
-            mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
-            throw new ProvisioningException(e.toString());
-        }
-        // It could take very long for the STATUS SMS to return. Waiting for it is unreliable.
-        // Just leave the CONFIG STATUS as CONFIGURING and end the task. The user can always
-        // manually retry if it took too long.
-    }
-
-    private String vvm3XmlRequest(String operation) throws ProvisioningException {
-        VvmLog.d(TAG, "Sending vvm3XmlRequest for " + operation);
-        String voicemailManagementGateway = mData.getString(VMG_URL_KEY);
-        if (voicemailManagementGateway == null) {
-            VvmLog.e(TAG, "voicemailManagementGateway url unknown");
-            return null;
-        }
-        String transactionId = createTransactionId();
-        String body = String.format(Locale.US, VMG_XML_REQUEST_FORMAT,
-                transactionId, mNumber, operation, Build.MODEL);
-
-        RequestFuture<String> future = RequestFuture.newFuture();
-        StringRequest stringRequest = new StringRequest(Request.Method.POST,
-                voicemailManagementGateway, future, future) {
-            @Override
-            public byte[] getBody() throws AuthFailureError {
-                return body.getBytes();
-            }
-        };
-        mRequestQueue.add(stringRequest);
-
-        try {
-            String response = future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
-            if (!transactionId.equals(extractText(response, TRANSACTION_ID_TAG))) {
-                throw new ProvisioningException("transactionId mismatch");
-            }
-            return response;
-        } catch (InterruptedException | ExecutionException | TimeoutException e) {
-            mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
-            throw new ProvisioningException(e.toString());
-        }
-    }
-
-    private String findSubscribeLink(String response) throws ProvisioningException {
-        Spanned doc = Html.fromHtml(response, Html.FROM_HTML_MODE_LEGACY);
-        URLSpan[] spans = doc.getSpans(0, doc.length(), URLSpan.class);
-        StringBuilder fulltext = new StringBuilder();
-        for (URLSpan span : spans) {
-            String text = doc.subSequence(doc.getSpanStart(span), doc.getSpanEnd(span)).toString();
-            if (BASIC_SUBSCRIBE_LINK_TEXT.equals(text)) {
-                return span.getURL();
-            }
-            fulltext.append(text);
-        }
-        throw new ProvisioningException("Subscribe link not found: " + fulltext);
-    }
-
-    private String createTransactionId() {
-        return String.valueOf(Math.abs(new Random().nextLong()));
-    }
-
-    private String extractText(String xml, String tag) throws ProvisioningException {
-        Pattern pattern = Pattern.compile("<" + tag + ">(.*)<\\/" + tag + ">");
-        Matcher matcher = pattern.matcher(xml);
-        if (matcher.find()) {
-            return matcher.group(1);
-        }
-        throw new ProvisioningException("Tag " + tag + " not found in xml response");
-    }
-
-    private static class NetworkSpecifiedHurlStack extends HurlStack {
-
-        private final Network mNetwork;
-
-        public NetworkSpecifiedHurlStack(Network network) {
-            mNetwork = network;
-        }
-
-        @Override
-        protected HttpURLConnection createConnection(URL url) throws IOException {
-            return (HttpURLConnection) mNetwork.openConnection(url);
-        }
-
-    }
-}
diff --git a/java/com/android/voicemailomtp/res/values/strings.xml b/java/com/android/voicemailomtp/res/values/strings.xml
deleted file mode 100644
index 7a14073..0000000
--- a/java/com/android/voicemailomtp/res/values/strings.xml
+++ /dev/null
@@ -1,86 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2007 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-
-    <!-- Title of the "Voicemail" settings screen, with a text label identifying which SIM the settings are for. -->
-    <string translatable="false" name="voicemail_settings_with_label">Voicemail (<xliff:g id="subscriptionlabel" example="Mock Carrier">%s</xliff:g>)</string>
-
-    <!-- Call settings screen, setting option name -->
-    <string translatable="false" name="voicemail_settings_label">Voicemail</string>
-
-    <!-- DO NOT TRANSLATE. Internal key for a visual voicemail preference. -->
-    <string translatable="false" name="voicemail_visual_voicemail_key">
-        voicemail_visual_voicemail_key
-    </string>
-    <!-- DO NOT TRANSLATE. Internal key for a voicemail change pin preference. -->
-    <string translatable="false" name="voicemail_change_pin_key">voicemail_change_pin_key</string>
-
-    <!-- Visual voicemail on/off title [CHAR LIMIT=40] -->
-    <string translatable="false" name="voicemail_visual_voicemail_switch_title">Visual Voicemail</string>
-
-    <!-- Voicemail change PIN dialog title [CHAR LIMIT=40] -->
-    <string translatable="false" name="voicemail_set_pin_dialog_title">Set PIN</string>
-    <!-- Voicemail change PIN dialog title [CHAR LIMIT=40] -->
-    <string translatable="false" name="voicemail_change_pin_dialog_title">Change PIN</string>
-
-    <!-- Hint for the old PIN field in the change vociemail PIN dialog -->
-    <string translatable="false" name="vm_change_pin_old_pin">Old PIN</string>
-    <!-- Hint for the new PIN field in the change vociemail PIN dialog -->
-    <string translatable="false" name="vm_change_pin_new_pin">New PIN</string>
-
-    <!-- Message on the dialog when PIN changing is in progress -->
-    <string translatable="false" name="vm_change_pin_progress_message">Please wait.</string>
-    <!-- Error message for the voicemail PIN change if the PIN is too short -->
-    <string translatable="false" name="vm_change_pin_error_too_short">The new PIN is too short.</string>
-    <!-- Error message for the voicemail PIN change if the PIN is too long -->
-    <string translatable="false" name="vm_change_pin_error_too_long">The new PIN is too long.</string>
-    <!-- Error message for the voicemail PIN change if the PIN is too weak -->
-    <string translatable="false" name="vm_change_pin_error_too_weak">The new PIN is too weak. A strong password should not have continuous sequence or repeated digits.</string>
-    <!-- Error message for the voicemail PIN change if the old PIN entered doesn't match  -->
-    <string translatable="false" name="vm_change_pin_error_mismatch">The old PIN does not match.</string>
-    <!-- Error message for the voicemail PIN change if the new PIN contains invalid character -->
-    <string translatable="false" name="vm_change_pin_error_invalid">The new PIN contains invalid characters.</string>
-    <!-- Error message for the voicemail PIN change if operation has failed -->
-    <string translatable="false" name="vm_change_pin_error_system_error">Unable to change PIN</string>
-    <!-- Message to replace the transcription if a visual voicemail message is not supported-->
-    <string translatable="false" name="vvm_unsupported_message_format">Unsupported message type, call <xliff:g id="number" example="*86">%s</xliff:g> to listen.</string>
-
-    <!-- The title for the change voicemail PIN activity -->
-    <string translatable="false" name="change_pin_title">Change Voicemail PIN</string>
-    <!-- The label for the continue button in change voicemail PIN activity -->
-    <string translatable="false" name="change_pin_continue_label">Continue</string>
-    <!-- The label for the cancel button in change voicemail PIN activity -->
-    <string translatable="false" name="change_pin_cancel_label">Cancel</string>
-    <!-- The label for the ok button in change voicemail PIN activity -->
-    <string translatable="false" name="change_pin_ok_label">Ok</string>
-    <!-- The title for the enter old pin step in change voicemail PIN activity -->
-    <string translatable="false" name="change_pin_enter_old_pin_header">Confirm your old PIN</string>
-    <!-- The hint for the enter old pin step in change voicemail PIN activity -->
-    <string translatable="false" name="change_pin_enter_old_pin_hint">Enter your voicemail PIN to continue.</string>
-    <!-- The title for the enter new pin step in change voicemail PIN activity -->
-    <string translatable="false" name="change_pin_enter_new_pin_header">Set a new PIN</string>
-    <!-- The hint for the enter new pin step in change voicemail PIN activity -->
-    <string translatable="false" name="change_pin_enter_new_pin_hint">PIN must be <xliff:g id="min" example="4">%1$d</xliff:g>-<xliff:g id="max" example="7">%2$d</xliff:g> digits.</string>
-    <!-- The title for the confirm new pin step in change voicemail PIN activity -->
-    <string translatable="false" name="change_pin_confirm_pin_header">Confirm your PIN</string>
-    <!-- The error message for th confirm new pin step in change voicemail PIN activity, if the pin doen't match the one previously entered -->
-    <string translatable="false" name="change_pin_confirm_pins_dont_match">PINs don\'t match</string>
-    <!-- The toast to show after the voicemail PIN has been successfully changed -->
-    <string translatable="false" name="change_pin_succeeded">Voicemail PIN updated</string>
-    <!-- The error message to show if the server reported an error while attempting to change the voicemail PIN -->
-    <string translatable="false" name="change_pin_system_error">Unable to set PIN</string>
-</resources>
diff --git a/java/com/android/voicemailomtp/res/xml/voicemail_settings.xml b/java/com/android/voicemailomtp/res/xml/voicemail_settings.xml
deleted file mode 100644
index 03bc34e..0000000
--- a/java/com/android/voicemailomtp/res/xml/voicemail_settings.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2014 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
-                  android:title="@string/voicemail_settings_label">
-
-    <SwitchPreference
-            android:key="@string/voicemail_visual_voicemail_key"
-            android:title="@string/voicemail_visual_voicemail_switch_title"/>"
-
-    <Preference
-            android:key="@string/voicemail_change_pin_key"
-            android:title="@string/voicemail_change_pin_dialog_title"/>
-</PreferenceScreen>
diff --git a/java/com/android/voicemailomtp/scheduling/BaseTask.java b/java/com/android/voicemailomtp/scheduling/BaseTask.java
deleted file mode 100644
index 8097bb4..0000000
--- a/java/com/android/voicemailomtp/scheduling/BaseTask.java
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.scheduling;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.SystemClock;
-import android.support.annotation.CallSuper;
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.WorkerThread;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SubscriptionManager;
-import com.android.voicemailomtp.Assert;
-import com.android.voicemailomtp.NeededForTesting;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Provides common utilities for task implementations, such as execution time and managing {@link
- * Policy}
- */
-public abstract class BaseTask implements Task {
-
-    private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
-
-    private Context mContext;
-
-    private int mId;
-    private PhoneAccountHandle mPhoneAccountHandle;
-
-    private boolean mHasStarted;
-    private volatile boolean mHasFailed;
-
-    @NonNull
-    private final List<Policy> mPolicies = new ArrayList<>();
-
-    private long mExecutionTime;
-
-    private static Clock sClock = new Clock();
-
-    protected BaseTask(int id) {
-        mId = id;
-        mExecutionTime = getTimeMillis();
-    }
-
-    /**
-     * Modify the task ID to prevent arbitrary task from executing. Can only be called before {@link
-     * #onCreate(Context, Intent, int, int)} returns.
-     */
-    @MainThread
-    public void setId(int id) {
-        Assert.isMainThread();
-        mId = id;
-    }
-
-    @MainThread
-    public boolean hasStarted() {
-        Assert.isMainThread();
-        return mHasStarted;
-    }
-
-    @MainThread
-    public boolean hasFailed() {
-        Assert.isMainThread();
-        return mHasFailed;
-    }
-
-    public Context getContext() {
-        return mContext;
-    }
-
-    public PhoneAccountHandle getPhoneAccountHandle() {
-        return mPhoneAccountHandle;
-    }
-    /**
-     * Should be call in the constructor or {@link Policy#onCreate(BaseTask, Intent, int, int)} will
-     * be missed.
-     */
-    @MainThread
-    public BaseTask addPolicy(Policy policy) {
-        Assert.isMainThread();
-        mPolicies.add(policy);
-        return this;
-    }
-
-    /**
-     * Indicate the task has failed. {@link Policy#onFail()} will be triggered once the execution
-     * ends. This mechanism is used by policies for actions such as determining whether to schedule
-     * a retry. Must be call inside {@link #onExecuteInBackgroundThread()}
-     */
-    @WorkerThread
-    public void fail() {
-        Assert.isNotMainThread();
-        mHasFailed = true;
-    }
-
-    @MainThread
-    public void setExecutionTime(long timeMillis) {
-        Assert.isMainThread();
-        mExecutionTime = timeMillis;
-    }
-
-    public long getTimeMillis() {
-        return sClock.getTimeMillis();
-    }
-
-    /**
-     * Creates an intent that can be used to restart the current task. Derived class should build
-     * their intent upon this.
-     */
-    public Intent createRestartIntent() {
-        return createIntent(getContext(), this.getClass(), mPhoneAccountHandle);
-    }
-
-    /**
-     * Creates an intent that can be used to start the {@link TaskSchedulerService}. Derived class
-     * should build their intent upon this.
-     */
-    public static Intent createIntent(Context context, Class<? extends BaseTask> task,
-            PhoneAccountHandle phoneAccountHandle) {
-        Intent intent = TaskSchedulerService.createIntent(context, task);
-        intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
-        return intent;
-    }
-
-    @Override
-    public TaskId getId() {
-        return new TaskId(mId, mPhoneAccountHandle);
-    }
-
-    @Override
-    @CallSuper
-    public void onCreate(Context context, Intent intent, int flags, int startId) {
-        mContext = context;
-        mPhoneAccountHandle = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
-        for (Policy policy : mPolicies) {
-            policy.onCreate(this, intent, flags, startId);
-        }
-    }
-
-    @Override
-    public long getReadyInMilliSeconds() {
-        return mExecutionTime - getTimeMillis();
-    }
-
-    @Override
-    @CallSuper
-    public void onBeforeExecute() {
-        for (Policy policy : mPolicies) {
-            policy.onBeforeExecute();
-        }
-        mHasStarted = true;
-    }
-
-    @Override
-    @CallSuper
-    public void onCompleted() {
-        if (mHasFailed) {
-            for (Policy policy : mPolicies) {
-                policy.onFail();
-            }
-        }
-
-        for (Policy policy : mPolicies) {
-            policy.onCompleted();
-        }
-    }
-
-    @Override
-    public void onDuplicatedTaskAdded(Task task) {
-        for (Policy policy : mPolicies) {
-            policy.onDuplicatedTaskAdded();
-        }
-    }
-
-    @NeededForTesting
-    static class Clock {
-
-        public long getTimeMillis() {
-            return SystemClock.elapsedRealtime();
-        }
-    }
-
-    /**
-     * Used to replace the clock with an deterministic clock
-     */
-    @NeededForTesting
-    static void setClockForTesting(Clock clock) {
-        sClock = clock;
-    }
-}
diff --git a/java/com/android/voicemailomtp/scheduling/BlockerTask.java b/java/com/android/voicemailomtp/scheduling/BlockerTask.java
deleted file mode 100644
index 55ad9a7..0000000
--- a/java/com/android/voicemailomtp/scheduling/BlockerTask.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.scheduling;
-
-import android.content.Context;
-import android.content.Intent;
-
-import com.android.voicemailomtp.VvmLog;
-
-/**
- * Task to block another task of the same ID from being queued for a certain amount of time.
- */
-public class BlockerTask extends BaseTask {
-
-    private static final String TAG = "BlockerTask";
-
-    public static final String EXTRA_TASK_ID = "extra_task_id";
-    public static final String EXTRA_BLOCK_FOR_MILLIS = "extra_block_for_millis";
-
-    public BlockerTask() {
-        super(TASK_INVALID);
-    }
-
-    @Override
-    public void onCreate(Context context, Intent intent, int flags, int startId) {
-        super.onCreate(context, intent, flags, startId);
-        setId(intent.getIntExtra(EXTRA_TASK_ID, TASK_INVALID));
-        setExecutionTime(getTimeMillis() + intent.getIntExtra(EXTRA_BLOCK_FOR_MILLIS, 0));
-    }
-
-    @Override
-    public void onExecuteInBackgroundThread() {
-        // Do nothing.
-    }
-
-    @Override
-    public void onDuplicatedTaskAdded(Task task) {
-        VvmLog
-            .v(TAG, task.toString() + "blocked, " + getReadyInMilliSeconds() + "millis remaining");
-    }
-}
diff --git a/java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java b/java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java
deleted file mode 100644
index bef449b..0000000
--- a/java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.scheduling;
-
-import android.content.Intent;
-
-import com.android.voicemailomtp.scheduling.Task.TaskId;
-
-/**
- * If a task with this policy succeeds, a {@link BlockerTask} with the same {@link TaskId} of the
- * task will be queued immediately, preventing the same task from running for a certain amount of
- * time.
- */
-public class MinimalIntervalPolicy implements Policy {
-
-    BaseTask mTask;
-    TaskId mId;
-    int mBlockForMillis;
-
-    public MinimalIntervalPolicy(int blockForMillis) {
-        mBlockForMillis = blockForMillis;
-    }
-
-    @Override
-    public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
-        mTask = task;
-        mId = mTask.getId();
-    }
-
-    @Override
-    public void onBeforeExecute() {
-
-    }
-
-    @Override
-    public void onCompleted() {
-        if (!mTask.hasFailed()) {
-            Intent intent = mTask
-                    .createIntent(mTask.getContext(), BlockerTask.class, mId.phoneAccountHandle);
-            intent.putExtra(BlockerTask.EXTRA_TASK_ID, mId.id);
-            intent.putExtra(BlockerTask.EXTRA_BLOCK_FOR_MILLIS, mBlockForMillis);
-            mTask.getContext().startService(intent);
-        }
-    }
-
-    @Override
-    public void onFail() {
-
-    }
-
-    @Override
-    public void onDuplicatedTaskAdded() {
-
-    }
-}
diff --git a/java/com/android/voicemailomtp/scheduling/PostponePolicy.java b/java/com/android/voicemailomtp/scheduling/PostponePolicy.java
deleted file mode 100644
index 27a82f0..0000000
--- a/java/com/android/voicemailomtp/scheduling/PostponePolicy.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.scheduling;
-
-import android.content.Intent;
-
-import com.android.voicemailomtp.VvmLog;
-
-/**
- * A task with Postpone policy will not be executed immediately. It will wait for a while and if a
- * duplicated task is queued during the duration, the task will be postponed further. The task will
- * only be executed if no new task was added in postponeMillis. Useful to batch small tasks in quick
- * succession together.
- */
-public class PostponePolicy implements Policy {
-
-    private static final String TAG = "PostponePolicy";
-
-    private final int mPostponeMillis;
-    private BaseTask mTask;
-
-    public PostponePolicy(int postponeMillis) {
-        mPostponeMillis = postponeMillis;
-    }
-
-    @Override
-    public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
-        mTask = task;
-        mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis);
-    }
-
-    @Override
-    public void onBeforeExecute() {
-        // Do nothing
-    }
-
-    @Override
-    public void onCompleted() {
-        // Do nothing
-    }
-
-    @Override
-    public void onFail() {
-        // Do nothing
-    }
-
-    @Override
-    public void onDuplicatedTaskAdded() {
-        if (mTask.hasStarted()) {
-            return;
-        }
-        VvmLog.d(TAG, "postponing " + mTask);
-        mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis);
-    }
-}
diff --git a/java/com/android/voicemailomtp/scheduling/RetryPolicy.java b/java/com/android/voicemailomtp/scheduling/RetryPolicy.java
deleted file mode 100644
index 4636574..0000000
--- a/java/com/android/voicemailomtp/scheduling/RetryPolicy.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.scheduling;
-
-import android.content.Intent;
-import android.telecom.PhoneAccountHandle;
-
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-
-/**
- * A task with this policy will automatically re-queue itself if {@link BaseTask#fail()} has been
- * called during {@link BaseTask#onExecuteInBackgroundThread()}. A task will be retried at most
- * <code>retryLimit</code> times and with a <code>retryDelayMillis</code> interval in between.
- */
-public class RetryPolicy implements Policy {
-
-    private static final String TAG = "RetryPolicy";
-    private static final String EXTRA_RETRY_COUNT = "extra_retry_count";
-
-    private final int mRetryLimit;
-    private final int mRetryDelayMillis;
-
-    private BaseTask mTask;
-
-    private int mRetryCount;
-    private boolean mFailed;
-
-    private VoicemailStatus.DeferredEditor mVoicemailStatusEditor;
-
-    public RetryPolicy(int retryLimit, int retryDelayMillis) {
-        mRetryLimit = retryLimit;
-        mRetryDelayMillis = retryDelayMillis;
-    }
-
-    private boolean hasMoreRetries() {
-        return mRetryCount < mRetryLimit;
-    }
-
-    /**
-     * Error status should only be set if retries has exhausted or the task is successful. Status
-     * writes to this editor will be deferred until the task has ended, and will only be committed
-     * if the task is successful or there are no retries left.
-     */
-    public VoicemailStatus.Editor getVoicemailStatusEditor() {
-        return mVoicemailStatusEditor;
-    }
-
-    @Override
-    public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
-        mTask = task;
-        mRetryCount = intent.getIntExtra(EXTRA_RETRY_COUNT, 0);
-        if (mRetryCount > 0) {
-            VvmLog.d(TAG, "retry #" + mRetryCount + " for " + mTask + " queued, executing in "
-                    + mRetryDelayMillis);
-            mTask.setExecutionTime(mTask.getTimeMillis() + mRetryDelayMillis);
-        }
-        PhoneAccountHandle phoneAccountHandle = task.getPhoneAccountHandle();
-        if (phoneAccountHandle == null) {
-            VvmLog.e(TAG,
-                    "null phone account for phoneAccountHandle " + task.getPhoneAccountHandle());
-            // This should never happen, but continue on if it does. The status write will be
-            // discarded.
-        }
-        mVoicemailStatusEditor = VoicemailStatus
-                .deferredEdit(task.getContext(), phoneAccountHandle);
-    }
-
-    @Override
-    public void onBeforeExecute() {
-
-    }
-
-    @Override
-    public void onCompleted() {
-        if (!mFailed || !hasMoreRetries()) {
-            if (!mFailed) {
-                VvmLog.d(TAG, mTask.toString() + " completed successfully");
-            }
-            if (!hasMoreRetries()) {
-                VvmLog.d(TAG, "Retry limit for " + mTask + " reached");
-            }
-            VvmLog.i(TAG, "committing deferred status: " + mVoicemailStatusEditor.getValues());
-            mVoicemailStatusEditor.deferredApply();
-            return;
-        }
-        VvmLog.i(TAG, "discarding deferred status: " + mVoicemailStatusEditor.getValues());
-        Intent intent = mTask.createRestartIntent();
-        intent.putExtra(EXTRA_RETRY_COUNT, mRetryCount + 1);
-
-        mTask.getContext().startService(intent);
-    }
-
-    @Override
-    public void onFail() {
-        mFailed = true;
-    }
-
-    @Override
-    public void onDuplicatedTaskAdded() {
-
-    }
-}
diff --git a/java/com/android/voicemailomtp/scheduling/Task.java b/java/com/android/voicemailomtp/scheduling/Task.java
deleted file mode 100644
index 61c3539..0000000
--- a/java/com/android/voicemailomtp/scheduling/Task.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.scheduling;
-
-import android.content.Context;
-import android.content.Intent;
-import android.support.annotation.MainThread;
-import android.support.annotation.WorkerThread;
-import android.telecom.PhoneAccountHandle;
-
-import java.util.Objects;
-
-/**
- * A task for {@link TaskSchedulerService} to execute. Since the task is sent through a intent to
- * the scheduler, The task must be constructable with the intent. Specifically, It must have a
- * constructor with zero arguments, and have all relevant data packed inside the intent. Use {@link
- * TaskSchedulerService#createIntent(Context, Class)} to create a intent that will construct the
- * Task.
- *
- * <p>Only {@link #onExecuteInBackgroundThread()} is run on the worker thread.
- */
-public interface Task {
-
-    /**
-     * TaskId to indicate it has not be set. If a task does not provide a default TaskId it should
-     * be set before {@link Task#onCreate(Context, Intent, int, int) returns}
-     */
-    int TASK_INVALID = -1;
-
-    /**
-     * TaskId to indicate it should always be queued regardless of duplicates. {@link
-     * Task#onDuplicatedTaskAdded(Task)} will never be called on tasks with this TaskId.
-     */
-    int TASK_ALLOW_DUPLICATES = -2;
-
-    int TASK_UPLOAD = 1;
-    int TASK_SYNC = 2;
-    int TASK_ACTIVATION = 3;
-
-    /**
-     * Used to differentiate between types of tasks. If a task with the same TaskId is already in
-     * the queue the new task will be rejected.
-     */
-    class TaskId {
-
-        /**
-         * Indicates the operation type of the task.
-         */
-        public final int id;
-        /**
-         * Same operation for a different phoneAccountHandle is allowed. phoneAccountHandle is used
-         * to differentiate phone accounts in multi-SIM scenario. For example, each SIM can queue a
-         * sync task for their own.
-         */
-        public final PhoneAccountHandle phoneAccountHandle;
-
-        public TaskId(int id, PhoneAccountHandle phoneAccountHandle) {
-            this.id = id;
-            this.phoneAccountHandle = phoneAccountHandle;
-        }
-
-        @Override
-        public boolean equals(Object object) {
-            if (!(object instanceof TaskId)) {
-                return false;
-            }
-            TaskId other = (TaskId) object;
-            return id == other.id && phoneAccountHandle.equals(other.phoneAccountHandle);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(id, phoneAccountHandle);
-        }
-    }
-
-    TaskId getId();
-
-    @MainThread
-    void onCreate(Context context, Intent intent, int flags, int startId);
-
-    /**
-     * @return number of milliSeconds the scheduler should wait before running this task. A value
-     * less than {@link TaskSchedulerService#READY_TOLERANCE_MILLISECONDS} will be considered ready.
-     * If no tasks are ready, the scheduler will sleep for this amount of time before doing another
-     * check (it will still wake if a new task is added). The first task in the queue that is ready
-     * will be executed.
-     */
-    @MainThread
-    long getReadyInMilliSeconds();
-
-    /**
-     * Called on the main thread when the scheduler is about to send the task into the worker
-     * thread, calling {@link #onExecuteInBackgroundThread()}
-     */
-    @MainThread
-    void onBeforeExecute();
-
-    /**
-     * The actual payload of the task, executed on the worker thread.
-     */
-    @WorkerThread
-    void onExecuteInBackgroundThread();
-
-    /**
-     * Called on the main thread when {@link #onExecuteInBackgroundThread()} has finished or thrown
-     * an uncaught exception. The task is already removed from the queue at this point, and a same
-     * task can be queued again.
-     */
-    @MainThread
-    void onCompleted();
-
-    /**
-     * Another task with the same TaskId has been added. Necessary data can be retrieved from the
-     * other task, and after this returns the task will be discarded.
-     */
-    @MainThread
-    void onDuplicatedTaskAdded(Task task);
-}
diff --git a/java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java b/java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java
deleted file mode 100644
index 90b50e9..0000000
--- a/java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java
+++ /dev/null
@@ -1,392 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.scheduling;
-
-import android.app.AlarmManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Binder;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.PowerManager;
-import android.os.PowerManager.WakeLock;
-import android.os.SystemClock;
-import android.support.annotation.MainThread;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.support.annotation.WorkerThread;
-import com.android.voicemailomtp.Assert;
-import com.android.voicemailomtp.NeededForTesting;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.scheduling.Task.TaskId;
-import java.util.ArrayDeque;
-import java.util.Queue;
-
-/**
- * A service to queue and run {@link Task} on a worker thread. Only one task will be ran at a time,
- * and same task cannot exist in the queue at the same time. The service will be started when a
- * intent is received, and stopped when there are no more tasks in the queue.
- */
-public class TaskSchedulerService extends Service {
-
-    private static final String TAG = "VvmTaskScheduler";
-
-    private static final String ACTION_WAKEUP = "action_wakeup";
-
-    private static final int READY_TOLERANCE_MILLISECONDS = 100;
-
-    /**
-     * Threshold to determine whether to do a short or long sleep when a task is scheduled in the
-     * future.
-     *
-     * <p>A short sleep will continue to held the wake lock and use {@link
-     * Handler#postDelayed(Runnable, long)} to wait for the next task.
-     *
-     * <p>A long sleep will release the wake lock and set a {@link AlarmManager} alarm. The alarm is
-     * exact and will wake up the device. Note: as this service is run in the telephony process it
-     * does not seem to be restricted by doze or sleep, it will fire exactly at the moment. The
-     * unbundled version should take doze into account.
-     */
-    private static final int SHORT_SLEEP_THRESHOLD_MILLISECONDS = 60_000;
-    /**
-     * When there are no more tasks to be run the service should be stopped. But when all tasks has
-     * finished there might still be more tasks in the message queue waiting to be processed,
-     * especially the ones submitted in {@link Task#onCompleted()}. Wait for a while before stopping
-     * the service to make sure there are no pending messages.
-     */
-    private static final int STOP_DELAY_MILLISECONDS = 5_000;
-    private static final String EXTRA_CLASS_NAME = "extra_class_name";
-
-    private static final String WAKE_LOCK_TAG = "TaskSchedulerService_wakelock";
-
-    // The thread to run tasks on
-    private volatile WorkerThreadHandler mWorkerThreadHandler;
-
-    private Context mContext = this;
-    /**
-     * Used by tests to turn task handling into a single threaded process by calling {@link
-     * Handler#handleMessage(Message)} directly
-     */
-    private MessageSender mMessageSender = new MessageSender();
-
-    private MainThreadHandler mMainThreadHandler;
-
-    private WakeLock mWakeLock;
-
-    /**
-     * Main thread only, access through {@link #getTasks()}
-     */
-    private final Queue<Task> mTasks = new ArrayDeque<>();
-    private boolean mWorkerThreadIsBusy = false;
-
-    private final Runnable mStopServiceWithDelay = new Runnable() {
-        @Override
-        public void run() {
-            VvmLog.d(TAG, "Stopping service");
-            stopSelf();
-        }
-    };
-    /**
-     * Should attempt to run the next task when a task has finished or been added.
-     */
-    private boolean mTaskAutoRunDisabledForTesting = false;
-
-    @VisibleForTesting
-    final class WorkerThreadHandler extends Handler {
-
-        public WorkerThreadHandler(Looper looper) {
-            super(looper);
-        }
-
-        @Override
-        @WorkerThread
-        public void handleMessage(Message msg) {
-            Assert.isNotMainThread();
-            Task task = (Task) msg.obj;
-            try {
-                VvmLog.v(TAG, "executing task " + task);
-                task.onExecuteInBackgroundThread();
-            } catch (Throwable throwable) {
-                VvmLog.e(TAG, "Exception while executing task " + task + ":", throwable);
-            }
-
-            Message schedulerMessage = mMainThreadHandler.obtainMessage();
-            schedulerMessage.obj = task;
-            mMessageSender.send(schedulerMessage);
-        }
-    }
-
-    @VisibleForTesting
-    final class MainThreadHandler extends Handler {
-
-        public MainThreadHandler(Looper looper) {
-            super(looper);
-        }
-
-        @Override
-        @MainThread
-        public void handleMessage(Message msg) {
-            Assert.isMainThread();
-            Task task = (Task) msg.obj;
-            getTasks().remove(task);
-            task.onCompleted();
-            mWorkerThreadIsBusy = false;
-            maybeRunNextTask();
-        }
-    }
-
-    @Override
-    @MainThread
-    public void onCreate() {
-        super.onCreate();
-        mWakeLock = getSystemService(PowerManager.class)
-                .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
-        mWakeLock.setReferenceCounted(false);
-        HandlerThread thread = new HandlerThread("VvmTaskSchedulerService");
-        thread.start();
-
-        mWorkerThreadHandler = new WorkerThreadHandler(thread.getLooper());
-        mMainThreadHandler = new MainThreadHandler(Looper.getMainLooper());
-    }
-
-    @Override
-    public void onDestroy() {
-        mWorkerThreadHandler.getLooper().quit();
-        mWakeLock.release();
-    }
-
-    @Override
-    @MainThread
-    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
-        Assert.isMainThread();
-        // maybeRunNextTask() will release the wakelock either by entering a long sleep or stopping
-        // the service.
-        mWakeLock.acquire();
-        if (ACTION_WAKEUP.equals(intent.getAction())) {
-            VvmLog.d(TAG, "woke up by AlarmManager");
-        } else {
-            Task task = createTask(intent, flags, startId);
-            if (task == null) {
-                VvmLog.e(TAG, "cannot create task form intent");
-            } else {
-                addTask(task);
-            }
-        }
-        maybeRunNextTask();
-        // STICKY means the service will be automatically restarted will the last intent if it is
-        // killed.
-        return START_NOT_STICKY;
-    }
-
-    @MainThread
-    @VisibleForTesting
-    void addTask(Task task) {
-        Assert.isMainThread();
-        if (task.getId().id == Task.TASK_INVALID) {
-            throw new AssertionError("Task id was not set to a valid value before adding.");
-        }
-        if (task.getId().id != Task.TASK_ALLOW_DUPLICATES) {
-            Task oldTask = getTask(task.getId());
-            if (oldTask != null) {
-                oldTask.onDuplicatedTaskAdded(task);
-                return;
-            }
-        }
-        mMainThreadHandler.removeCallbacks(mStopServiceWithDelay);
-        getTasks().add(task);
-        maybeRunNextTask();
-    }
-
-    @MainThread
-    @Nullable
-    private Task getTask(TaskId taskId) {
-        Assert.isMainThread();
-        for (Task task : getTasks()) {
-            if (task.getId().equals(taskId)) {
-                return task;
-            }
-        }
-        return null;
-    }
-
-    @MainThread
-    private Queue<Task> getTasks() {
-        Assert.isMainThread();
-        return mTasks;
-    }
-
-    /**
-     * Create an intent that will queue the <code>task</code>
-     */
-    public static Intent createIntent(Context context, Class<? extends Task> task) {
-        Intent intent = new Intent(context, TaskSchedulerService.class);
-        intent.putExtra(EXTRA_CLASS_NAME, task.getName());
-        return intent;
-    }
-
-    @VisibleForTesting
-    @MainThread
-    @Nullable
-    Task createTask(@Nullable Intent intent, int flags, int startId) {
-        Assert.isMainThread();
-        if (intent == null) {
-            return null;
-        }
-        String className = intent.getStringExtra(EXTRA_CLASS_NAME);
-        VvmLog.d(TAG, "create task:" + className);
-        if (className == null) {
-            throw new IllegalArgumentException("EXTRA_CLASS_NAME expected");
-        }
-        try {
-            Task task = (Task) Class.forName(className).newInstance();
-            task.onCreate(mContext, intent, flags, startId);
-            return task;
-        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
-            throw new IllegalArgumentException(e);
-        }
-    }
-
-    @MainThread
-    private void maybeRunNextTask() {
-        Assert.isMainThread();
-        if (mWorkerThreadIsBusy) {
-            return;
-        }
-        if (mTaskAutoRunDisabledForTesting) {
-            // If mTaskAutoRunDisabledForTesting is true, runNextTask() must be explicitly called
-            // to run the next task.
-            return;
-        }
-
-        runNextTask();
-    }
-
-    @VisibleForTesting
-    @MainThread
-    void runNextTask() {
-        Assert.isMainThread();
-        // The current alarm is no longer valid, a new one will be set up if required.
-        getSystemService(AlarmManager.class).cancel(getWakeupIntent());
-        if (getTasks().isEmpty()) {
-            prepareStop();
-            return;
-        }
-        Long minimalWaitTime = null;
-        for (Task task : getTasks()) {
-            long waitTime = task.getReadyInMilliSeconds();
-            if (waitTime < READY_TOLERANCE_MILLISECONDS) {
-                task.onBeforeExecute();
-                Message message = mWorkerThreadHandler.obtainMessage();
-                message.obj = task;
-                mWorkerThreadIsBusy = true;
-                mMessageSender.send(message);
-                return;
-            } else {
-                if (minimalWaitTime == null || waitTime < minimalWaitTime) {
-                    minimalWaitTime = waitTime;
-                }
-            }
-        }
-        VvmLog.d(TAG, "minimal wait time:" + minimalWaitTime);
-        if (!mTaskAutoRunDisabledForTesting && minimalWaitTime != null) {
-            // No tasks are currently ready. Sleep until the next one should be.
-            // If a new task is added during the sleep the service will wake immediately.
-            sleep(minimalWaitTime);
-        }
-    }
-
-    private void sleep(long timeMillis) {
-        if (timeMillis < SHORT_SLEEP_THRESHOLD_MILLISECONDS) {
-            mMainThreadHandler.postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    maybeRunNextTask();
-                }
-            }, timeMillis);
-            return;
-        }
-
-        // Tasks does not have a strict timing requirement, use AlarmManager.set() so the OS could
-        // optimize the battery usage. As this service currently run in the telephony process the
-        // OS give it privileges to behave the same as setExact(), but set() is the targeted
-        // behavior once this is unbundled.
-        getSystemService(AlarmManager.class).set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
-                SystemClock.elapsedRealtime() + timeMillis,
-                getWakeupIntent());
-        mWakeLock.release();
-        VvmLog.d(TAG, "Long sleep for " + timeMillis + " millis");
-    }
-
-    private PendingIntent getWakeupIntent() {
-        Intent intent = new Intent(ACTION_WAKEUP, null, this, getClass());
-        return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
-    }
-
-    private void prepareStop() {
-        VvmLog.d(TAG,
-                "No more tasks, stopping service if no task are added in "
-                        + STOP_DELAY_MILLISECONDS + " millis");
-        mMainThreadHandler.postDelayed(mStopServiceWithDelay, STOP_DELAY_MILLISECONDS);
-    }
-
-    static class MessageSender {
-
-        public void send(Message message) {
-            message.sendToTarget();
-        }
-    }
-
-    @NeededForTesting
-    void setContextForTest(Context context) {
-        mContext = context;
-    }
-
-    @NeededForTesting
-    void setTaskAutoRunDisabledForTest(boolean value) {
-        mTaskAutoRunDisabledForTesting = value;
-    }
-
-    @NeededForTesting
-    void setMessageSenderForTest(MessageSender sender) {
-        mMessageSender = sender;
-    }
-
-    @NeededForTesting
-    void clearTasksForTest() {
-        mTasks.clear();
-    }
-
-    @Override
-    @Nullable
-    public IBinder onBind(Intent intent) {
-        return new LocalBinder();
-    }
-
-    @NeededForTesting
-    class LocalBinder extends Binder {
-
-        @NeededForTesting
-        public TaskSchedulerService getService() {
-            return TaskSchedulerService.this;
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java b/java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java
deleted file mode 100644
index 5cec528..0000000
--- a/java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.settings;
-
-import android.content.Context;
-import android.telecom.PhoneAccountHandle;
-
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VisualVoicemailPreferences;
-import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
-
-/**
- * Save whether or not a particular account is enabled in shared to be retrieved later.
- */
-public class VisualVoicemailSettingsUtil {
-
-    private static final String IS_ENABLED_KEY = "is_enabled";
-
-
-    public static void setEnabled(Context context, PhoneAccountHandle phoneAccount,
-            boolean isEnabled) {
-        new VisualVoicemailPreferences(context, phoneAccount).edit()
-                .putBoolean(IS_ENABLED_KEY, isEnabled)
-                .apply();
-        OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(context, phoneAccount);
-        if (isEnabled) {
-            OmtpVvmSourceManager.getInstance(context).addPhoneStateListener(phoneAccount);
-            config.startActivation();
-        } else {
-            OmtpVvmSourceManager.getInstance(context).removeSource(phoneAccount);
-            config.startDeactivation();
-        }
-    }
-
-    public static boolean isEnabled(Context context,
-            PhoneAccountHandle phoneAccount) {
-        if (phoneAccount == null) {
-            return false;
-        }
-
-        VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount);
-        if (prefs.contains(IS_ENABLED_KEY)) {
-            // isEnableByDefault is a bit expensive, so don't use it as default value of
-            // getBoolean(). The "false" here should never be actually used.
-            return prefs.getBoolean(IS_ENABLED_KEY, false);
-        }
-        return new OmtpVvmCarrierConfigHelper(context, phoneAccount).isEnabledByDefault();
-    }
-
-    /**
-     * Whether the client enabled status is explicitly set by user or by default(Whether carrier VVM
-     * app is installed). This is used to determine whether to disable the client when the carrier
-     * VVM app is installed. If the carrier VVM app is installed the client should give priority to
-     * it if the settings are not touched.
-     */
-    public static boolean isEnabledUserSet(Context context,
-            PhoneAccountHandle phoneAccount) {
-        if (phoneAccount == null) {
-            return false;
-        }
-        VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount);
-        return prefs.contains(IS_ENABLED_KEY);
-    }
-}
diff --git a/java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java b/java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java
deleted file mode 100644
index e679e99..0000000
--- a/java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java
+++ /dev/null
@@ -1,634 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.settings;
-
-import android.annotation.TargetApi;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnDismissListener;
-import android.net.Network;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import android.text.Editable;
-import android.text.InputFilter;
-import android.text.InputFilter.LengthFilter;
-import android.text.TextWatcher;
-import android.view.KeyEvent;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.WindowManager;
-import android.view.inputmethod.EditorInfo;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.TextView;
-import android.widget.TextView.OnEditorActionListener;
-import android.widget.Toast;
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.OmtpConstants.ChangePinResult;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.R;
-import com.android.voicemailomtp.VisualVoicemailPreferences;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.imap.ImapHelper;
-import com.android.voicemailomtp.imap.ImapHelper.InitializingException;
-import com.android.voicemailomtp.mail.MessagingException;
-import com.android.voicemailomtp.sync.VvmNetworkRequestCallback;
-
-/**
- * Dialog to change the voicemail PIN. The TUI (Telephony User Interface) PIN is used when accessing
- * traditional voicemail through phone call. The intent to launch this activity must contain {@link
- * #EXTRA_PHONE_ACCOUNT_HANDLE}
- */
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class VoicemailChangePinActivity extends Activity
-    implements OnClickListener, OnEditorActionListener, TextWatcher {
-
-    private static final String TAG = "VmChangePinActivity";
-
-    public static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
-
-    private static final String KEY_DEFAULT_OLD_PIN = "default_old_pin";
-
-    private static final int MESSAGE_HANDLE_RESULT = 1;
-
-    private PhoneAccountHandle mPhoneAccountHandle;
-    private OmtpVvmCarrierConfigHelper mConfig;
-
-    private int mPinMinLength;
-    private int mPinMaxLength;
-
-    private State mUiState = State.Initial;
-    private String mOldPin;
-    private String mFirstPin;
-
-    private ProgressDialog mProgressDialog;
-
-    private TextView mHeaderText;
-    private TextView mHintText;
-    private TextView mErrorText;
-    private EditText mPinEntry;
-    private Button mCancelButton;
-    private Button mNextButton;
-
-    private Handler mHandler = new Handler() {
-        @Override
-        public void handleMessage(Message message) {
-            if (message.what == MESSAGE_HANDLE_RESULT) {
-                mUiState.handleResult(VoicemailChangePinActivity.this, message.arg1);
-            }
-        }
-    };
-
-    private enum State {
-        /**
-         * Empty state to handle initial state transition. Will immediately switch into {@link
-         * #VerifyOldPin} if a default PIN has been set by the OMTP client, or {@link #EnterOldPin}
-         * if not.
-         */
-        Initial,
-        /**
-         * Prompt the user to enter old PIN. The PIN will be verified with the server before
-         * proceeding to {@link #EnterNewPin}.
-         */
-        EnterOldPin {
-            @Override
-            public void onEnter(VoicemailChangePinActivity activity) {
-                activity.setHeader(R.string.change_pin_enter_old_pin_header);
-                activity.mHintText.setText(R.string.change_pin_enter_old_pin_hint);
-                activity.mNextButton.setText(R.string.change_pin_continue_label);
-                activity.mErrorText.setText(null);
-            }
-
-            @Override
-            public void onInputChanged(VoicemailChangePinActivity activity) {
-                activity.setNextEnabled(activity.getCurrentPasswordInput().length() > 0);
-            }
-
-
-            @Override
-            public void handleNext(VoicemailChangePinActivity activity) {
-                activity.mOldPin = activity.getCurrentPasswordInput();
-                activity.verifyOldPin();
-            }
-
-            @Override
-            public void handleResult(VoicemailChangePinActivity activity,
-                    @ChangePinResult int result) {
-                if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
-                    activity.updateState(State.EnterNewPin);
-                } else {
-                    CharSequence message = activity.getChangePinResultMessage(result);
-                    activity.showError(message);
-                    activity.mPinEntry.setText("");
-                }
-            }
-        },
-        /**
-         * The default old PIN is found. Show a blank screen while verifying with the server to make
-         * sure the PIN is still valid. If the PIN is still valid, proceed to {@link #EnterNewPin}.
-         * If not, the user probably changed the PIN through other means, proceed to {@link
-         * #EnterOldPin}. If any other issue caused the verifying to fail, show an error and exit.
-         */
-        VerifyOldPin {
-            @Override
-            public void onEnter(VoicemailChangePinActivity activity) {
-                activity.findViewById(android.R.id.content).setVisibility(View.INVISIBLE);
-                activity.verifyOldPin();
-            }
-
-            @Override
-            public void handleResult(final VoicemailChangePinActivity activity,
-                    @ChangePinResult int result) {
-                if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
-                    activity.updateState(State.EnterNewPin);
-                } else if (result == OmtpConstants.CHANGE_PIN_SYSTEM_ERROR) {
-                    activity.getWindow().setSoftInputMode(
-                            WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
-                    activity.showError(activity.getString(R.string.change_pin_system_error),
-                            new OnDismissListener() {
-                                @Override
-                                public void onDismiss(DialogInterface dialog) {
-                                    activity.finish();
-                                }
-                            });
-                } else {
-                    VvmLog.e(TAG, "invalid default old PIN: " + activity
-                            .getChangePinResultMessage(result));
-                    // If the default old PIN is rejected by the server, the PIN is probably changed
-                    // through other means, or the generated pin is invalid
-                    // Wipe the default old PIN so the old PIN input box will be shown to the user
-                    // on the next time.
-                    setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null);
-                    activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET);
-                    activity.updateState(State.EnterOldPin);
-                }
-            }
-
-            @Override
-            public void onLeave(VoicemailChangePinActivity activity) {
-                activity.findViewById(android.R.id.content).setVisibility(View.VISIBLE);
-            }
-        },
-        /**
-         * Let the user enter the new PIN and validate the format. Only length is enforced, PIN
-         * strength check relies on the server. After a valid PIN is entered, proceed to {@link
-         * #ConfirmNewPin}
-         */
-        EnterNewPin {
-            @Override
-            public void onEnter(VoicemailChangePinActivity activity) {
-                activity.mHeaderText.setText(R.string.change_pin_enter_new_pin_header);
-                activity.mNextButton.setText(R.string.change_pin_continue_label);
-                activity.mHintText.setText(
-                        activity.getString(R.string.change_pin_enter_new_pin_hint,
-                                activity.mPinMinLength, activity.mPinMaxLength));
-            }
-
-            @Override
-            public void onInputChanged(VoicemailChangePinActivity activity) {
-                String password = activity.getCurrentPasswordInput();
-                if (password.length() == 0) {
-                    activity.setNextEnabled(false);
-                    return;
-                }
-                CharSequence error = activity.validatePassword(password);
-                if (error != null) {
-                    activity.mErrorText.setText(error);
-                    activity.setNextEnabled(false);
-                } else {
-                    activity.mErrorText.setText(null);
-                    activity.setNextEnabled(true);
-                }
-            }
-
-            @Override
-            public void handleNext(VoicemailChangePinActivity activity) {
-                CharSequence errorMsg;
-                errorMsg = activity.validatePassword(activity.getCurrentPasswordInput());
-                if (errorMsg != null) {
-                    activity.showError(errorMsg);
-                    return;
-                }
-                activity.mFirstPin = activity.getCurrentPasswordInput();
-                activity.updateState(State.ConfirmNewPin);
-            }
-        },
-        /**
-         * Let the user type in the same PIN again to avoid typos. If the PIN matches then perform a
-         * PIN change to the server. Finish the activity if succeeded. Return to {@link
-         * #EnterOldPin} if the old PIN is rejected, {@link #EnterNewPin} for other failure.
-         */
-        ConfirmNewPin {
-            @Override
-            public void onEnter(VoicemailChangePinActivity activity) {
-                activity.mHeaderText.setText(R.string.change_pin_confirm_pin_header);
-                activity.mHintText.setText(null);
-                activity.mNextButton.setText(R.string.change_pin_ok_label);
-            }
-
-            @Override
-            public void onInputChanged(VoicemailChangePinActivity activity) {
-                if (activity.getCurrentPasswordInput().length() == 0) {
-                    activity.setNextEnabled(false);
-                    return;
-                }
-                if (activity.getCurrentPasswordInput().equals(activity.mFirstPin)) {
-                    activity.setNextEnabled(true);
-                    activity.mErrorText.setText(null);
-                } else {
-                    activity.setNextEnabled(false);
-                    activity.mErrorText.setText(R.string.change_pin_confirm_pins_dont_match);
-                }
-            }
-
-            @Override
-            public void handleResult(VoicemailChangePinActivity activity,
-                    @ChangePinResult int result) {
-                if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
-                    // If the PIN change succeeded we no longer know what the old (current) PIN is.
-                    // Wipe the default old PIN so the old PIN input box will be shown to the user
-                    // on the next time.
-                    setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null);
-                    activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET);
-
-                    activity.finish();
-
-                    Toast.makeText(activity, activity.getString(R.string.change_pin_succeeded),
-                            Toast.LENGTH_SHORT).show();
-                } else {
-                    CharSequence message = activity.getChangePinResultMessage(result);
-                    VvmLog.i(TAG, "Change PIN failed: " + message);
-                    activity.showError(message);
-                    if (result == OmtpConstants.CHANGE_PIN_MISMATCH) {
-                        // Somehow the PIN has changed, prompt to enter the old PIN again.
-                        activity.updateState(State.EnterOldPin);
-                    } else {
-                        // The new PIN failed to fulfil other restrictions imposed by the server.
-                        activity.updateState(State.EnterNewPin);
-                    }
-
-                }
-
-            }
-
-            @Override
-            public void handleNext(VoicemailChangePinActivity activity) {
-                activity.processPinChange(activity.mOldPin, activity.mFirstPin);
-            }
-        };
-
-        /**
-         * The activity has switched from another state to this one.
-         */
-        public void onEnter(VoicemailChangePinActivity activity) {
-            // Do nothing
-        }
-
-        /**
-         * The user has typed something into the PIN input field. Also called after {@link
-         * #onEnter(VoicemailChangePinActivity)}
-         */
-        public void onInputChanged(VoicemailChangePinActivity activity) {
-            // Do nothing
-        }
-
-        /**
-         * The asynchronous call to change the PIN on the server has returned.
-         */
-        public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) {
-            // Do nothing
-        }
-
-        /**
-         * The user has pressed the "next" button.
-         */
-        public void handleNext(VoicemailChangePinActivity activity) {
-            // Do nothing
-        }
-
-        /**
-         * The activity has switched from this state to another one.
-         */
-        public void onLeave(VoicemailChangePinActivity activity) {
-            // Do nothing
-        }
-
-    }
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        mPhoneAccountHandle = getIntent().getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
-        mConfig = new OmtpVvmCarrierConfigHelper(this, mPhoneAccountHandle);
-        setContentView(R.layout.voicemail_change_pin);
-        setTitle(R.string.change_pin_title);
-
-        readPinLength();
-
-        View view = findViewById(android.R.id.content);
-
-        mCancelButton = (Button) view.findViewById(R.id.cancel_button);
-        mCancelButton.setOnClickListener(this);
-        mNextButton = (Button) view.findViewById(R.id.next_button);
-        mNextButton.setOnClickListener(this);
-
-        mPinEntry = (EditText) view.findViewById(R.id.pin_entry);
-        mPinEntry.setOnEditorActionListener(this);
-        mPinEntry.addTextChangedListener(this);
-        if (mPinMaxLength != 0) {
-            mPinEntry.setFilters(new InputFilter[]{new LengthFilter(mPinMaxLength)});
-        }
-
-
-        mHeaderText = (TextView) view.findViewById(R.id.headerText);
-        mHintText = (TextView) view.findViewById(R.id.hintText);
-        mErrorText = (TextView) view.findViewById(R.id.errorText);
-
-        if (isDefaultOldPinSet(this, mPhoneAccountHandle)) {
-            mOldPin = getDefaultOldPin(this, mPhoneAccountHandle);
-            updateState(State.VerifyOldPin);
-        } else {
-            updateState(State.EnterOldPin);
-        }
-    }
-
-    private void handleOmtpEvent(OmtpEvents event) {
-        mConfig.handleEvent(getVoicemailStatusEditor(), event);
-    }
-
-    private VoicemailStatus.Editor getVoicemailStatusEditor() {
-        // This activity does not have any automatic retry mechanism, errors should be written right
-        // away.
-        return VoicemailStatus.edit(this, mPhoneAccountHandle);
-    }
-
-    /**
-     * Extracts the pin length requirement sent by the server with a STATUS SMS.
-     */
-    private void readPinLength() {
-        VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(this,
-                mPhoneAccountHandle);
-        // The OMTP pin length format is {min}-{max}
-        String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-");
-        if (lengths.length == 2) {
-            try {
-                mPinMinLength = Integer.parseInt(lengths[0]);
-                mPinMaxLength = Integer.parseInt(lengths[1]);
-            } catch (NumberFormatException e) {
-                mPinMinLength = 0;
-                mPinMaxLength = 0;
-            }
-        } else {
-            mPinMinLength = 0;
-            mPinMaxLength = 0;
-        }
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-        updateState(mUiState);
-
-    }
-
-    public void handleNext() {
-        if (mPinEntry.length() == 0) {
-            return;
-        }
-        mUiState.handleNext(this);
-    }
-
-    public void onClick(View v) {
-    if (v.getId() == R.id.next_button) {
-      handleNext();
-    } else if (v.getId() == R.id.cancel_button) {
-      finish();
-        }
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        if (item.getItemId() == android.R.id.home) {
-            onBackPressed();
-            return true;
-        }
-        return super.onOptionsItemSelected(item);
-    }
-
-    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
-      if (!mNextButton.isEnabled()) {
-        return true;
-      }
-        // Check if this was the result of hitting the enter or "done" key
-        if (actionId == EditorInfo.IME_NULL
-                || actionId == EditorInfo.IME_ACTION_DONE
-                || actionId == EditorInfo.IME_ACTION_NEXT) {
-            handleNext();
-            return true;
-        }
-        return false;
-    }
-
-    public void afterTextChanged(Editable s) {
-        mUiState.onInputChanged(this);
-    }
-
-    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-        // Do nothing
-    }
-
-    public void onTextChanged(CharSequence s, int start, int before, int count) {
-        // Do nothing
-    }
-
-    /**
-     * After replacing the default PIN with a random PIN, call this to store the random PIN. The
-     * stored PIN will be automatically entered when the user attempts to change the PIN.
-     */
-    public static void setDefaultOldPIN(Context context, PhoneAccountHandle phoneAccountHandle,
-            String pin) {
-        new VisualVoicemailPreferences(context, phoneAccountHandle)
-                .edit().putString(KEY_DEFAULT_OLD_PIN, pin).apply();
-    }
-
-    public static boolean isDefaultOldPinSet(Context context,
-            PhoneAccountHandle phoneAccountHandle) {
-        return getDefaultOldPin(context, phoneAccountHandle) != null;
-    }
-
-    private static String getDefaultOldPin(Context context, PhoneAccountHandle phoneAccountHandle) {
-        return new VisualVoicemailPreferences(context, phoneAccountHandle)
-                .getString(KEY_DEFAULT_OLD_PIN);
-    }
-
-    private String getCurrentPasswordInput() {
-        return mPinEntry.getText().toString();
-    }
-
-    private void updateState(State state) {
-        State previousState = mUiState;
-        mUiState = state;
-        if (previousState != state) {
-            previousState.onLeave(this);
-            mPinEntry.setText("");
-            mUiState.onEnter(this);
-        }
-        mUiState.onInputChanged(this);
-    }
-
-    /**
-     * Validates PIN and returns a message to display if PIN fails test.
-     *
-     * @param password the raw password the user typed in
-     * @return error message to show to user or null if password is OK
-     */
-    private CharSequence validatePassword(String password) {
-        if (mPinMinLength == 0 && mPinMaxLength == 0) {
-            // Invalid length requirement is sent by the server, just accept anything and let the
-            // server decide.
-            return null;
-        }
-
-        if (password.length() < mPinMinLength) {
-            return getString(R.string.vm_change_pin_error_too_short);
-        }
-        return null;
-    }
-
-    private void setHeader(int text) {
-        mHeaderText.setText(text);
-        mPinEntry.setContentDescription(mHeaderText.getText());
-    }
-
-    /**
-     * Get the corresponding message for the {@link ChangePinResult}.<code>result</code> must not
-     * {@link OmtpConstants#CHANGE_PIN_SUCCESS}
-     */
-    private CharSequence getChangePinResultMessage(@ChangePinResult int result) {
-        switch (result) {
-            case OmtpConstants.CHANGE_PIN_TOO_SHORT:
-                return getString(R.string.vm_change_pin_error_too_short);
-            case OmtpConstants.CHANGE_PIN_TOO_LONG:
-                return getString(R.string.vm_change_pin_error_too_long);
-            case OmtpConstants.CHANGE_PIN_TOO_WEAK:
-                return getString(R.string.vm_change_pin_error_too_weak);
-            case OmtpConstants.CHANGE_PIN_INVALID_CHARACTER:
-                return getString(R.string.vm_change_pin_error_invalid);
-            case OmtpConstants.CHANGE_PIN_MISMATCH:
-                return getString(R.string.vm_change_pin_error_mismatch);
-            case OmtpConstants.CHANGE_PIN_SYSTEM_ERROR:
-                return getString(R.string.vm_change_pin_error_system_error);
-            default:
-                VvmLog.wtf(TAG, "Unexpected ChangePinResult " + result);
-                return null;
-        }
-    }
-
-    private void verifyOldPin() {
-        processPinChange(mOldPin, mOldPin);
-    }
-
-    private void setNextEnabled(boolean enabled) {
-        mNextButton.setEnabled(enabled);
-    }
-
-
-    private void showError(CharSequence message) {
-        showError(message, null);
-    }
-
-    private void showError(CharSequence message, @Nullable OnDismissListener callback) {
-        new AlertDialog.Builder(this)
-                .setMessage(message)
-                .setPositiveButton(android.R.string.ok, null)
-                .setOnDismissListener(callback)
-                .show();
-    }
-
-    /**
-     * Asynchronous call to change the PIN on the server.
-     */
-    private void processPinChange(String oldPin, String newPin) {
-        mProgressDialog = new ProgressDialog(this);
-        mProgressDialog.setCancelable(false);
-        mProgressDialog.setMessage(getString(R.string.vm_change_pin_progress_message));
-        mProgressDialog.show();
-
-        ChangePinNetworkRequestCallback callback = new ChangePinNetworkRequestCallback(oldPin,
-                newPin);
-        callback.requestNetwork();
-    }
-
-    private class ChangePinNetworkRequestCallback extends VvmNetworkRequestCallback {
-
-        private final String mOldPin;
-        private final String mNewPin;
-
-        public ChangePinNetworkRequestCallback(String oldPin, String newPin) {
-            super(mConfig, mPhoneAccountHandle,
-                VoicemailChangePinActivity.this.getVoicemailStatusEditor());
-            mOldPin = oldPin;
-            mNewPin = newPin;
-        }
-
-        @Override
-        public void onAvailable(Network network) {
-            super.onAvailable(network);
-            try (ImapHelper helper =
-                new ImapHelper(VoicemailChangePinActivity.this, mPhoneAccountHandle, network,
-                    getVoicemailStatusEditor())) {
-
-                @ChangePinResult int result =
-                        helper.changePin(mOldPin, mNewPin);
-                sendResult(result);
-            } catch (InitializingException | MessagingException e) {
-                VvmLog.e(TAG, "ChangePinNetworkRequestCallback: onAvailable: ", e);
-                sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR);
-            }
-        }
-
-        @Override
-        public void onFailed(String reason) {
-            super.onFailed(reason);
-            sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR);
-        }
-
-        private void sendResult(@ChangePinResult int result) {
-            VvmLog.i(TAG, "Change PIN result: " + result);
-            if (mProgressDialog.isShowing() && !VoicemailChangePinActivity.this.isDestroyed() &&
-                    !VoicemailChangePinActivity.this.isFinishing()) {
-                mProgressDialog.dismiss();
-            } else {
-                VvmLog.i(TAG, "Dialog not visible, not dismissing");
-            }
-            mHandler.obtainMessage(MESSAGE_HANDLE_RESULT, result, 0).sendToTarget();
-            releaseNetwork();
-        }
-    }
-
-}
diff --git a/java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java b/java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java
deleted file mode 100644
index ac0df6f..0000000
--- a/java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java
+++ /dev/null
@@ -1,222 +0,0 @@
-/**
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.settings;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.preference.Preference;
-import android.preference.PreferenceActivity;
-import android.preference.PreferenceScreen;
-import android.preference.SwitchPreference;
-import android.telecom.PhoneAccountHandle;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.MenuItem;
-
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.R;
-import com.android.voicemailomtp.SubscriptionInfoHelper;
-import com.android.voicemailomtp.VisualVoicemailPreferences;
-
-public class VoicemailSettingsActivity extends PreferenceActivity implements
-        Preference.OnPreferenceChangeListener {
-    private static final String LOG_TAG = VoicemailSettingsActivity.class.getSimpleName();
-    private static final boolean DBG = true;
-
-    /**
-     * Intent action to bring up Voicemail Provider settings
-     * DO NOT RENAME. There are existing apps which use this intent value.
-     */
-    public static final String ACTION_ADD_VOICEMAIL =
-            "com.android.voicemailomtp.CallFeaturesSetting.ADD_VOICEMAIL";
-
-    /**
-     * Intent action to bring up the {@code VoicemailSettingsActivity}.
-     * DO NOT RENAME. There are existing apps which use this intent value.
-     */
-    public static final String ACTION_CONFIGURE_VOICEMAIL =
-            "com.android.voicemailomtp.CallFeaturesSetting.CONFIGURE_VOICEMAIL";
-
-    // Extra put in the return from VM provider config containing voicemail number to set
-    public static final String VM_NUMBER_EXTRA = "com.android.voicemailomtp.VoicemailNumber";
-    // Extra put in the return from VM provider config containing call forwarding number to set
-    public static final String FWD_NUMBER_EXTRA = "com.android.voicemailomtp.ForwardingNumber";
-    // Extra put in the return from VM provider config containing call forwarding number to set
-    public static final String FWD_NUMBER_TIME_EXTRA = "com.android.voicemailomtp.ForwardingNumberTime";
-    // If the VM provider returns non null value in this extra we will force the user to
-    // choose another VM provider
-    public static final String SIGNOUT_EXTRA = "com.android.voicemailomtp.Signout";
-
-    /**
-     * String Extra put into ACTION_ADD_VOICEMAIL call to indicate which provider should be hidden
-     * in the list of providers presented to the user. This allows a provider which is being
-     * disabled (e.g. GV user logging out) to force the user to pick some other provider.
-     */
-    public static final String IGNORE_PROVIDER_EXTRA = "com.android.voicemailomtp.ProviderToIgnore";
-
-    /**
-     * String Extra put into ACTION_ADD_VOICEMAIL to indicate that the voicemail setup screen should
-     * be opened.
-     */
-    public static final String SETUP_VOICEMAIL_EXTRA = "com.android.voicemailomtp.SetupVoicemail";
-
-    /** Event for Async voicemail change call */
-    private static final int EVENT_VOICEMAIL_CHANGED        = 500;
-    private static final int EVENT_FORWARDING_CHANGED       = 501;
-    private static final int EVENT_FORWARDING_GET_COMPLETED = 502;
-
-    /** Handle to voicemail pref */
-    private static final int VOICEMAIL_PREF_ID = 1;
-    private static final int VOICEMAIL_PROVIDER_CFG_ID = 2;
-
-    /**
-     * Used to indicate that the voicemail preference should be shown.
-     */
-    private boolean mShowVoicemailPreference = false;
-
-    private int mSubId;
-    private PhoneAccountHandle mPhoneAccountHandle;
-    private SubscriptionInfoHelper mSubscriptionInfoHelper;
-    private OmtpVvmCarrierConfigHelper mOmtpVvmCarrierConfigHelper;
-
-    private SwitchPreference mVoicemailVisualVoicemail;
-    private Preference mVoicemailChangePinPreference;
-
-    //*********************************************************************************************
-    // Preference Activity Methods
-    //*********************************************************************************************
-
-    @Override
-    protected void onCreate(Bundle icicle) {
-        super.onCreate(icicle);
-
-        // Show the voicemail preference in onResume if the calling intent specifies the
-        // ACTION_ADD_VOICEMAIL action.
-        mShowVoicemailPreference = (icicle == null) &&
-                TextUtils.equals(getIntent().getAction(), ACTION_ADD_VOICEMAIL);
-
-        mSubscriptionInfoHelper = new SubscriptionInfoHelper(this, getIntent());
-        mSubscriptionInfoHelper.setActionBarTitle(
-                getActionBar(), getResources(), R.string.voicemail_settings_with_label);
-        mSubId = mSubscriptionInfoHelper.getSubId();
-        // TODO: scrap this activity.
-        /*
-        mPhoneAccountHandle = PhoneAccountHandleConverter
-                .fromSubId(this, mSubId);
-
-        mOmtpVvmCarrierConfigHelper = new OmtpVvmCarrierConfigHelper(
-                this, mSubId);
-        */
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-
-        PreferenceScreen preferenceScreen = getPreferenceScreen();
-        if (preferenceScreen != null) {
-            preferenceScreen.removeAll();
-        }
-
-        addPreferencesFromResource(R.xml.voicemail_settings);
-
-        PreferenceScreen prefSet = getPreferenceScreen();
-
-        mVoicemailVisualVoicemail = (SwitchPreference) findPreference(
-                getResources().getString(R.string.voicemail_visual_voicemail_key));
-
-        mVoicemailChangePinPreference = findPreference(
-                getResources().getString(R.string.voicemail_change_pin_key));
-        Intent changePinIntent = new Intent(new Intent(this, VoicemailChangePinActivity.class));
-        changePinIntent.putExtra(VoicemailChangePinActivity.EXTRA_PHONE_ACCOUNT_HANDLE,
-                mPhoneAccountHandle);
-
-        mVoicemailChangePinPreference.setIntent(changePinIntent);
-        if (VoicemailChangePinActivity.isDefaultOldPinSet(this, mPhoneAccountHandle)) {
-            mVoicemailChangePinPreference.setTitle(R.string.voicemail_set_pin_dialog_title);
-        } else {
-            mVoicemailChangePinPreference.setTitle(R.string.voicemail_change_pin_dialog_title);
-        }
-
-        if (mOmtpVvmCarrierConfigHelper.isValid()) {
-            mVoicemailVisualVoicemail.setOnPreferenceChangeListener(this);
-            mVoicemailVisualVoicemail.setChecked(
-                    VisualVoicemailSettingsUtil.isEnabled(this, mPhoneAccountHandle));
-            if (!isVisualVoicemailActivated()) {
-                prefSet.removePreference(mVoicemailChangePinPreference);
-            }
-        } else {
-            prefSet.removePreference(mVoicemailVisualVoicemail);
-            prefSet.removePreference(mVoicemailChangePinPreference);
-        }
-    }
-
-    @Override
-    public void onPause() {
-        super.onPause();
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        if (item.getItemId() == android.R.id.home) {
-            onBackPressed();
-            return true;
-        }
-        return super.onOptionsItemSelected(item);
-    }
-
-    /**
-     * Implemented to support onPreferenceChangeListener to look for preference changes.
-     *
-     * @param preference is the preference to be changed
-     * @param objValue should be the value of the selection, NOT its localized
-     * display value.
-     */
-    @Override
-    public boolean onPreferenceChange(Preference preference, Object objValue) {
-        if (DBG) log("onPreferenceChange: \"" + preference + "\" changed to \"" + objValue + "\"");
-        if (preference.getKey().equals(mVoicemailVisualVoicemail.getKey())) {
-            boolean isEnabled = (boolean) objValue;
-            VisualVoicemailSettingsUtil
-                    .setEnabled(this, mPhoneAccountHandle, isEnabled);
-            PreferenceScreen prefSet = getPreferenceScreen();
-            if (isVisualVoicemailActivated()) {
-                prefSet.addPreference(mVoicemailChangePinPreference);
-            } else {
-                prefSet.removePreference(mVoicemailChangePinPreference);
-            }
-        }
-
-        // Always let the preference setting proceed.
-        return true;
-    }
-
-    private boolean isVisualVoicemailActivated() {
-        if (!VisualVoicemailSettingsUtil.isEnabled(this, mPhoneAccountHandle)) {
-            return false;
-        }
-        VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(this,
-                mPhoneAccountHandle);
-        return preferences.getString(OmtpConstants.SERVER_ADDRESS, null) != null;
-
-    }
-
-    private static void log(String msg) {
-        Log.d(LOG_TAG, msg);
-    }
-}
diff --git a/java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java b/java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java
deleted file mode 100644
index bb722bf..0000000
--- a/java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2016 Google Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.sms;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.VisualVoicemailSms;
-
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.TelephonyManagerStub;
-import com.android.voicemailomtp.VvmLog;
-
-/**
- * Class ot handle voicemail SMS under legacy mode
- *
- * @see OmtpVvmCarrierConfigHelper#isLegacyModeEnabled()
- */
-public class LegacyModeSmsHandler {
-
-    private static final String TAG = "LegacyModeSmsHandler";
-
-    public static void handle(Context context, VisualVoicemailSms sms) {
-        VvmLog.v(TAG, "processing VVM SMS on legacy mode");
-        String eventType = sms.getPrefix();
-        Bundle data = sms.getFields();
-        PhoneAccountHandle handle = sms.getPhoneAccountHandle();
-
-        if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
-            SyncMessage message = new SyncMessage(data);
-            VvmLog.v(TAG, "Received SYNC sms for " + handle +
-                    " with event " + message.getSyncTriggerEvent());
-
-            switch (message.getSyncTriggerEvent()) {
-                case OmtpConstants.NEW_MESSAGE:
-                case OmtpConstants.MAILBOX_UPDATE:
-                    // The user has called into the voicemail and the new message count could
-                    // change.
-                    // For some carriers new message count could be set to 0 even if there are still
-                    // unread messages, to clear the message waiting indicator.
-                    VvmLog.v(TAG, "updating MWI");
-
-                    // Setting voicemail message count to non-zero will show the telephony voicemail
-                    // notification, and zero will clear it.
-                    TelephonyManagerStub.showVoicemailNotification(message.getNewMessageCount());
-                    break;
-                default:
-                    break;
-            }
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java b/java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java
deleted file mode 100644
index 63af2c1..0000000
--- a/java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2015 Google Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sms;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SmsManager;
-import com.android.voicemailomtp.OmtpConstants;
-
-/**
- * An implementation of the OmtpMessageSender for T-Mobile.
- */
-public class OmtpCvvmMessageSender extends OmtpMessageSender {
-    public OmtpCvvmMessageSender(Context context, PhoneAccountHandle phoneAccountHandle,
-            short applicationPort, String destinationNumber) {
-        super(context, phoneAccountHandle, applicationPort, destinationNumber);
-    }
-
-    @Override
-    public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
-        sendCvvmMessage(OmtpConstants.ACTIVATE_REQUEST, sentIntent);
-    }
-
-    @Override
-    public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
-        sendCvvmMessage(OmtpConstants.DEACTIVATE_REQUEST, sentIntent);
-    }
-
-    @Override
-    public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
-        sendCvvmMessage(OmtpConstants.STATUS_REQUEST, sentIntent);
-    }
-
-    private void sendCvvmMessage(String request, PendingIntent sentIntent) {
-        StringBuilder sb = new StringBuilder().append(request);
-        sb.append(OmtpConstants.SMS_PREFIX_SEPARATOR);
-        appendField(sb, "dt", "15");
-        sendSms(sb.toString(), sentIntent);
-    }
-}
diff --git a/java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java b/java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java
deleted file mode 100644
index c4ad208..0000000
--- a/java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sms;
-
-import android.annotation.TargetApi;
-import android.content.BroadcastReceiver;
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.os.UserManager;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.VisualVoicemailSms;
-import com.android.voicemailomtp.ActivationTask;
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.OmtpService;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.Voicemail;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.protocol.VisualVoicemailProtocol;
-import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil;
-import com.android.voicemailomtp.sync.OmtpVvmSyncService;
-import com.android.voicemailomtp.sync.SyncOneTask;
-import com.android.voicemailomtp.sync.SyncTask;
-import com.android.voicemailomtp.sync.VoicemailsQueryHelper;
-import com.android.voicemailomtp.utils.VoicemailDatabaseUtil;
-
-/** Receive SMS messages and send for processing by the OMTP visual voicemail source. */
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class OmtpMessageReceiver extends BroadcastReceiver {
-
-    private static final String TAG = "OmtpMessageReceiver";
-
-    private Context mContext;
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        mContext = context;
-        VisualVoicemailSms sms = intent.getExtras().getParcelable(OmtpService.EXTRA_VOICEMAIL_SMS);
-        PhoneAccountHandle phone = sms.getPhoneAccountHandle();
-
-        if (phone == null) {
-            // This should never happen
-            VvmLog.i(TAG, "Received message for null phone account");
-            return;
-        }
-
-        if (!context.getSystemService(UserManager.class).isUserUnlocked()) {
-            VvmLog.i(TAG, "Received message on locked device");
-            // LegacyModeSmsHandler can handle new message notifications without storage access
-            LegacyModeSmsHandler.handle(context, sms);
-            // A full sync will happen after the device is unlocked, so nothing else need to be
-            // done.
-            return;
-        }
-
-        OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(mContext, phone);
-        if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phone)) {
-            if (helper.isLegacyModeEnabled()) {
-                LegacyModeSmsHandler.handle(context, sms);
-            } else {
-                VvmLog.i(TAG, "Received vvm message for disabled vvm source.");
-            }
-            return;
-        }
-
-        String eventType = sms.getPrefix();
-        Bundle data = sms.getFields();
-
-        if (eventType == null || data == null) {
-            VvmLog.e(TAG, "Unparsable VVM SMS received, ignoring");
-            return;
-        }
-
-        if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
-            SyncMessage message = new SyncMessage(data);
-
-            VvmLog.v(TAG, "Received SYNC sms for " + phone +
-                    " with event " + message.getSyncTriggerEvent());
-            processSync(phone, message);
-        } else if (eventType.equals(OmtpConstants.STATUS_SMS_PREFIX)) {
-            VvmLog.v(TAG, "Received Status sms for " + phone);
-            // If the STATUS SMS is initiated by ActivationTask the TaskSchedulerService will reject
-            // the follow request. Providing the data will also prevent ActivationTask from
-            // requesting another STATUS SMS. The following task will only run if the carrier
-            // spontaneous send a STATUS SMS, in that case, the VVM service should be reactivated.
-            ActivationTask.start(context, phone, data);
-        } else {
-            VvmLog.w(TAG, "Unknown prefix: " + eventType);
-            VisualVoicemailProtocol protocol = helper.getProtocol();
-            if (protocol == null) {
-                return;
-            }
-            Bundle statusData = helper.getProtocol()
-                    .translateStatusSmsBundle(helper, eventType, data);
-            if (statusData != null) {
-                VvmLog.i(TAG, "Protocol recognized the SMS as STATUS, activating");
-                ActivationTask.start(context, phone, data);
-            }
-        }
-    }
-
-    /**
-     * A sync message has two purposes: to signal a new voicemail message, and to indicate the
-     * voicemails on the server have changed remotely (usually through the TUI). Save the new
-     * message to the voicemail provider if it is the former case and perform a full sync in the
-     * latter case.
-     *
-     * @param message The sync message to extract data from.
-     */
-    private void processSync(PhoneAccountHandle phone, SyncMessage message) {
-        switch (message.getSyncTriggerEvent()) {
-            case OmtpConstants.NEW_MESSAGE:
-                if (!OmtpConstants.VOICE.equals(message.getContentType())) {
-                    VvmLog.i(TAG, "Non-voice message of type '" + message.getContentType()
-                        + "' received, ignoring");
-                    return;
-                }
-
-                Voicemail.Builder builder = Voicemail.createForInsertion(
-                        message.getTimestampMillis(), message.getSender())
-                        .setPhoneAccount(phone)
-                        .setSourceData(message.getId())
-                        .setDuration(message.getLength())
-                        .setSourcePackage(mContext.getPackageName());
-                Voicemail voicemail = builder.build();
-
-                VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
-                if (queryHelper.isVoicemailUnique(voicemail)) {
-                    Uri uri = VoicemailDatabaseUtil.insert(mContext, voicemail);
-                    voicemail = builder.setId(ContentUris.parseId(uri)).setUri(uri).build();
-                    SyncOneTask.start(mContext, phone, voicemail);
-                }
-                break;
-            case OmtpConstants.MAILBOX_UPDATE:
-                SyncTask.start(mContext, phone, OmtpVvmSyncService.SYNC_DOWNLOAD_ONLY);
-                break;
-            case OmtpConstants.GREETINGS_UPDATE:
-                // Not implemented in V1
-                break;
-            default:
-                VvmLog.e(TAG,
-                        "Unrecognized sync trigger event: " + message.getSyncTriggerEvent());
-                break;
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/sms/OmtpMessageSender.java b/java/com/android/voicemailomtp/sms/OmtpMessageSender.java
deleted file mode 100644
index 2323e4b..0000000
--- a/java/com/android/voicemailomtp/sms/OmtpMessageSender.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2015 Google Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sms;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SmsManager;
-import android.telephony.VisualVoicemailService;
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.TelephonyManagerStub;
-import com.android.voicemailomtp.VvmLog;
-import java.io.UnsupportedEncodingException;
-import java.util.Locale;
-
-/**
- * Send client originated OMTP messages to the OMTP server.
- * <p>
- * Uses {@link PendingIntent} instead of a call back to notify when the message is
- * sent. This is primarily to keep the implementation simple and reuse what the underlying
- * {@link SmsManager} interface provides.
- * <p>
- * Provides simple APIs to send different types of mobile originated OMTP SMS to the VVM server.
- */
-public abstract class OmtpMessageSender {
-    protected static final String TAG = "OmtpMessageSender";
-    protected final Context mContext;
-    protected final PhoneAccountHandle mPhoneAccountHandle;
-    protected final short mApplicationPort;
-    protected final String mDestinationNumber;
-
-
-    public OmtpMessageSender(Context context, PhoneAccountHandle phoneAccountHandle,
-            short applicationPort,
-            String destinationNumber) {
-        mContext = context;
-        mPhoneAccountHandle = phoneAccountHandle;
-        mApplicationPort = applicationPort;
-        mDestinationNumber = destinationNumber;
-    }
-
-    /**
-     * Sends a request to the VVM server to activate VVM for the current subscriber.
-     *
-     * @param sentIntent If not NULL this PendingIntent is broadcast when the message is
-     *            successfully sent, or failed.
-     */
-    public void requestVvmActivation(@Nullable PendingIntent sentIntent) {}
-
-    /**
-     * Sends a request to the VVM server to deactivate VVM for the current subscriber.
-     *
-     * @param sentIntent If not NULL this PendingIntent is broadcast when the message is
-     *            successfully sent, or failed.
-     */
-    public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {}
-
-    /**
-     * Send a request to the VVM server to get account status of the current subscriber.
-     *
-     * @param sentIntent If not NULL this PendingIntent is broadcast when the message is
-     *            successfully sent, or failed.
-     */
-    public void requestVvmStatus(@Nullable PendingIntent sentIntent) {}
-
-    protected void sendSms(String text, PendingIntent sentIntent) {
-        VisualVoicemailService
-                .sendVisualVoicemailSms(mContext, mPhoneAccountHandle, mDestinationNumber,
-                        mApplicationPort, text, sentIntent);
-    }
-
-    protected void appendField(StringBuilder sb, String field, Object value) {
-        sb.append(field).append(OmtpConstants.SMS_KEY_VALUE_SEPARATOR).append(value);
-    }
-}
diff --git a/java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java b/java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java
deleted file mode 100644
index aa83747..0000000
--- a/java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (C) 2015 Google Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sms;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SmsManager;
-import android.text.TextUtils;
-import com.android.voicemailomtp.OmtpConstants;
-
-/**
- * A implementation of the OmtpMessageSender using the standard OMTP sms protocol.
- */
-public class OmtpStandardMessageSender extends OmtpMessageSender {
-    private final String mClientType;
-    private final String mProtocolVersion;
-    private final String mClientPrefix;
-
-    /**
-     * Creates a new instance of OmtpStandardMessageSender.
-     *
-     * @param applicationPort If set to a value > 0 then a binary sms is sent to this port number.
-     *            Otherwise, a standard text SMS is sent.
-     * @param destinationNumber Destination number to be used.
-     * @param clientType The "ct" field to be set in the MO message. This is the value used by the
-     *            VVM server to identify the client. Certain VVM servers require a specific agreed
-     *            value for this field.
-     * @param protocolVersion OMTP protocol version.
-     * @param clientPrefix The client prefix requested to be used by the server in its MT messages.
-     */
-    public OmtpStandardMessageSender(Context context, PhoneAccountHandle phoneAccountHandle,
-            short applicationPort,
-            String destinationNumber, String clientType, String protocolVersion,
-            String clientPrefix) {
-        super(context, phoneAccountHandle, applicationPort, destinationNumber);
-        mClientType = clientType;
-        mProtocolVersion = protocolVersion;
-        mClientPrefix = clientPrefix;
-    }
-
-    // Activate message:
-    // V1.1: Activate:pv=<value>;ct=<value>
-    // V1.2: Activate:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
-    // V1.3: Activate:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
-    @Override
-    public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
-        StringBuilder sb = new StringBuilder().append(OmtpConstants.ACTIVATE_REQUEST);
-
-        appendProtocolVersionAndClientType(sb);
-        if (TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_2) ||
-                TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_3)) {
-            appendApplicationPort(sb);
-            appendClientPrefix(sb);
-        }
-
-        sendSms(sb.toString(), sentIntent);
-    }
-
-    // Deactivate message:
-    // V1.1: Deactivate:pv=<value>;ct=<string>
-    // V1.2: Deactivate:pv=<value>;ct=<string>
-    // V1.3: Deactivate:pv=<value>;ct=<string>
-    @Override
-    public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
-        StringBuilder sb = new StringBuilder().append(OmtpConstants.DEACTIVATE_REQUEST);
-        appendProtocolVersionAndClientType(sb);
-
-        sendSms(sb.toString(), sentIntent);
-    }
-
-    // Status message:
-    // V1.1: STATUS
-    // V1.2: STATUS
-    // V1.3: STATUS:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
-    @Override
-    public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
-        StringBuilder sb = new StringBuilder().append(OmtpConstants.STATUS_REQUEST);
-
-        if (TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_3)) {
-            appendProtocolVersionAndClientType(sb);
-            appendApplicationPort(sb);
-            appendClientPrefix(sb);
-        }
-
-        sendSms(sb.toString(), sentIntent);
-    }
-
-    private void appendProtocolVersionAndClientType(StringBuilder sb) {
-        sb.append(OmtpConstants.SMS_PREFIX_SEPARATOR);
-        appendField(sb, OmtpConstants.PROTOCOL_VERSION, mProtocolVersion);
-        sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
-        appendField(sb, OmtpConstants.CLIENT_TYPE, mClientType);
-    }
-
-    private void appendApplicationPort(StringBuilder sb) {
-        sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
-        appendField(sb, OmtpConstants.APPLICATION_PORT, mApplicationPort);
-    }
-
-    private void appendClientPrefix(StringBuilder sb) {
-        sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
-        sb.append(mClientPrefix);
-    }
-}
diff --git a/java/com/android/voicemailomtp/sms/StatusMessage.java b/java/com/android/voicemailomtp/sms/StatusMessage.java
deleted file mode 100644
index 3dfd497..0000000
--- a/java/com/android/voicemailomtp/sms/StatusMessage.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sms;
-
-import android.os.Bundle;
-import com.android.voicemailomtp.NeededForTesting;
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.VisualVoicemailPreferences;
-import com.android.voicemailomtp.VvmLog;
-
-/**
- * Structured data representation of OMTP STATUS message.
- *
- * The getters will return null if the field was not set in the message body or it could not be
- * parsed.
- */
-public class StatusMessage {
-    // NOTE: Following Status SMS fields are not yet parsed, as they do not seem
-    // to be useful for initial omtp source implementation.
-    // lang, g_len, vs_len, pw_len, pm, gm, vtc, vt
-
-    private final String mProvisioningStatus;
-    private final String mStatusReturnCode;
-    private final String mSubscriptionUrl;
-    private final String mServerAddress;
-    private final String mTuiAccessNumber;
-    private final String mClientSmsDestinationNumber;
-    private final String mImapPort;
-    private final String mImapUserName;
-    private final String mImapPassword;
-    private final String mSmtpPort;
-    private final String mSmtpUserName;
-    private final String mSmtpPassword;
-    private final String mTuiPasswordLength;
-
-    @Override
-    public String toString() {
-        return "StatusMessage [mProvisioningStatus=" + mProvisioningStatus
-                + ", mStatusReturnCode=" + mStatusReturnCode
-                + ", mSubscriptionUrl=" + mSubscriptionUrl
-                + ", mServerAddress=" + mServerAddress
-                + ", mTuiAccessNumber=" + mTuiAccessNumber
-                + ", mClientSmsDestinationNumber=" + mClientSmsDestinationNumber
-                + ", mImapPort=" + mImapPort
-                + ", mImapUserName=" + mImapUserName
-                + ", mImapPassword=" + VvmLog.pii(mImapPassword)
-                + ", mSmtpPort=" + mSmtpPort
-                + ", mSmtpUserName=" + mSmtpUserName
-                + ", mSmtpPassword=" + VvmLog.pii(mSmtpPassword)
-                + ", mTuiPasswordLength=" + mTuiPasswordLength + "]";
-    }
-
-    public StatusMessage(Bundle wrappedData) {
-        mProvisioningStatus = unquote(getString(wrappedData, OmtpConstants.PROVISIONING_STATUS));
-        mStatusReturnCode = getString(wrappedData, OmtpConstants.RETURN_CODE);
-        mSubscriptionUrl = getString(wrappedData, OmtpConstants.SUBSCRIPTION_URL);
-        mServerAddress = getString(wrappedData, OmtpConstants.SERVER_ADDRESS);
-        mTuiAccessNumber = getString(wrappedData, OmtpConstants.TUI_ACCESS_NUMBER);
-        mClientSmsDestinationNumber = getString(wrappedData,
-                OmtpConstants.CLIENT_SMS_DESTINATION_NUMBER);
-        mImapPort = getString(wrappedData, OmtpConstants.IMAP_PORT);
-        mImapUserName = getString(wrappedData, OmtpConstants.IMAP_USER_NAME);
-        mImapPassword = getString(wrappedData, OmtpConstants.IMAP_PASSWORD);
-        mSmtpPort = getString(wrappedData, OmtpConstants.SMTP_PORT);
-        mSmtpUserName = getString(wrappedData, OmtpConstants.SMTP_USER_NAME);
-        mSmtpPassword = getString(wrappedData, OmtpConstants.SMTP_PASSWORD);
-        mTuiPasswordLength = getString(wrappedData, OmtpConstants.TUI_PASSWORD_LENGTH);
-    }
-
-    private static String unquote(String string) {
-        if (string.length() < 2) {
-            return string;
-        }
-        if (string.startsWith("\"") && string.endsWith("\"")) {
-            return string.substring(1, string.length() - 1);
-        }
-        return string;
-    }
-
-    /**
-     * @return the subscriber's VVM provisioning status.
-     */
-    public String getProvisioningStatus() {
-        return mProvisioningStatus;
-    }
-
-    /**
-     * @return the return-code of the status SMS.
-     */
-    public String getReturnCode() {
-        return mStatusReturnCode;
-    }
-
-    /**
-     * @return the URL of the voicemail server. This is the URL to send the users to for subscribing
-     * to the visual voicemail service.
-     */
-    @NeededForTesting
-    public String getSubscriptionUrl() {
-        return mSubscriptionUrl;
-    }
-
-    /**
-     * @return the voicemail server address. Either server IP address or fully qualified domain
-     * name.
-     */
-    public String getServerAddress() {
-        return mServerAddress;
-    }
-
-    /**
-     * @return the Telephony User Interface number to call to access voicemails directly from the
-     * IVR.
-     */
-    @NeededForTesting
-    public String getTuiAccessNumber() {
-        return mTuiAccessNumber;
-    }
-
-    /**
-     * @return the number to which client originated SMSes should be sent to.
-     */
-    @NeededForTesting
-    public String getClientSmsDestinationNumber() {
-        return mClientSmsDestinationNumber;
-    }
-
-    /**
-     * @return the IMAP server port to talk to.
-     */
-    public String getImapPort() {
-        return mImapPort;
-    }
-
-    /**
-     * @return the IMAP user name to be used for authentication.
-     */
-    public String getImapUserName() {
-        return mImapUserName;
-    }
-
-    /**
-     * @return the IMAP password to be used for authentication.
-     */
-    public String getImapPassword() {
-        return mImapPassword;
-    }
-
-    /**
-     * @return the SMTP server port to talk to.
-     */
-    @NeededForTesting
-    public String getSmtpPort() {
-        return mSmtpPort;
-    }
-
-    /**
-     * @return the SMTP user name to be used for SMTP authentication.
-     */
-    @NeededForTesting
-    public String getSmtpUserName() {
-        return mSmtpUserName;
-    }
-
-    /**
-     * @return the SMTP password to be used for SMTP authentication.
-     */
-    @NeededForTesting
-    public String getSmtpPassword() {
-        return mSmtpPassword;
-    }
-
-    public String getTuiPasswordLength() {
-        return mTuiPasswordLength;
-    }
-
-    private static String getString(Bundle bundle, String key) {
-        String value = bundle.getString(key);
-        if (value == null) {
-            return "";
-        }
-        return value;
-    }
-
-    /**
-     * Saves a StatusMessage to the {@link VisualVoicemailPreferences}. Not all fields are saved.
-     */
-    public VisualVoicemailPreferences.Editor putStatus(VisualVoicemailPreferences.Editor editor) {
-        return editor
-                .putString(OmtpConstants.IMAP_PORT, getImapPort())
-                .putString(OmtpConstants.SERVER_ADDRESS, getServerAddress())
-                .putString(OmtpConstants.IMAP_USER_NAME, getImapUserName())
-                .putString(OmtpConstants.IMAP_PASSWORD, getImapPassword())
-                .putString(OmtpConstants.TUI_PASSWORD_LENGTH, getTuiPasswordLength());
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/sms/StatusSmsFetcher.java b/java/com/android/voicemailomtp/sms/StatusSmsFetcher.java
deleted file mode 100644
index 4e10c0e..0000000
--- a/java/com/android/voicemailomtp/sms/StatusSmsFetcher.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.sms;
-
-import android.annotation.TargetApi;
-import android.app.Activity;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.support.annotation.MainThread;
-import android.support.annotation.Nullable;
-import android.support.annotation.WorkerThread;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SmsManager;
-import android.telephony.VisualVoicemailSms;
-import com.android.voicemailomtp.Assert;
-import com.android.voicemailomtp.OmtpConstants;
-import com.android.voicemailomtp.OmtpService;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.protocol.VisualVoicemailProtocol;
-import java.io.Closeable;
-import java.io.IOException;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/** Intercepts a incoming STATUS SMS with a blocking call. */
-@SuppressWarnings("AndroidApiChecker") /* CompletableFuture is java8*/
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class StatusSmsFetcher extends BroadcastReceiver implements Closeable {
-
-    private static final String TAG = "VvmStatusSmsFetcher";
-
-    private static final long STATUS_SMS_TIMEOUT_MILLIS = 60_000;
-
-    private static final String ACTION_REQUEST_SENT_INTENT
-            = "com.android.voicemailomtp.sms.REQUEST_SENT";
-    private static final int ACTION_REQUEST_SENT_REQUEST_CODE = 0;
-
-    private CompletableFuture<Bundle> mFuture = new CompletableFuture<>();
-
-    private final Context mContext;
-    private final PhoneAccountHandle mPhoneAccountHandle;
-
-    public StatusSmsFetcher(Context context, PhoneAccountHandle phoneAccountHandle) {
-        mContext = context;
-        mPhoneAccountHandle = phoneAccountHandle;
-        IntentFilter filter = new IntentFilter(ACTION_REQUEST_SENT_INTENT);
-        filter.addAction(OmtpService.ACTION_SMS_RECEIVED);
-        context.registerReceiver(this, filter);
-    }
-
-    @Override
-    public void close() throws IOException {
-        mContext.unregisterReceiver(this);
-    }
-
-    @WorkerThread
-    @Nullable
-    public Bundle get() throws InterruptedException, ExecutionException, TimeoutException,
-            CancellationException {
-        Assert.isNotMainThread();
-        return mFuture.get(STATUS_SMS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
-    }
-
-    public PendingIntent getSentIntent() {
-        Intent intent = new Intent(ACTION_REQUEST_SENT_INTENT);
-        intent.setPackage(mContext.getPackageName());
-        // Because the receiver is registered dynamically, implicit intent must be used.
-        // There should only be a single status SMS request at a time.
-        return PendingIntent.getBroadcast(mContext, ACTION_REQUEST_SENT_REQUEST_CODE, intent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
-    }
-
-    @Override
-    @MainThread
-    public void onReceive(Context context, Intent intent) {
-        Assert.isMainThread();
-        if (ACTION_REQUEST_SENT_INTENT.equals(intent.getAction())) {
-            int resultCode = getResultCode();
-
-            if (resultCode == Activity.RESULT_OK) {
-                VvmLog.d(TAG, "Request SMS successfully sent");
-                return;
-            }
-
-            VvmLog.e(TAG, "Request SMS send failed: " + sentSmsResultToString(resultCode));
-            mFuture.cancel(true);
-            return;
-        }
-
-        VisualVoicemailSms sms = intent.getExtras().getParcelable(OmtpService.EXTRA_VOICEMAIL_SMS);
-
-        if (!mPhoneAccountHandle.equals(sms.getPhoneAccountHandle())) {
-            return;
-        }
-        String eventType = sms.getPrefix();
-
-        if (eventType.equals(OmtpConstants.STATUS_SMS_PREFIX)) {
-            mFuture.complete(sms.getFields());
-            return;
-        }
-
-        if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
-            return;
-        }
-
-        VvmLog.i(TAG, "VVM SMS with event " + eventType
-                + " received, attempting to translate to STATUS SMS");
-        OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(context,
-                mPhoneAccountHandle);
-        VisualVoicemailProtocol protocol = helper.getProtocol();
-        if (protocol == null) {
-            return;
-        }
-        Bundle translatedBundle = protocol.translateStatusSmsBundle(helper, eventType,
-                sms.getFields());
-
-        if (translatedBundle != null) {
-            VvmLog.i(TAG, "Translated to STATUS SMS");
-            mFuture.complete(translatedBundle);
-        }
-    }
-
-    private static String sentSmsResultToString(int resultCode) {
-        switch (resultCode) {
-            case Activity.RESULT_OK:
-                return "OK";
-            case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
-                return "RESULT_ERROR_GENERIC_FAILURE";
-            case SmsManager.RESULT_ERROR_NO_SERVICE:
-                return "RESULT_ERROR_GENERIC_FAILURE";
-            case SmsManager.RESULT_ERROR_NULL_PDU:
-                return "RESULT_ERROR_GENERIC_FAILURE";
-            case SmsManager.RESULT_ERROR_RADIO_OFF:
-                return "RESULT_ERROR_GENERIC_FAILURE";
-            default:
-                return "UNKNOWN CODE: " + resultCode;
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/sms/SyncMessage.java b/java/com/android/voicemailomtp/sms/SyncMessage.java
deleted file mode 100644
index 89cfc0f..0000000
--- a/java/com/android/voicemailomtp/sms/SyncMessage.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sms;
-
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import com.android.voicemailomtp.NeededForTesting;
-import com.android.voicemailomtp.OmtpConstants;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Locale;
-
-/**
- * Structured data representation of an OMTP SYNC message.
- *
- * Getters will return null if the field was not set in the message body or it could not be parsed.
- */
-public class SyncMessage {
-    // Sync event that triggered this message.
-    private final String mSyncTriggerEvent;
-    // Total number of new messages on the server.
-    private final int mNewMessageCount;
-    // UID of the new message.
-    private final String mMessageId;
-    // Length of the message.
-    private final int mMessageLength;
-    // Content type (voice, video, fax...) of the new message.
-    private final String mContentType;
-    // Sender of the new message.
-    private final String mSender;
-    // Timestamp (in millis) of the new message.
-    private final long mMsgTimeMillis;
-
-    @Override
-    public String toString() {
-        return "SyncMessage [mSyncTriggerEvent=" + mSyncTriggerEvent
-                + ", mNewMessageCount=" + mNewMessageCount
-                + ", mMessageId=" + mMessageId
-                + ", mMessageLength=" + mMessageLength
-                + ", mContentType=" + mContentType
-                + ", mSender=" + mSender
-                + ", mMsgTimeMillis=" + mMsgTimeMillis + "]";
-    }
-
-    public SyncMessage(Bundle wrappedData) {
-        mSyncTriggerEvent = getString(wrappedData, OmtpConstants.SYNC_TRIGGER_EVENT);
-        mMessageId = getString(wrappedData, OmtpConstants.MESSAGE_UID);
-        mMessageLength = getInt(wrappedData, OmtpConstants.MESSAGE_LENGTH);
-        mContentType = getString(wrappedData, OmtpConstants.CONTENT_TYPE);
-        mSender = getString(wrappedData, OmtpConstants.SENDER);
-        mNewMessageCount = getInt(wrappedData, OmtpConstants.NUM_MESSAGE_COUNT);
-        mMsgTimeMillis = parseTime(wrappedData.getString(OmtpConstants.TIME));
-    }
-
-    private static long parseTime(@Nullable String value) {
-        if (value == null) {
-            return 0L;
-        }
-        try {
-            return new SimpleDateFormat(
-                    OmtpConstants.DATE_TIME_FORMAT, Locale.US)
-                    .parse(value).getTime();
-        } catch (ParseException e) {
-            return 0L;
-        }
-    }
-    /**
-     * @return the event that triggered the sync message. This is a mandatory field and must always
-     * be set.
-     */
-    public String getSyncTriggerEvent() {
-        return mSyncTriggerEvent;
-    }
-
-    /**
-     * @return the number of new messages stored on the voicemail server.
-     */
-    @NeededForTesting
-    public int getNewMessageCount() {
-        return mNewMessageCount;
-    }
-
-    /**
-     * @return the message ID of the new message.
-     * <p>
-     * Expected to be set only for
-     * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE}
-     */
-    public String getId() {
-        return mMessageId;
-    }
-
-    /**
-     * @return the content type of the new message.
-     * <p>
-     * Expected to be set only for
-     * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE}
-     */
-    @NeededForTesting
-    public String getContentType() {
-        return mContentType;
-    }
-
-    /**
-     * @return the message length of the new message.
-     * <p>
-     * Expected to be set only for
-     * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE}
-     */
-    public int getLength() {
-        return mMessageLength;
-    }
-
-    /**
-     * @return the sender's phone number of the new message specified as MSISDN.
-     * <p>
-     * Expected to be set only for
-     * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE}
-     */
-    public String getSender() {
-        return mSender;
-    }
-
-    /**
-     * @return the timestamp as milliseconds for the new message.
-     * <p>
-     * Expected to be set only for
-     * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE}
-     */
-    public long getTimestampMillis() {
-        return mMsgTimeMillis;
-    }
-
-    private static int getInt(Bundle wrappedData, String key) {
-        String value = wrappedData.getString(key);
-        if (value == null) {
-            return 0;
-        }
-        try {
-            return Integer.parseInt(value);
-        } catch (NumberFormatException e) {
-            return 0;
-        }
-    }
-
-    private static String getString(Bundle wrappedData, String key) {
-        String value = wrappedData.getString(key);
-        if (value == null) {
-            return "";
-        }
-        return value;
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/sms/Vvm3MessageSender.java b/java/com/android/voicemailomtp/sms/Vvm3MessageSender.java
deleted file mode 100644
index 02e4659..0000000
--- a/java/com/android/voicemailomtp/sms/Vvm3MessageSender.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sms;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.support.annotation.Nullable;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SmsManager;
-
-public class Vvm3MessageSender extends OmtpMessageSender {
-
-    /**
-     * Creates a new instance of Vvm3MessageSender.
-     *
-     * @param applicationPort If set to a value > 0 then a binary sms is sent to this port number.
-     * Otherwise, a standard text SMS is sent.
-     */
-    public Vvm3MessageSender(Context context, PhoneAccountHandle phoneAccountHandle,
-            short applicationPort, String destinationNumber) {
-        super(context, phoneAccountHandle, applicationPort, destinationNumber);
-    }
-
-    @Override
-    public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
-        // Activation not supported for VVM3, send a status request instead.
-        requestVvmStatus(sentIntent);
-    }
-
-    @Override
-    public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
-        // Deactivation not supported for VVM3, do nothing
-    }
-
-
-    @Override
-    public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
-        // Status message:
-        // STATUS
-        StringBuilder sb = new StringBuilder().append("STATUS");
-        sendSms(sb.toString(), sentIntent);
-    }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/commons/io/IOUtils.java b/java/com/android/voicemailomtp/src/org/apache/commons/io/IOUtils.java
deleted file mode 100644
index b414507..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/commons/io/IOUtils.java
+++ /dev/null
@@ -1,1202 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.commons.io;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.CharArrayWriter;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.io.Reader;
-import java.io.StringWriter;
-import java.io.Writer;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.List;
-
-/**
- * General IO stream manipulation utilities.
- * <p>
- * This class provides static utility methods for input/output operations.
- * <ul>
- * <li>closeQuietly - these methods close a stream ignoring nulls and exceptions
- * <li>toXxx/read - these methods read data from a stream
- * <li>write - these methods write data to a stream
- * <li>copy - these methods copy all the data from one stream to another
- * <li>contentEquals - these methods compare the content of two streams
- * </ul>
- * <p>
- * The byte-to-char methods and char-to-byte methods involve a conversion step.
- * Two methods are provided in each case, one that uses the platform default
- * encoding and the other which allows you to specify an encoding. You are
- * encouraged to always specify an encoding because relying on the platform
- * default can lead to unexpected results, for example when moving from
- * development to production.
- * <p>
- * All the methods in this class that read a stream are buffered internally.
- * This means that there is no cause to use a <code>BufferedInputStream</code>
- * or <code>BufferedReader</code>. The default buffer size of 4K has been shown
- * to be efficient in tests.
- * <p>
- * Wherever possible, the methods in this class do <em>not</em> flush or close
- * the stream. This is to avoid making non-portable assumptions about the
- * streams' origin and further use. Thus the caller is still responsible for
- * closing streams after use.
- * <p>
- * Origin of code: Excalibur.
- *
- * @author Peter Donald
- * @author Jeff Turner
- * @author Matthew Hawthorne
- * @author Stephen Colebourne
- * @author Gareth Davis
- * @author Ian Springer
- * @author Niall Pemberton
- * @author Sandy McArthur
- * @version $Id: IOUtils.java 481854 2006-12-03 18:30:07Z scolebourne $
- */
-public class IOUtils {
-    // NOTE: This class is focussed on InputStream, OutputStream, Reader and
-    // Writer. Each method should take at least one of these as a parameter,
-    // or return one of them.
-
-    /**
-     * The Unix directory separator character.
-     */
-    public static final char DIR_SEPARATOR_UNIX = '/';
-    /**
-     * The Windows directory separator character.
-     */
-    public static final char DIR_SEPARATOR_WINDOWS = '\\';
-    /**
-     * The system directory separator character.
-     */
-    public static final char DIR_SEPARATOR = File.separatorChar;
-    /**
-     * The Unix line separator string.
-     */
-    public static final String LINE_SEPARATOR_UNIX = "\n";
-    /**
-     * The Windows line separator string.
-     */
-    public static final String LINE_SEPARATOR_WINDOWS = "\r\n";
-    /**
-     * The system line separator string.
-     */
-    public static final String LINE_SEPARATOR;
-    static {
-        // avoid security issues
-        StringWriter buf = new StringWriter(4);
-        PrintWriter out = new PrintWriter(buf);
-        out.println();
-        LINE_SEPARATOR = buf.toString();
-    }
-
-    /**
-     * The default buffer size to use.
-     */
-    private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
-
-    /**
-     * Instances should NOT be constructed in standard programming.
-     */
-    public IOUtils() {
-        super();
-    }
-
-    //-----------------------------------------------------------------------
-    /**
-     * Unconditionally close an <code>Reader</code>.
-     * <p>
-     * Equivalent to {@link Reader#close()}, except any exceptions will be ignored.
-     * This is typically used in finally blocks.
-     *
-     * @param input  the Reader to close, may be null or already closed
-     */
-    public static void closeQuietly(Reader input) {
-        try {
-            if (input != null) {
-                input.close();
-            }
-        } catch (IOException ioe) {
-            // ignore
-        }
-    }
-
-    /**
-     * Unconditionally close a <code>Writer</code>.
-     * <p>
-     * Equivalent to {@link Writer#close()}, except any exceptions will be ignored.
-     * This is typically used in finally blocks.
-     *
-     * @param output  the Writer to close, may be null or already closed
-     */
-    public static void closeQuietly(Writer output) {
-        try {
-            if (output != null) {
-                output.close();
-            }
-        } catch (IOException ioe) {
-            // ignore
-        }
-    }
-
-    /**
-     * Unconditionally close an <code>InputStream</code>.
-     * <p>
-     * Equivalent to {@link InputStream#close()}, except any exceptions will be ignored.
-     * This is typically used in finally blocks.
-     *
-     * @param input  the InputStream to close, may be null or already closed
-     */
-    public static void closeQuietly(InputStream input) {
-        try {
-            if (input != null) {
-                input.close();
-            }
-        } catch (IOException ioe) {
-            // ignore
-        }
-    }
-
-    /**
-     * Unconditionally close an <code>OutputStream</code>.
-     * <p>
-     * Equivalent to {@link OutputStream#close()}, except any exceptions will be ignored.
-     * This is typically used in finally blocks.
-     *
-     * @param output  the OutputStream to close, may be null or already closed
-     */
-    public static void closeQuietly(OutputStream output) {
-        try {
-            if (output != null) {
-                output.close();
-            }
-        } catch (IOException ioe) {
-            // ignore
-        }
-    }
-
-    // read toByteArray
-    //-----------------------------------------------------------------------
-    /**
-     * Get the contents of an <code>InputStream</code> as a <code>byte[]</code>.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedInputStream</code>.
-     *
-     * @param input  the <code>InputStream</code> to read from
-     * @return the requested byte array
-     * @throws NullPointerException if the input is null
-     * @throws IOException if an I/O error occurs
-     */
-    public static byte[] toByteArray(InputStream input) throws IOException {
-        ByteArrayOutputStream output = new ByteArrayOutputStream();
-        copy(input, output);
-        return output.toByteArray();
-    }
-
-    /**
-     * Get the contents of a <code>Reader</code> as a <code>byte[]</code>
-     * using the default character encoding of the platform.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedReader</code>.
-     *
-     * @param input  the <code>Reader</code> to read from
-     * @return the requested byte array
-     * @throws NullPointerException if the input is null
-     * @throws IOException if an I/O error occurs
-     */
-    public static byte[] toByteArray(Reader input) throws IOException {
-        ByteArrayOutputStream output = new ByteArrayOutputStream();
-        copy(input, output);
-        return output.toByteArray();
-    }
-
-    /**
-     * Get the contents of a <code>Reader</code> as a <code>byte[]</code>
-     * using the specified character encoding.
-     * <p>
-     * Character encoding names can be found at
-     * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedReader</code>.
-     *
-     * @param input  the <code>Reader</code> to read from
-     * @param encoding  the encoding to use, null means platform default
-     * @return the requested byte array
-     * @throws NullPointerException if the input is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static byte[] toByteArray(Reader input, String encoding)
-            throws IOException {
-        ByteArrayOutputStream output = new ByteArrayOutputStream();
-        copy(input, output, encoding);
-        return output.toByteArray();
-    }
-
-    /**
-     * Get the contents of a <code>String</code> as a <code>byte[]</code>
-     * using the default character encoding of the platform.
-     * <p>
-     * This is the same as {@link String#getBytes()}.
-     *
-     * @param input  the <code>String</code> to convert
-     * @return the requested byte array
-     * @throws NullPointerException if the input is null
-     * @throws IOException if an I/O error occurs (never occurs)
-     * @deprecated Use {@link String#getBytes()}
-     */
-    @Deprecated
-    public static byte[] toByteArray(String input) throws IOException {
-        return input.getBytes();
-    }
-
-    // read char[]
-    //-----------------------------------------------------------------------
-    /**
-     * Get the contents of an <code>InputStream</code> as a character array
-     * using the default character encoding of the platform.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedInputStream</code>.
-     *
-     * @param is  the <code>InputStream</code> to read from
-     * @return the requested character array
-     * @throws NullPointerException if the input is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static char[] toCharArray(InputStream is) throws IOException {
-        CharArrayWriter output = new CharArrayWriter();
-        copy(is, output);
-        return output.toCharArray();
-    }
-
-    /**
-     * Get the contents of an <code>InputStream</code> as a character array
-     * using the specified character encoding.
-     * <p>
-     * Character encoding names can be found at
-     * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedInputStream</code>.
-     *
-     * @param is  the <code>InputStream</code> to read from
-     * @param encoding  the encoding to use, null means platform default
-     * @return the requested character array
-     * @throws NullPointerException if the input is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static char[] toCharArray(InputStream is, String encoding)
-            throws IOException {
-        CharArrayWriter output = new CharArrayWriter();
-        copy(is, output, encoding);
-        return output.toCharArray();
-    }
-
-    /**
-     * Get the contents of a <code>Reader</code> as a character array.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedReader</code>.
-     *
-     * @param input  the <code>Reader</code> to read from
-     * @return the requested character array
-     * @throws NullPointerException if the input is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static char[] toCharArray(Reader input) throws IOException {
-        CharArrayWriter sw = new CharArrayWriter();
-        copy(input, sw);
-        return sw.toCharArray();
-    }
-
-    // read toString
-    //-----------------------------------------------------------------------
-    /**
-     * Get the contents of an <code>InputStream</code> as a String
-     * using the default character encoding of the platform.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedInputStream</code>.
-     *
-     * @param input  the <code>InputStream</code> to read from
-     * @return the requested String
-     * @throws NullPointerException if the input is null
-     * @throws IOException if an I/O error occurs
-     */
-    public static String toString(InputStream input) throws IOException {
-        StringWriter sw = new StringWriter();
-        copy(input, sw);
-        return sw.toString();
-    }
-
-    /**
-     * Get the contents of an <code>InputStream</code> as a String
-     * using the specified character encoding.
-     * <p>
-     * Character encoding names can be found at
-     * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedInputStream</code>.
-     *
-     * @param input  the <code>InputStream</code> to read from
-     * @param encoding  the encoding to use, null means platform default
-     * @return the requested String
-     * @throws NullPointerException if the input is null
-     * @throws IOException if an I/O error occurs
-     */
-    public static String toString(InputStream input, String encoding)
-            throws IOException {
-        StringWriter sw = new StringWriter();
-        copy(input, sw, encoding);
-        return sw.toString();
-    }
-
-    /**
-     * Get the contents of a <code>Reader</code> as a String.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedReader</code>.
-     *
-     * @param input  the <code>Reader</code> to read from
-     * @return the requested String
-     * @throws NullPointerException if the input is null
-     * @throws IOException if an I/O error occurs
-     */
-    public static String toString(Reader input) throws IOException {
-        StringWriter sw = new StringWriter();
-        copy(input, sw);
-        return sw.toString();
-    }
-
-    /**
-     * Get the contents of a <code>byte[]</code> as a String
-     * using the default character encoding of the platform.
-     *
-     * @param input the byte array to read from
-     * @return the requested String
-     * @throws NullPointerException if the input is null
-     * @throws IOException if an I/O error occurs (never occurs)
-     * @deprecated Use {@link String#String(byte[])}
-     */
-    @Deprecated
-    public static String toString(byte[] input) throws IOException {
-        return new String(input);
-    }
-
-    /**
-     * Get the contents of a <code>byte[]</code> as a String
-     * using the specified character encoding.
-     * <p>
-     * Character encoding names can be found at
-     * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
-     *
-     * @param input the byte array to read from
-     * @param encoding  the encoding to use, null means platform default
-     * @return the requested String
-     * @throws NullPointerException if the input is null
-     * @throws IOException if an I/O error occurs (never occurs)
-     * @deprecated Use {@link String#String(byte[],String)}
-     */
-    @Deprecated
-    public static String toString(byte[] input, String encoding)
-            throws IOException {
-        if (encoding == null) {
-            return new String(input);
-        } else {
-            return new String(input, encoding);
-        }
-    }
-
-    // readLines
-    //-----------------------------------------------------------------------
-    /**
-     * Get the contents of an <code>InputStream</code> as a list of Strings,
-     * one entry per line, using the default character encoding of the platform.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedInputStream</code>.
-     *
-     * @param input  the <code>InputStream</code> to read from, not null
-     * @return the list of Strings, never null
-     * @throws NullPointerException if the input is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static List<String> readLines(InputStream input) throws IOException {
-        InputStreamReader reader = new InputStreamReader(input);
-        return readLines(reader);
-    }
-
-    /**
-     * Get the contents of an <code>InputStream</code> as a list of Strings,
-     * one entry per line, using the specified character encoding.
-     * <p>
-     * Character encoding names can be found at
-     * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedInputStream</code>.
-     *
-     * @param input  the <code>InputStream</code> to read from, not null
-     * @param encoding  the encoding to use, null means platform default
-     * @return the list of Strings, never null
-     * @throws NullPointerException if the input is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static List<String> readLines(InputStream input, String encoding) throws IOException {
-        if (encoding == null) {
-            return readLines(input);
-        } else {
-            InputStreamReader reader = new InputStreamReader(input, encoding);
-            return readLines(reader);
-        }
-    }
-
-    /**
-     * Get the contents of a <code>Reader</code> as a list of Strings,
-     * one entry per line.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedReader</code>.
-     *
-     * @param input  the <code>Reader</code> to read from, not null
-     * @return the list of Strings, never null
-     * @throws NullPointerException if the input is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static List<String> readLines(Reader input) throws IOException {
-        BufferedReader reader = new BufferedReader(input);
-        List<String> list = new ArrayList<String>();
-        String line = reader.readLine();
-        while (line != null) {
-            list.add(line);
-            line = reader.readLine();
-        }
-        return list;
-    }
-
-    //-----------------------------------------------------------------------
-    /**
-     * Convert the specified string to an input stream, encoded as bytes
-     * using the default character encoding of the platform.
-     *
-     * @param input the string to convert
-     * @return an input stream
-     * @since Commons IO 1.1
-     */
-    public static InputStream toInputStream(String input) {
-        byte[] bytes = input.getBytes();
-        return new ByteArrayInputStream(bytes);
-    }
-
-    /**
-     * Convert the specified string to an input stream, encoded as bytes
-     * using the specified character encoding.
-     * <p>
-     * Character encoding names can be found at
-     * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
-     *
-     * @param input the string to convert
-     * @param encoding the encoding to use, null means platform default
-     * @throws IOException if the encoding is invalid
-     * @return an input stream
-     * @since Commons IO 1.1
-     */
-    public static InputStream toInputStream(String input, String encoding) throws IOException {
-        byte[] bytes = encoding != null ? input.getBytes(encoding) : input.getBytes();
-        return new ByteArrayInputStream(bytes);
-    }
-
-    // write byte[]
-    //-----------------------------------------------------------------------
-    /**
-     * Writes bytes from a <code>byte[]</code> to an <code>OutputStream</code>.
-     *
-     * @param data  the byte array to write, do not modify during output,
-     * null ignored
-     * @param output  the <code>OutputStream</code> to write to
-     * @throws NullPointerException if output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void write(byte[] data, OutputStream output)
-            throws IOException {
-        if (data != null) {
-            output.write(data);
-        }
-    }
-
-    /**
-     * Writes bytes from a <code>byte[]</code> to chars on a <code>Writer</code>
-     * using the default character encoding of the platform.
-     * <p>
-     * This method uses {@link String#String(byte[])}.
-     *
-     * @param data  the byte array to write, do not modify during output,
-     * null ignored
-     * @param output  the <code>Writer</code> to write to
-     * @throws NullPointerException if output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void write(byte[] data, Writer output) throws IOException {
-        if (data != null) {
-            output.write(new String(data));
-        }
-    }
-
-    /**
-     * Writes bytes from a <code>byte[]</code> to chars on a <code>Writer</code>
-     * using the specified character encoding.
-     * <p>
-     * Character encoding names can be found at
-     * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
-     * <p>
-     * This method uses {@link String#String(byte[], String)}.
-     *
-     * @param data  the byte array to write, do not modify during output,
-     * null ignored
-     * @param output  the <code>Writer</code> to write to
-     * @param encoding  the encoding to use, null means platform default
-     * @throws NullPointerException if output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void write(byte[] data, Writer output, String encoding)
-            throws IOException {
-        if (data != null) {
-            if (encoding == null) {
-                write(data, output);
-            } else {
-                output.write(new String(data, encoding));
-            }
-        }
-    }
-
-    // write char[]
-    //-----------------------------------------------------------------------
-    /**
-     * Writes chars from a <code>char[]</code> to a <code>Writer</code>
-     * using the default character encoding of the platform.
-     *
-     * @param data  the char array to write, do not modify during output,
-     * null ignored
-     * @param output  the <code>Writer</code> to write to
-     * @throws NullPointerException if output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void write(char[] data, Writer output) throws IOException {
-        if (data != null) {
-            output.write(data);
-        }
-    }
-
-    /**
-     * Writes chars from a <code>char[]</code> to bytes on an
-     * <code>OutputStream</code>.
-     * <p>
-     * This method uses {@link String#String(char[])} and
-     * {@link String#getBytes()}.
-     *
-     * @param data  the char array to write, do not modify during output,
-     * null ignored
-     * @param output  the <code>OutputStream</code> to write to
-     * @throws NullPointerException if output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void write(char[] data, OutputStream output)
-            throws IOException {
-        if (data != null) {
-            output.write(new String(data).getBytes());
-        }
-    }
-
-    /**
-     * Writes chars from a <code>char[]</code> to bytes on an
-     * <code>OutputStream</code> using the specified character encoding.
-     * <p>
-     * Character encoding names can be found at
-     * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
-     * <p>
-     * This method uses {@link String#String(char[])} and
-     * {@link String#getBytes(String)}.
-     *
-     * @param data  the char array to write, do not modify during output,
-     * null ignored
-     * @param output  the <code>OutputStream</code> to write to
-     * @param encoding  the encoding to use, null means platform default
-     * @throws NullPointerException if output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void write(char[] data, OutputStream output, String encoding)
-            throws IOException {
-        if (data != null) {
-            if (encoding == null) {
-                write(data, output);
-            } else {
-                output.write(new String(data).getBytes(encoding));
-            }
-        }
-    }
-
-    // write String
-    //-----------------------------------------------------------------------
-    /**
-     * Writes chars from a <code>String</code> to a <code>Writer</code>.
-     *
-     * @param data  the <code>String</code> to write, null ignored
-     * @param output  the <code>Writer</code> to write to
-     * @throws NullPointerException if output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void write(String data, Writer output) throws IOException {
-        if (data != null) {
-            output.write(data);
-        }
-    }
-
-    /**
-     * Writes chars from a <code>String</code> to bytes on an
-     * <code>OutputStream</code> using the default character encoding of the
-     * platform.
-     * <p>
-     * This method uses {@link String#getBytes()}.
-     *
-     * @param data  the <code>String</code> to write, null ignored
-     * @param output  the <code>OutputStream</code> to write to
-     * @throws NullPointerException if output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void write(String data, OutputStream output)
-            throws IOException {
-        if (data != null) {
-            output.write(data.getBytes());
-        }
-    }
-
-    /**
-     * Writes chars from a <code>String</code> to bytes on an
-     * <code>OutputStream</code> using the specified character encoding.
-     * <p>
-     * Character encoding names can be found at
-     * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
-     * <p>
-     * This method uses {@link String#getBytes(String)}.
-     *
-     * @param data  the <code>String</code> to write, null ignored
-     * @param output  the <code>OutputStream</code> to write to
-     * @param encoding  the encoding to use, null means platform default
-     * @throws NullPointerException if output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void write(String data, OutputStream output, String encoding)
-            throws IOException {
-        if (data != null) {
-            if (encoding == null) {
-                write(data, output);
-            } else {
-                output.write(data.getBytes(encoding));
-            }
-        }
-    }
-
-    // write StringBuffer
-    //-----------------------------------------------------------------------
-    /**
-     * Writes chars from a <code>StringBuffer</code> to a <code>Writer</code>.
-     *
-     * @param data  the <code>StringBuffer</code> to write, null ignored
-     * @param output  the <code>Writer</code> to write to
-     * @throws NullPointerException if output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void write(StringBuffer data, Writer output)
-            throws IOException {
-        if (data != null) {
-            output.write(data.toString());
-        }
-    }
-
-    /**
-     * Writes chars from a <code>StringBuffer</code> to bytes on an
-     * <code>OutputStream</code> using the default character encoding of the
-     * platform.
-     * <p>
-     * This method uses {@link String#getBytes()}.
-     *
-     * @param data  the <code>StringBuffer</code> to write, null ignored
-     * @param output  the <code>OutputStream</code> to write to
-     * @throws NullPointerException if output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void write(StringBuffer data, OutputStream output)
-            throws IOException {
-        if (data != null) {
-            output.write(data.toString().getBytes());
-        }
-    }
-
-    /**
-     * Writes chars from a <code>StringBuffer</code> to bytes on an
-     * <code>OutputStream</code> using the specified character encoding.
-     * <p>
-     * Character encoding names can be found at
-     * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
-     * <p>
-     * This method uses {@link String#getBytes(String)}.
-     *
-     * @param data  the <code>StringBuffer</code> to write, null ignored
-     * @param output  the <code>OutputStream</code> to write to
-     * @param encoding  the encoding to use, null means platform default
-     * @throws NullPointerException if output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void write(StringBuffer data, OutputStream output,
-            String encoding) throws IOException {
-        if (data != null) {
-            if (encoding == null) {
-                write(data, output);
-            } else {
-                output.write(data.toString().getBytes(encoding));
-            }
-        }
-    }
-
-    // writeLines
-    //-----------------------------------------------------------------------
-    /**
-     * Writes the <code>toString()</code> value of each item in a collection to
-     * an <code>OutputStream</code> line by line, using the default character
-     * encoding of the platform and the specified line ending.
-     *
-     * @param lines  the lines to write, null entries produce blank lines
-     * @param lineEnding  the line separator to use, null is system default
-     * @param output  the <code>OutputStream</code> to write to, not null, not closed
-     * @throws NullPointerException if the output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void writeLines(Collection<Object> lines, String lineEnding,
-            OutputStream output) throws IOException {
-        if (lines == null) {
-            return;
-        }
-        if (lineEnding == null) {
-            lineEnding = LINE_SEPARATOR;
-        }
-        for (Iterator<Object> it = lines.iterator(); it.hasNext(); ) {
-            Object line = it.next();
-            if (line != null) {
-                output.write(line.toString().getBytes());
-            }
-            output.write(lineEnding.getBytes());
-        }
-    }
-
-    /**
-     * Writes the <code>toString()</code> value of each item in a collection to
-     * an <code>OutputStream</code> line by line, using the specified character
-     * encoding and the specified line ending.
-     * <p>
-     * Character encoding names can be found at
-     * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
-     *
-     * @param lines  the lines to write, null entries produce blank lines
-     * @param lineEnding  the line separator to use, null is system default
-     * @param output  the <code>OutputStream</code> to write to, not null, not closed
-     * @param encoding  the encoding to use, null means platform default
-     * @throws NullPointerException if the output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void writeLines(Collection<Object> lines, String lineEnding,
-            OutputStream output, String encoding) throws IOException {
-        if (encoding == null) {
-            writeLines(lines, lineEnding, output);
-        } else {
-            if (lines == null) {
-                return;
-            }
-            if (lineEnding == null) {
-                lineEnding = LINE_SEPARATOR;
-            }
-            for (Iterator<Object> it = lines.iterator(); it.hasNext(); ) {
-                Object line = it.next();
-                if (line != null) {
-                    output.write(line.toString().getBytes(encoding));
-                }
-                output.write(lineEnding.getBytes(encoding));
-            }
-        }
-    }
-
-    /**
-     * Writes the <code>toString()</code> value of each item in a collection to
-     * a <code>Writer</code> line by line, using the specified line ending.
-     *
-     * @param lines  the lines to write, null entries produce blank lines
-     * @param lineEnding  the line separator to use, null is system default
-     * @param writer  the <code>Writer</code> to write to, not null, not closed
-     * @throws NullPointerException if the input is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void writeLines(Collection<Object> lines, String lineEnding,
-            Writer writer) throws IOException {
-        if (lines == null) {
-            return;
-        }
-        if (lineEnding == null) {
-            lineEnding = LINE_SEPARATOR;
-        }
-        for (Iterator<Object> it = lines.iterator(); it.hasNext(); ) {
-            Object line = it.next();
-            if (line != null) {
-                writer.write(line.toString());
-            }
-            writer.write(lineEnding);
-        }
-    }
-
-    // copy from InputStream
-    //-----------------------------------------------------------------------
-    /**
-     * Copy bytes from an <code>InputStream</code> to an
-     * <code>OutputStream</code>.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedInputStream</code>.
-     * <p>
-     * Large streams (over 2GB) will return a bytes copied value of
-     * <code>-1</code> after the copy has completed since the correct
-     * number of bytes cannot be returned as an int. For large streams
-     * use the <code>copyLarge(InputStream, OutputStream)</code> method.
-     *
-     * @param input  the <code>InputStream</code> to read from
-     * @param output  the <code>OutputStream</code> to write to
-     * @return the number of bytes copied
-     * @throws NullPointerException if the input or output is null
-     * @throws IOException if an I/O error occurs
-     * @throws ArithmeticException if the byte count is too large
-     * @since Commons IO 1.1
-     */
-    public static int copy(InputStream input, OutputStream output) throws IOException {
-        long count = copyLarge(input, output);
-        if (count > Integer.MAX_VALUE) {
-            return -1;
-        }
-        return (int) count;
-    }
-
-    /**
-     * Copy bytes from a large (over 2GB) <code>InputStream</code> to an
-     * <code>OutputStream</code>.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedInputStream</code>.
-     *
-     * @param input  the <code>InputStream</code> to read from
-     * @param output  the <code>OutputStream</code> to write to
-     * @return the number of bytes copied
-     * @throws NullPointerException if the input or output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.3
-     */
-    public static long copyLarge(InputStream input, OutputStream output)
-            throws IOException {
-        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
-        long count = 0;
-        int n = 0;
-        while (-1 != (n = input.read(buffer))) {
-            output.write(buffer, 0, n);
-            count += n;
-        }
-        return count;
-    }
-
-    /**
-     * Copy bytes from an <code>InputStream</code> to chars on a
-     * <code>Writer</code> using the default character encoding of the platform.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedInputStream</code>.
-     * <p>
-     * This method uses {@link InputStreamReader}.
-     *
-     * @param input  the <code>InputStream</code> to read from
-     * @param output  the <code>Writer</code> to write to
-     * @throws NullPointerException if the input or output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void copy(InputStream input, Writer output)
-            throws IOException {
-        InputStreamReader in = new InputStreamReader(input);
-        copy(in, output);
-    }
-
-    /**
-     * Copy bytes from an <code>InputStream</code> to chars on a
-     * <code>Writer</code> using the specified character encoding.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedInputStream</code>.
-     * <p>
-     * Character encoding names can be found at
-     * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
-     * <p>
-     * This method uses {@link InputStreamReader}.
-     *
-     * @param input  the <code>InputStream</code> to read from
-     * @param output  the <code>Writer</code> to write to
-     * @param encoding  the encoding to use, null means platform default
-     * @throws NullPointerException if the input or output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void copy(InputStream input, Writer output, String encoding)
-            throws IOException {
-        if (encoding == null) {
-            copy(input, output);
-        } else {
-            InputStreamReader in = new InputStreamReader(input, encoding);
-            copy(in, output);
-        }
-    }
-
-    // copy from Reader
-    //-----------------------------------------------------------------------
-    /**
-     * Copy chars from a <code>Reader</code> to a <code>Writer</code>.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedReader</code>.
-     * <p>
-     * Large streams (over 2GB) will return a chars copied value of
-     * <code>-1</code> after the copy has completed since the correct
-     * number of chars cannot be returned as an int. For large streams
-     * use the <code>copyLarge(Reader, Writer)</code> method.
-     *
-     * @param input  the <code>Reader</code> to read from
-     * @param output  the <code>Writer</code> to write to
-     * @return the number of characters copied
-     * @throws NullPointerException if the input or output is null
-     * @throws IOException if an I/O error occurs
-     * @throws ArithmeticException if the character count is too large
-     * @since Commons IO 1.1
-     */
-    public static int copy(Reader input, Writer output) throws IOException {
-        long count = copyLarge(input, output);
-        if (count > Integer.MAX_VALUE) {
-            return -1;
-        }
-        return (int) count;
-    }
-
-    /**
-     * Copy chars from a large (over 2GB) <code>Reader</code> to a <code>Writer</code>.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedReader</code>.
-     *
-     * @param input  the <code>Reader</code> to read from
-     * @param output  the <code>Writer</code> to write to
-     * @return the number of characters copied
-     * @throws NullPointerException if the input or output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.3
-     */
-    public static long copyLarge(Reader input, Writer output) throws IOException {
-        char[] buffer = new char[DEFAULT_BUFFER_SIZE];
-        long count = 0;
-        int n = 0;
-        while (-1 != (n = input.read(buffer))) {
-            output.write(buffer, 0, n);
-            count += n;
-        }
-        return count;
-    }
-
-    /**
-     * Copy chars from a <code>Reader</code> to bytes on an
-     * <code>OutputStream</code> using the default character encoding of the
-     * platform, and calling flush.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedReader</code>.
-     * <p>
-     * Due to the implementation of OutputStreamWriter, this method performs a
-     * flush.
-     * <p>
-     * This method uses {@link OutputStreamWriter}.
-     *
-     * @param input  the <code>Reader</code> to read from
-     * @param output  the <code>OutputStream</code> to write to
-     * @throws NullPointerException if the input or output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void copy(Reader input, OutputStream output)
-            throws IOException {
-        OutputStreamWriter out = new OutputStreamWriter(output);
-        copy(input, out);
-        // XXX Unless anyone is planning on rewriting OutputStreamWriter, we
-        // have to flush here.
-        out.flush();
-    }
-
-    /**
-     * Copy chars from a <code>Reader</code> to bytes on an
-     * <code>OutputStream</code> using the specified character encoding, and
-     * calling flush.
-     * <p>
-     * This method buffers the input internally, so there is no need to use a
-     * <code>BufferedReader</code>.
-     * <p>
-     * Character encoding names can be found at
-     * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
-     * <p>
-     * Due to the implementation of OutputStreamWriter, this method performs a
-     * flush.
-     * <p>
-     * This method uses {@link OutputStreamWriter}.
-     *
-     * @param input  the <code>Reader</code> to read from
-     * @param output  the <code>OutputStream</code> to write to
-     * @param encoding  the encoding to use, null means platform default
-     * @throws NullPointerException if the input or output is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static void copy(Reader input, OutputStream output, String encoding)
-            throws IOException {
-        if (encoding == null) {
-            copy(input, output);
-        } else {
-            OutputStreamWriter out = new OutputStreamWriter(output, encoding);
-            copy(input, out);
-            // XXX Unless anyone is planning on rewriting OutputStreamWriter,
-            // we have to flush here.
-            out.flush();
-        }
-    }
-
-    // content equals
-    //-----------------------------------------------------------------------
-    /**
-     * Compare the contents of two Streams to determine if they are equal or
-     * not.
-     * <p>
-     * This method buffers the input internally using
-     * <code>BufferedInputStream</code> if they are not already buffered.
-     *
-     * @param input1  the first stream
-     * @param input2  the second stream
-     * @return true if the content of the streams are equal or they both don't
-     * exist, false otherwise
-     * @throws NullPointerException if either input is null
-     * @throws IOException if an I/O error occurs
-     */
-    public static boolean contentEquals(InputStream input1, InputStream input2)
-            throws IOException {
-        if (!(input1 instanceof BufferedInputStream)) {
-            input1 = new BufferedInputStream(input1);
-        }
-        if (!(input2 instanceof BufferedInputStream)) {
-            input2 = new BufferedInputStream(input2);
-        }
-
-        int ch = input1.read();
-        while (-1 != ch) {
-            int ch2 = input2.read();
-            if (ch != ch2) {
-                return false;
-            }
-            ch = input1.read();
-        }
-
-        int ch2 = input2.read();
-        return (ch2 == -1);
-    }
-
-    /**
-     * Compare the contents of two Readers to determine if they are equal or
-     * not.
-     * <p>
-     * This method buffers the input internally using
-     * <code>BufferedReader</code> if they are not already buffered.
-     *
-     * @param input1  the first reader
-     * @param input2  the second reader
-     * @return true if the content of the readers are equal or they both don't
-     * exist, false otherwise
-     * @throws NullPointerException if either input is null
-     * @throws IOException if an I/O error occurs
-     * @since Commons IO 1.1
-     */
-    public static boolean contentEquals(Reader input1, Reader input2)
-            throws IOException {
-        if (!(input1 instanceof BufferedReader)) {
-            input1 = new BufferedReader(input1);
-        }
-        if (!(input2 instanceof BufferedReader)) {
-            input2 = new BufferedReader(input2);
-        }
-
-        int ch = input1.read();
-        while (-1 != ch) {
-            int ch2 = input2.read();
-            if (ch != ch2) {
-                return false;
-            }
-            ch = input1.read();
-        }
-
-        int ch2 = input2.read();
-        return (ch2 == -1);
-    }
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/BodyDescriptor.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/BodyDescriptor.java
deleted file mode 100644
index 867c43d..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/BodyDescriptor.java
+++ /dev/null
@@ -1,392 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Encapsulates the values of the MIME-specific header fields
- * (which starts with <code>Content-</code>).
- *
- *
- * @version $Id: BodyDescriptor.java,v 1.4 2005/02/11 10:08:37 ntherning Exp $
- */
-public class BodyDescriptor {
-    private static Log log = LogFactory.getLog(BodyDescriptor.class);
-
-    private String mimeType = "text/plain";
-    private String boundary = null;
-    private String charset = "us-ascii";
-    private String transferEncoding = "7bit";
-    private Map<String, String> parameters = new HashMap<String, String>();
-    private boolean contentTypeSet = false;
-    private boolean contentTransferEncSet = false;
-
-    /**
-     * Creates a new root <code>BodyDescriptor</code> instance.
-     */
-    public BodyDescriptor() {
-        this(null);
-    }
-
-    /**
-     * Creates a new <code>BodyDescriptor</code> instance.
-     *
-     * @param parent the descriptor of the parent or <code>null</code> if this
-     *        is the root descriptor.
-     */
-    public BodyDescriptor(BodyDescriptor parent) {
-        if (parent != null && parent.isMimeType("multipart/digest")) {
-            mimeType = "message/rfc822";
-        } else {
-            mimeType = "text/plain";
-        }
-    }
-
-    /**
-     * Should be called for each <code>Content-</code> header field of
-     * a MIME message or part.
-     *
-     * @param name the field name.
-     * @param value the field value.
-     */
-    public void addField(String name, String value) {
-
-        name = name.trim().toLowerCase();
-
-        if (name.equals("content-transfer-encoding") && !contentTransferEncSet) {
-            contentTransferEncSet = true;
-
-            value = value.trim().toLowerCase();
-            if (value.length() > 0) {
-                transferEncoding = value;
-            }
-
-        } else if (name.equals("content-type") && !contentTypeSet) {
-            contentTypeSet = true;
-
-            value = value.trim();
-
-            /*
-             * Unfold Content-Type value
-             */
-            StringBuffer sb = new StringBuffer();
-            for (int i = 0; i < value.length(); i++) {
-                char c = value.charAt(i);
-                if (c == '\r' || c == '\n') {
-                    continue;
-                }
-                sb.append(c);
-            }
-
-            Map<String, String> params = getHeaderParams(sb.toString());
-
-            String main = params.get("");
-            if (main != null) {
-                main = main.toLowerCase().trim();
-                int index = main.indexOf('/');
-                boolean valid = false;
-                if (index != -1) {
-                    String type = main.substring(0, index).trim();
-                    String subtype = main.substring(index + 1).trim();
-                    if (type.length() > 0 && subtype.length() > 0) {
-                        main = type + "/" + subtype;
-                        valid = true;
-                    }
-                }
-
-                if (!valid) {
-                    main = null;
-                }
-            }
-            String b = params.get("boundary");
-
-            if (main != null
-                    && ((main.startsWith("multipart/") && b != null)
-                            || !main.startsWith("multipart/"))) {
-
-                mimeType = main;
-            }
-
-            if (isMultipart()) {
-                boundary = b;
-            }
-
-            String c = params.get("charset");
-            if (c != null) {
-                c = c.trim();
-                if (c.length() > 0) {
-                    charset = c.toLowerCase();
-                }
-            }
-
-            /*
-             * Add all other parameters to parameters.
-             */
-            parameters.putAll(params);
-            parameters.remove("");
-            parameters.remove("boundary");
-            parameters.remove("charset");
-        }
-    }
-
-    private Map<String, String> getHeaderParams(String headerValue) {
-        Map<String, String> result = new HashMap<String, String>();
-
-        // split main value and parameters
-        String main;
-        String rest;
-        if (headerValue.indexOf(";") == -1) {
-            main = headerValue;
-            rest = null;
-        } else {
-            main = headerValue.substring(0, headerValue.indexOf(";"));
-            rest = headerValue.substring(main.length() + 1);
-        }
-
-        result.put("", main);
-        if (rest != null) {
-            char[] chars = rest.toCharArray();
-            StringBuffer paramName = new StringBuffer();
-            StringBuffer paramValue = new StringBuffer();
-
-            final byte READY_FOR_NAME = 0;
-            final byte IN_NAME = 1;
-            final byte READY_FOR_VALUE = 2;
-            final byte IN_VALUE = 3;
-            final byte IN_QUOTED_VALUE = 4;
-            final byte VALUE_DONE = 5;
-            final byte ERROR = 99;
-
-            byte state = READY_FOR_NAME;
-            boolean escaped = false;
-            for (int i = 0; i < chars.length; i++) {
-                char c = chars[i];
-
-                switch (state) {
-                    case ERROR:
-                        if (c == ';')
-                            state = READY_FOR_NAME;
-                        break;
-
-                    case READY_FOR_NAME:
-                        if (c == '=') {
-                            log.error("Expected header param name, got '='");
-                            state = ERROR;
-                            break;
-                        }
-
-                        paramName = new StringBuffer();
-                        paramValue = new StringBuffer();
-
-                        state = IN_NAME;
-                        // $FALL-THROUGH$
-
-                    case IN_NAME:
-                        if (c == '=') {
-                            if (paramName.length() == 0)
-                                state = ERROR;
-                            else
-                                state = READY_FOR_VALUE;
-                            break;
-                        }
-
-                        // not '='... just add to name
-                        paramName.append(c);
-                        break;
-
-                    case READY_FOR_VALUE:
-                        boolean fallThrough = false;
-                        switch (c) {
-                            case ' ':
-                            case '\t':
-                                break;  // ignore spaces, especially before '"'
-
-                            case '"':
-                                state = IN_QUOTED_VALUE;
-                                break;
-
-                            default:
-                                state = IN_VALUE;
-                                fallThrough = true;
-                                break;
-                        }
-                        if (!fallThrough)
-                            break;
-
-                        // $FALL-THROUGH$
-
-                    case IN_VALUE:
-                        fallThrough = false;
-                        switch (c) {
-                            case ';':
-                            case ' ':
-                            case '\t':
-                                result.put(
-                                   paramName.toString().trim().toLowerCase(),
-                                   paramValue.toString().trim());
-                                state = VALUE_DONE;
-                                fallThrough = true;
-                                break;
-                            default:
-                                paramValue.append(c);
-                                break;
-                        }
-                        if (!fallThrough)
-                            break;
-
-                        // $FALL-THROUGH$
-
-                    case VALUE_DONE:
-                        switch (c) {
-                            case ';':
-                                state = READY_FOR_NAME;
-                                break;
-
-                            case ' ':
-                            case '\t':
-                                break;
-
-                            default:
-                                state = ERROR;
-                                break;
-                        }
-                        break;
-
-                    case IN_QUOTED_VALUE:
-                        switch (c) {
-                            case '"':
-                                if (!escaped) {
-                                    // don't trim quoted strings; the spaces could be intentional.
-                                    result.put(
-                                            paramName.toString().trim().toLowerCase(),
-                                            paramValue.toString());
-                                    state = VALUE_DONE;
-                                } else {
-                                    escaped = false;
-                                    paramValue.append(c);
-                                }
-                                break;
-
-                            case '\\':
-                                if (escaped) {
-                                    paramValue.append('\\');
-                                }
-                                escaped = !escaped;
-                                break;
-
-                            default:
-                                if (escaped) {
-                                    paramValue.append('\\');
-                                }
-                                escaped = false;
-                                paramValue.append(c);
-                                break;
-                        }
-                        break;
-
-                }
-            }
-
-            // done looping.  check if anything is left over.
-            if (state == IN_VALUE) {
-                result.put(
-                        paramName.toString().trim().toLowerCase(),
-                        paramValue.toString().trim());
-            }
-        }
-
-        return result;
-    }
-
-
-    public boolean isMimeType(String mimeType) {
-        return this.mimeType.equals(mimeType.toLowerCase());
-    }
-
-    /**
-     * Return true if the BodyDescriptor belongs to a message
-     */
-    public boolean isMessage() {
-        return mimeType.equals("message/rfc822");
-    }
-
-    /**
-     * Return true if the BodyDescripotro belongs to a multipart
-     */
-    public boolean isMultipart() {
-        return mimeType.startsWith("multipart/");
-    }
-
-    /**
-     * Return the MimeType
-     */
-    public String getMimeType() {
-        return mimeType;
-    }
-
-    /**
-     * Return the boundary
-     */
-    public String getBoundary() {
-        return boundary;
-    }
-
-    /**
-     * Return the charset
-     */
-    public String getCharset() {
-        return charset;
-    }
-
-    /**
-     * Return all parameters for the BodyDescriptor
-     */
-    public Map<String, String> getParameters() {
-        return parameters;
-    }
-
-    /**
-     * Return the TransferEncoding
-     */
-    public String getTransferEncoding() {
-        return transferEncoding;
-    }
-
-    /**
-     * Return true if it's base64 encoded
-     */
-    public boolean isBase64Encoded() {
-        return "base64".equals(transferEncoding);
-    }
-
-    /**
-     * Return true if it's quoted-printable
-     */
-    public boolean isQuotedPrintableEncoded() {
-        return "quoted-printable".equals(transferEncoding);
-    }
-
-    @Override
-    public String toString() {
-        return mimeType;
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/CloseShieldInputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/CloseShieldInputStream.java
deleted file mode 100644
index d9f3b07..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/CloseShieldInputStream.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j;
-
-import java.io.InputStream;
-import java.io.IOException;
-
-/**
- * InputStream that shields its underlying input stream from
- * being closed.
- *
- *
- * @version $Id: CloseShieldInputStream.java,v 1.2 2004/10/02 12:41:10 ntherning Exp $
- */
-public class CloseShieldInputStream extends InputStream {
-
-    /**
-     * Underlying InputStream
-     */
-    private InputStream is;
-
-    public CloseShieldInputStream(InputStream is) {
-        this.is = is;
-    }
-
-    public InputStream getUnderlyingStream() {
-        return is;
-    }
-
-    /**
-     * @see java.io.InputStream#read()
-     */
-    public int read() throws IOException {
-        checkIfClosed();
-        return is.read();
-    }
-
-    /**
-     * @see java.io.InputStream#available()
-     */
-    public int available() throws IOException {
-        checkIfClosed();
-        return is.available();
-    }
-
-
-    /**
-     * Set the underlying InputStream to null
-     */
-    public void close() throws IOException {
-        is = null;
-    }
-
-    /**
-     * @see java.io.FilterInputStream#reset()
-     */
-    public synchronized void reset() throws IOException {
-        checkIfClosed();
-        is.reset();
-    }
-
-    /**
-     * @see java.io.FilterInputStream#markSupported()
-     */
-    public boolean markSupported() {
-        if (is == null)
-            return false;
-        return is.markSupported();
-    }
-
-    /**
-     * @see java.io.FilterInputStream#mark(int)
-     */
-    public synchronized void mark(int readlimit) {
-        if (is != null)
-            is.mark(readlimit);
-    }
-
-    /**
-     * @see java.io.FilterInputStream#skip(long)
-     */
-    public long skip(long n) throws IOException {
-        checkIfClosed();
-        return is.skip(n);
-    }
-
-    /**
-     * @see java.io.FilterInputStream#read(byte[])
-     */
-    public int read(byte b[]) throws IOException {
-        checkIfClosed();
-        return is.read(b);
-    }
-
-    /**
-     * @see java.io.FilterInputStream#read(byte[], int, int)
-     */
-    public int read(byte b[], int off, int len) throws IOException {
-        checkIfClosed();
-        return is.read(b, off, len);
-    }
-
-    /**
-     * Check if the underlying InputStream is null. If so throw an Exception
-     *
-     * @throws IOException if the underlying InputStream is null
-     */
-    private void checkIfClosed() throws IOException {
-        if (is == null)
-            throw new IOException("Stream is closed");
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/ContentHandler.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/ContentHandler.java
deleted file mode 100644
index b437e73..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/ContentHandler.java
+++ /dev/null
@@ -1,177 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * <p>
- * Receives notifications of the content of a plain RFC822 or MIME message.
- * Implement this interface and register an instance of that implementation
- * with a <code>MimeStreamParser</code> instance using its
- * {@link org.apache.james.mime4j.MimeStreamParser#setContentHandler(ContentHandler)}
- * method. The parser uses the <code>ContentHandler</code> instance to report
- * basic message-related events like the start and end of the body of a
- * part in a multipart MIME entity.
- * </p>
- * <p>
- * Events will be generated in the order the corresponding elements occur in
- * the message stream parsed by the parser. E.g.:
- * <pre>
- *      startMessage()
- *          startHeader()
- *              field(...)
- *              field(...)
- *              ...
- *          endHeader()
- *          startMultipart()
- *              preamble(...)
- *              startBodyPart()
- *                  startHeader()
- *                      field(...)
- *                      field(...)
- *                      ...
- *                  endHeader()
- *                  body()
- *              endBodyPart()
- *              startBodyPart()
- *                  startHeader()
- *                      field(...)
- *                      field(...)
- *                      ...
- *                  endHeader()
- *                  body()
- *              endBodyPart()
- *              epilogue(...)
- *          endMultipart()
- *      endMessage()
- * </pre>
- * The above shows an example of a MIME message consisting of a multipart
- * body containing two body parts.
- * </p>
- * <p>
- * See MIME RFCs 2045-2049 for more information on the structure of MIME
- * messages and RFC 822 and 2822 for the general structure of Internet mail
- * messages.
- * </p>
- *
- *
- * @version $Id: ContentHandler.java,v 1.3 2004/10/02 12:41:10 ntherning Exp $
- */
-public interface ContentHandler {
-    /**
-     * Called when a new message starts (a top level message or an embedded
-     * rfc822 message).
-     */
-    void startMessage();
-
-    /**
-     * Called when a message ends.
-     */
-    void endMessage();
-
-    /**
-     * Called when a new body part starts inside a
-     * <code>multipart/*</code> entity.
-     */
-    void startBodyPart();
-
-    /**
-     * Called when a body part ends.
-     */
-    void endBodyPart();
-
-    /**
-     * Called when a header (of a message or body part) is about to be parsed.
-     */
-    void startHeader();
-
-    /**
-     * Called for each field of a header.
-     *
-     * @param fieldData the raw contents of the field
-     *        (<code>Field-Name: field value</code>). The value will not be
-     *        unfolded.
-     */
-    void field(String fieldData);
-
-    /**
-     * Called when there are no more header fields in a message or body part.
-     */
-    void endHeader();
-
-    /**
-     * Called for the preamble (whatever comes before the first body part)
-     * of a <code>multipart/*</code> entity.
-     *
-     * @param is used to get the contents of the preamble.
-     * @throws IOException should be thrown on I/O errors.
-     */
-    void preamble(InputStream is) throws IOException;
-
-    /**
-     * Called for the epilogue (whatever comes after the final body part)
-     * of a <code>multipart/*</code> entity.
-     *
-     * @param is used to get the contents of the epilogue.
-     * @throws IOException should be thrown on I/O errors.
-     */
-    void epilogue(InputStream is) throws IOException;
-
-    /**
-     * Called when the body of a multipart entity is about to be parsed.
-     *
-     * @param bd encapsulates the values (either read from the
-     *        message stream or, if not present, determined implictly
-     *        as described in the
-     *        MIME rfc:s) of the <code>Content-Type</code> and
-     *        <code>Content-Transfer-Encoding</code> header fields.
-     */
-    void startMultipart(BodyDescriptor bd);
-
-    /**
-     * Called when the body of an entity has been parsed.
-     */
-    void endMultipart();
-
-    /**
-     * Called when the body of a discrete (non-multipart) entity is about to
-     * be parsed.
-     *
-     * @param bd see {@link #startMultipart(BodyDescriptor)}
-     * @param is the contents of the body. NOTE: this is the raw body contents
-     *           - it will not be decoded if encoded. The <code>bd</code>
-     *           parameter should be used to determine how the stream data
-     *           should be decoded.
-     * @throws IOException should be thrown on I/O errors.
-     */
-    void body(BodyDescriptor bd, InputStream is) throws IOException;
-
-    /**
-     * Called when a new entity (message or body part) starts and the
-     * parser is in <code>raw</code> mode.
-     *
-     * @param is the raw contents of the entity.
-     * @throws IOException should be thrown on I/O errors.
-     * @see MimeStreamParser#setRaw(boolean)
-     */
-    void raw(InputStream is) throws IOException;
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/EOLConvertingInputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/EOLConvertingInputStream.java
deleted file mode 100644
index d6ef706..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/EOLConvertingInputStream.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PushbackInputStream;
-
-/**
- * InputStream which converts <code>\r</code>
- * bytes not followed by <code>\n</code> and <code>\n</code> not
- * preceded by <code>\r</code> to <code>\r\n</code>.
- *
- *
- * @version $Id: EOLConvertingInputStream.java,v 1.4 2004/11/29 13:15:42 ntherning Exp $
- */
-public class EOLConvertingInputStream extends InputStream {
-    /** Converts single '\r' to '\r\n' */
-    public static final int CONVERT_CR   = 1;
-    /** Converts single '\n' to '\r\n' */
-    public static final int CONVERT_LF   = 2;
-    /** Converts single '\r' and '\n' to '\r\n' */
-    public static final int CONVERT_BOTH = 3;
-
-    private PushbackInputStream in = null;
-    private int previous = 0;
-    private int flags = CONVERT_BOTH;
-    private int size = 0;
-    private int pos = 0;
-    private int nextTenPctPos;
-    private int tenPctSize;
-    private Callback callback;
-
-    public interface Callback {
-        public void report(int bytesRead);
-    }
-
-    /**
-     * Creates a new <code>EOLConvertingInputStream</code>
-     * instance converting bytes in the given <code>InputStream</code>.
-     * The flag <code>CONVERT_BOTH</code> is the default.
-     *
-     * @param in the <code>InputStream</code> to read from.
-     */
-    public EOLConvertingInputStream(InputStream _in) {
-        super();
-        in = new PushbackInputStream(_in, 2);
-    }
-
-    /**
-     * Creates a new <code>EOLConvertingInputStream</code>
-     * instance converting bytes in the given <code>InputStream</code>.
-     *
-     * @param _in the <code>InputStream</code> to read from.
-     * @param _size the size of the input stream (need not be exact)
-     * @param _callback a callback reporting when each 10% of stream's size is reached
-     */
-    public EOLConvertingInputStream(InputStream _in, int _size, Callback _callback) {
-        this(_in);
-        size = _size;
-        tenPctSize = size / 10;
-        nextTenPctPos = tenPctSize;
-        callback = _callback;
-    }
-
-    /**
-     * Closes the underlying stream.
-     *
-     * @throws IOException on I/O errors.
-     */
-    public void close() throws IOException {
-        in.close();
-    }
-
-    private int readByte() throws IOException {
-        int b = in.read();
-        if (b != -1) {
-            if (callback != null && pos++ == nextTenPctPos) {
-                nextTenPctPos += tenPctSize;
-                if (callback != null) {
-                    callback.report(pos);
-                }
-            }
-        }
-        return b;
-    }
-
-    private void unreadByte(int c) throws IOException {
-        in.unread(c);
-        pos--;
-    }
-
-    /**
-     * @see java.io.InputStream#read()
-     */
-    public int read() throws IOException {
-        int b = readByte();
-
-        if (b == -1) {
-            pos = size;
-            return -1;
-        }
-
-        if ((flags & CONVERT_CR) != 0 && b == '\r') {
-            int c = readByte();
-            if (c != -1) {
-                unreadByte(c);
-            }
-            if (c != '\n') {
-                unreadByte('\n');
-            }
-        } else if ((flags & CONVERT_LF) != 0 && b == '\n' && previous != '\r') {
-            b = '\r';
-            unreadByte('\n');
-        }
-
-        previous = b;
-
-        return b;
-    }
-
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/Log.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/Log.java
deleted file mode 100644
index 5eeead5..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/Log.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.james.mime4j;
-
-/**
- * Empty stub for the apache logging library.
- */
-public class Log {
-    private static final String LOG_TAG = "Email Log";
-
-    public Log(Class mClazz) {
-    }
-
-    public boolean isDebugEnabled() {
-        return false;
-    }
-
-    public boolean isErrorEnabled() {
-        return true;
-    }
-
-    public boolean isFatalEnabled() {
-        return true;
-    }
-
-    public boolean isInfoEnabled() {
-        return false;
-    }
-
-    public boolean isTraceEnabled() {
-        return false;
-    }
-
-    public boolean isWarnEnabled() {
-        return true;
-    }
-
-    public void trace(Object message) {
-        if (!isTraceEnabled()) return;
-        android.util.Log.v(LOG_TAG, toString(message, null));
-    }
-
-    public void trace(Object message, Throwable t) {
-        if (!isTraceEnabled()) return;
-        android.util.Log.v(LOG_TAG, toString(message, t));
-    }
-
-    public void debug(Object message) {
-        if (!isDebugEnabled()) return;
-        android.util.Log.d(LOG_TAG, toString(message, null));
-    }
-
-    public void debug(Object message, Throwable t) {
-        if (!isDebugEnabled()) return;
-        android.util.Log.d(LOG_TAG, toString(message, t));
-    }
-
-    public void info(Object message) {
-        if (!isInfoEnabled()) return;
-        android.util.Log.i(LOG_TAG, toString(message, null));
-    }
-
-    public void info(Object message, Throwable t) {
-        if (!isInfoEnabled()) return;
-        android.util.Log.i(LOG_TAG, toString(message, t));
-    }
-
-    public void warn(Object message) {
-        android.util.Log.w(LOG_TAG, toString(message, null));
-    }
-
-    public void warn(Object message, Throwable t) {
-        android.util.Log.w(LOG_TAG, toString(message, t));
-    }
-
-    public void error(Object message) {
-        android.util.Log.e(LOG_TAG, toString(message, null));
-    }
-
-    public void error(Object message, Throwable t) {
-        android.util.Log.e(LOG_TAG, toString(message, t));
-    }
-
-    public void fatal(Object message) {
-        android.util.Log.e(LOG_TAG, toString(message, null));
-    }
-
-    public void fatal(Object message, Throwable t) {
-        android.util.Log.e(LOG_TAG, toString(message, t));
-    }
-
-    private static String toString(Object o, Throwable t) {
-        String m = (o == null) ? "(null)" : o.toString();
-        if (t == null) {
-            return m;
-        } else {
-            return m + " " + t.getMessage();
-        }
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/LogFactory.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/LogFactory.java
deleted file mode 100644
index ed6e3de..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/LogFactory.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.james.mime4j;
-
-/**
- * Empty stub for the apache logging library.
- */
-public final class LogFactory {
-    private LogFactory() {
-    }
-
-    public static Log getLog(Class clazz) {
-        return new Log(clazz);
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeBoundaryInputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeBoundaryInputStream.java
deleted file mode 100644
index c6d6f24..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeBoundaryInputStream.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PushbackInputStream;
-
-/**
- * Stream that constrains itself to a single MIME body part.
- * After the stream ends (i.e. read() returns -1) {@link #hasMoreParts()}
- * can be used to determine if a final boundary has been seen or not.
- * If {@link #parentEOF()} is <code>true</code> an unexpected end of stream
- * has been detected in the parent stream.
- *
- *
- *
- * @version $Id: MimeBoundaryInputStream.java,v 1.2 2004/11/29 13:15:42 ntherning Exp $
- */
-public class MimeBoundaryInputStream extends InputStream {
-
-    private PushbackInputStream s = null;
-    private byte[] boundary = null;
-    private boolean first = true;
-    private boolean eof = false;
-    private boolean parenteof = false;
-    private boolean moreParts = true;
-
-    /**
-     * Creates a new MimeBoundaryInputStream.
-     * @param s The underlying stream.
-     * @param boundary Boundary string (not including leading hyphens).
-     */
-    public MimeBoundaryInputStream(InputStream s, String boundary)
-            throws IOException {
-
-        this.s = new PushbackInputStream(s, boundary.length() + 4);
-
-        boundary = "--" + boundary;
-        this.boundary = new byte[boundary.length()];
-        for (int i = 0; i < this.boundary.length; i++) {
-            this.boundary[i] = (byte) boundary.charAt(i);
-        }
-
-        /*
-         * By reading one byte we will update moreParts to be as expected
-         * before any bytes have been read.
-         */
-        int b = read();
-        if (b != -1) {
-            this.s.unread(b);
-        }
-    }
-
-    /**
-     * Closes the underlying stream.
-     *
-     * @throws IOException on I/O errors.
-     */
-    public void close() throws IOException {
-        s.close();
-    }
-
-    /**
-     * Determines if the underlying stream has more parts (this stream has
-     * not seen an end boundary).
-     *
-     * @return <code>true</code> if there are more parts in the underlying
-     *         stream, <code>false</code> otherwise.
-     */
-    public boolean hasMoreParts() {
-        return moreParts;
-    }
-
-    /**
-     * Determines if the parent stream has reached EOF
-     *
-     * @return <code>true</code>  if EOF has been reached for the parent stream,
-     *         <code>false</code> otherwise.
-     */
-    public boolean parentEOF() {
-        return parenteof;
-    }
-
-    /**
-     * Consumes all unread bytes of this stream. After a call to this method
-     * this stream will have reached EOF.
-     *
-     * @throws IOException on I/O errors.
-     */
-    public void consume() throws IOException {
-        while (read() != -1) {
-        }
-    }
-
-    /**
-     * @see java.io.InputStream#read()
-     */
-    public int read() throws IOException {
-        if (eof) {
-            return -1;
-        }
-
-        if (first) {
-            first = false;
-            if (matchBoundary()) {
-                return -1;
-            }
-        }
-
-        int b1 = s.read();
-        int b2 = s.read();
-
-        if (b1 == '\r' && b2 == '\n') {
-            if (matchBoundary()) {
-                return -1;
-            }
-        }
-
-        if (b2 != -1) {
-            s.unread(b2);
-        }
-
-        parenteof = b1 == -1;
-        eof = parenteof;
-
-        return b1;
-    }
-
-    private boolean matchBoundary() throws IOException {
-
-        for (int i = 0; i < boundary.length; i++) {
-            int b = s.read();
-            if (b != boundary[i]) {
-                if (b != -1) {
-                    s.unread(b);
-                }
-                for (int j = i - 1; j >= 0; j--) {
-                    s.unread(boundary[j]);
-                }
-                return false;
-            }
-        }
-
-        /*
-         * We have a match. Is it an end boundary?
-         */
-        int prev = s.read();
-        int curr = s.read();
-        moreParts = !(prev == '-' && curr == '-');
-        do {
-            if (curr == '\n' && prev == '\r') {
-                break;
-            }
-            prev = curr;
-        } while ((curr = s.read()) != -1);
-
-        if (curr == -1) {
-            moreParts = false;
-            parenteof = true;
-        }
-
-        eof = true;
-
-        return true;
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeStreamParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeStreamParser.java
deleted file mode 100644
index a8aad5a..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeStreamParser.java
+++ /dev/null
@@ -1,324 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j;
-
-import org.apache.james.mime4j.decoder.Base64InputStream;
-import org.apache.james.mime4j.decoder.QuotedPrintableInputStream;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.BitSet;
-import java.util.LinkedList;
-
-/**
- * <p>
- * Parses MIME (or RFC822) message streams of bytes or characters and reports
- * parsing events to a <code>ContentHandler</code> instance.
- * </p>
- * <p>
- * Typical usage:<br/>
- * <pre>
- *      ContentHandler handler = new MyHandler();
- *      MimeStreamParser parser = new MimeStreamParser();
- *      parser.setContentHandler(handler);
- *      parser.parse(new BufferedInputStream(new FileInputStream("mime.msg")));
- * </pre>
- * <strong>NOTE:</strong> All lines must end with CRLF
- * (<code>\r\n</code>). If you are unsure of the line endings in your stream
- * you should wrap it in a {@link org.apache.james.mime4j.EOLConvertingInputStream} instance.
- *
- *
- * @version $Id: MimeStreamParser.java,v 1.8 2005/02/11 10:12:02 ntherning Exp $
- */
-public class MimeStreamParser {
-    private static final Log log = LogFactory.getLog(MimeStreamParser.class);
-
-    private static BitSet fieldChars = null;
-
-    private RootInputStream rootStream = null;
-    private LinkedList<BodyDescriptor> bodyDescriptors = new LinkedList<BodyDescriptor>();
-    private ContentHandler handler = null;
-    private boolean raw = false;
-    private boolean prematureEof = false;
-
-    static {
-        fieldChars = new BitSet();
-        for (int i = 0x21; i <= 0x39; i++) {
-            fieldChars.set(i);
-        }
-        for (int i = 0x3b; i <= 0x7e; i++) {
-            fieldChars.set(i);
-        }
-    }
-
-    /**
-     * Creates a new <code>MimeStreamParser</code> instance.
-     */
-    public MimeStreamParser() {
-    }
-
-    /**
-     * Parses a stream of bytes containing a MIME message.
-     *
-     * @param is the stream to parse.
-     * @throws IOException on I/O errors.
-     */
-    public void parse(InputStream is) throws IOException {
-        rootStream = new RootInputStream(is);
-        parseMessage(rootStream);
-    }
-
-    /**
-     * Determines if this parser is currently in raw mode.
-     *
-     * @return <code>true</code> if in raw mode, <code>false</code>
-     *         otherwise.
-     * @see #setRaw(boolean)
-     */
-    public boolean isRaw() {
-        return raw;
-    }
-
-    /**
-     * Enables or disables raw mode. In raw mode all future entities
-     * (messages or body parts) in the stream will be reported to the
-     * {@link ContentHandler#raw(InputStream)} handler method only.
-     * The stream will contain the entire unparsed entity contents
-     * including header fields and whatever is in the body.
-     *
-     * @param raw <code>true</code> enables raw mode, <code>false</code>
-     *        disables it.
-     */
-    public void setRaw(boolean raw) {
-        this.raw = raw;
-    }
-
-    /**
-     * Finishes the parsing and stops reading lines.
-     * NOTE: No more lines will be parsed but the parser
-     * will still call
-     * {@link ContentHandler#endMultipart()},
-     * {@link ContentHandler#endBodyPart()},
-     * {@link ContentHandler#endMessage()}, etc to match previous calls
-     * to
-     * {@link ContentHandler#startMultipart(BodyDescriptor)},
-     * {@link ContentHandler#startBodyPart()},
-     * {@link ContentHandler#startMessage()}, etc.
-     */
-    public void stop() {
-        rootStream.truncate();
-    }
-
-    /**
-     * Parses an entity which consists of a header followed by a body containing
-     * arbitrary data, body parts or an embedded message.
-     *
-     * @param is the stream to parse.
-     * @throws IOException on I/O errors.
-     */
-    private void parseEntity(InputStream is) throws IOException {
-        BodyDescriptor bd = parseHeader(is);
-
-        if (bd.isMultipart()) {
-            bodyDescriptors.addFirst(bd);
-
-            handler.startMultipart(bd);
-
-            MimeBoundaryInputStream tempIs =
-                new MimeBoundaryInputStream(is, bd.getBoundary());
-            handler.preamble(new CloseShieldInputStream(tempIs));
-            tempIs.consume();
-
-            while (tempIs.hasMoreParts()) {
-                tempIs = new MimeBoundaryInputStream(is, bd.getBoundary());
-                parseBodyPart(tempIs);
-                tempIs.consume();
-                if (tempIs.parentEOF()) {
-                    prematureEof = true;
-//                    if (log.isWarnEnabled()) {
-//                        log.warn("Line " + rootStream.getLineNumber()
-//                                + ": Body part ended prematurely. "
-//                                + "Higher level boundary detected or "
-//                                + "EOF reached.");
-//                    }
-                    break;
-                }
-            }
-
-            handler.epilogue(new CloseShieldInputStream(is));
-
-            handler.endMultipart();
-
-            bodyDescriptors.removeFirst();
-
-        } else if (bd.isMessage()) {
-            if (bd.isBase64Encoded()) {
-                log.warn("base64 encoded message/rfc822 detected");
-                is = new EOLConvertingInputStream(
-                        new Base64InputStream(is));
-            } else if (bd.isQuotedPrintableEncoded()) {
-                log.warn("quoted-printable encoded message/rfc822 detected");
-                is = new EOLConvertingInputStream(
-                        new QuotedPrintableInputStream(is));
-            }
-            bodyDescriptors.addFirst(bd);
-            parseMessage(is);
-            bodyDescriptors.removeFirst();
-        } else {
-            handler.body(bd, new CloseShieldInputStream(is));
-        }
-
-        /*
-         * Make sure the stream has been consumed.
-         */
-        while (is.read() != -1) {
-        }
-    }
-
-    private void parseMessage(InputStream is) throws IOException {
-        if (raw) {
-            handler.raw(new CloseShieldInputStream(is));
-        } else {
-            handler.startMessage();
-            parseEntity(is);
-            handler.endMessage();
-        }
-    }
-
-    public boolean getPrematureEof() {
-        return prematureEof;
-    }
-
-    private void parseBodyPart(InputStream is) throws IOException {
-        if (raw) {
-            handler.raw(new CloseShieldInputStream(is));
-        } else {
-            handler.startBodyPart();
-            parseEntity(is);
-            handler.endBodyPart();
-        }
-    }
-
-    /**
-     * Parses a header.
-     *
-     * @param is the stream to parse.
-     * @return a <code>BodyDescriptor</code> describing the body following
-     *         the header.
-     */
-    private BodyDescriptor parseHeader(InputStream is) throws IOException {
-        BodyDescriptor bd = new BodyDescriptor(bodyDescriptors.isEmpty()
-                        ? null : (BodyDescriptor) bodyDescriptors.getFirst());
-
-        handler.startHeader();
-
-        int lineNumber = rootStream.getLineNumber();
-
-        StringBuffer sb = new StringBuffer();
-        int curr = 0;
-        int prev = 0;
-        while ((curr = is.read()) != -1) {
-            if (curr == '\n' && (prev == '\n' || prev == 0)) {
-                /*
-                 * [\r]\n[\r]\n or an immediate \r\n have been seen.
-                 */
-                sb.deleteCharAt(sb.length() - 1);
-                break;
-            }
-            sb.append((char) curr);
-            prev = curr == '\r' ? prev : curr;
-        }
-
-//        if (curr == -1 && log.isWarnEnabled()) {
-//            log.warn("Line " + rootStream.getLineNumber()
-//                    + ": Unexpected end of headers detected. "
-//                    + "Boundary detected in header or EOF reached.");
-//        }
-
-        int start = 0;
-        int pos = 0;
-        int startLineNumber = lineNumber;
-        while (pos < sb.length()) {
-            while (pos < sb.length() && sb.charAt(pos) != '\r') {
-                pos++;
-            }
-            if (pos < sb.length() - 1 && sb.charAt(pos + 1) != '\n') {
-                pos++;
-                continue;
-            }
-
-            if (pos >= sb.length() - 2 || fieldChars.get(sb.charAt(pos + 2))) {
-
-                /*
-                 * field should be the complete field data excluding the
-                 * trailing \r\n.
-                 */
-                String field = sb.substring(start, pos);
-                start = pos + 2;
-
-                /*
-                 * Check for a valid field.
-                 */
-                int index = field.indexOf(':');
-                boolean valid = false;
-                if (index != -1 && fieldChars.get(field.charAt(0))) {
-                    valid = true;
-                    String fieldName = field.substring(0, index).trim();
-                    for (int i = 0; i < fieldName.length(); i++) {
-                        if (!fieldChars.get(fieldName.charAt(i))) {
-                            valid = false;
-                            break;
-                        }
-                    }
-
-                    if (valid) {
-                        handler.field(field);
-                        bd.addField(fieldName, field.substring(index + 1));
-                    }
-                }
-
-                if (!valid && log.isWarnEnabled()) {
-                    log.warn("Line " + startLineNumber
-                            + ": Ignoring invalid field: '" + field.trim() + "'");
-                }
-
-                startLineNumber = lineNumber;
-            }
-
-            pos += 2;
-            lineNumber++;
-        }
-
-        handler.endHeader();
-
-        return bd;
-    }
-
-    /**
-     * Sets the <code>ContentHandler</code> to use when reporting
-     * parsing events.
-     *
-     * @param h the <code>ContentHandler</code>.
-     */
-    public void setContentHandler(ContentHandler h) {
-        this.handler = h;
-    }
-
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/RootInputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/RootInputStream.java
deleted file mode 100644
index cc8b241..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/RootInputStream.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * <code>InputStream</code> used by the parser to wrap the original user
- * supplied stream. This stream keeps track of the current line number and
- * can also be truncated. When truncated the stream will appear to have
- * reached end of file. This is used by the parser's
- * {@link org.apache.james.mime4j.MimeStreamParser#stop()} method.
- *
- *
- * @version $Id: RootInputStream.java,v 1.2 2004/10/02 12:41:10 ntherning Exp $
- */
-class RootInputStream extends InputStream {
-    private InputStream is = null;
-    private int lineNumber = 1;
-    private int prev = -1;
-    private boolean truncated = false;
-
-    /**
-     * Creates a new <code>RootInputStream</code>.
-     *
-     * @param in the stream to read from.
-     */
-    public RootInputStream(InputStream is) {
-        this.is = is;
-    }
-
-    /**
-     * Gets the current line number starting at 1
-     * (the number of <code>\r\n</code> read so far plus 1).
-     *
-     * @return the current line number.
-     */
-    public int getLineNumber() {
-        return lineNumber;
-    }
-
-    /**
-     * Truncates this <code>InputStream</code>. After this call any
-     * call to {@link #read()}, {@link #read(byte[]) or
-     * {@link #read(byte[], int, int)} will return
-     * -1 as if end-of-file had been reached.
-     */
-    public void truncate() {
-        this.truncated = true;
-    }
-
-    /**
-     * @see java.io.InputStream#read()
-     */
-    public int read() throws IOException {
-        if (truncated) {
-            return -1;
-        }
-
-        int b = is.read();
-        if (prev == '\r' && b == '\n') {
-            lineNumber++;
-        }
-        prev = b;
-        return b;
-    }
-
-    /**
-     *
-     * @see java.io.InputStream#read(byte[], int, int)
-     */
-    public int read(byte[] b, int off, int len) throws IOException {
-        if (truncated) {
-            return -1;
-        }
-
-        int n = is.read(b, off, len);
-        for (int i = off; i < off + n; i++) {
-            if (prev == '\r' && b[i] == '\n') {
-                lineNumber++;
-            }
-            prev = b[i];
-        }
-        return n;
-    }
-
-    /**
-     * @see java.io.InputStream#read(byte[])
-     */
-    public int read(byte[] b) throws IOException {
-        return read(b, 0, b.length);
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/codec/EncoderUtil.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/codec/EncoderUtil.java
deleted file mode 100644
index 6841bc9..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/codec/EncoderUtil.java
+++ /dev/null
@@ -1,630 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.codec;
-
-import java.nio.ByteBuffer;
-import java.nio.charset.Charset;
-import java.util.BitSet;
-import java.util.Locale;
-
-import org.apache.james.mime4j.util.CharsetUtil;
-
-/**
- * ANDROID:  THIS CLASS IS COPIED FROM A NEWER VERSION OF MIME4J
- */
-
-/**
- * Static methods for encoding header field values. This includes encoded-words
- * as defined in <a href='http://www.faqs.org/rfcs/rfc2047.html'>RFC 2047</a>
- * or display-names of an e-mail address, for example.
- * 
- */
-public class EncoderUtil {
-
-    // This array is a lookup table that translates 6-bit positive integer index
-    // values into their "Base64 Alphabet" equivalents as specified in Table 1
-    // of RFC 2045.
-    // ANDROID:  THIS TABLE IS COPIED FROM BASE64OUTPUTSTREAM
-    static final byte[] BASE64_TABLE = { 'A', 'B', 'C', 'D', 'E', 'F',
-            'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
-            'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
-            'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
-            't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5',
-            '6', '7', '8', '9', '+', '/' };
-
-    // Byte used to pad output.
-    private static final byte BASE64_PAD = '=';
-
-    private static final BitSet Q_REGULAR_CHARS = initChars("=_?");
-
-    private static final BitSet Q_RESTRICTED_CHARS = initChars("=_?\"#$%&'(),.:;<>@[\\]^`{|}~");
-
-    private static final int MAX_USED_CHARACTERS = 50;
-
-    private static final String ENC_WORD_PREFIX = "=?";
-    private static final String ENC_WORD_SUFFIX = "?=";
-
-    private static final int ENCODED_WORD_MAX_LENGTH = 75; // RFC 2047
-
-    private static final BitSet TOKEN_CHARS = initChars("()<>@,;:\\\"/[]?=");
-
-    private static final BitSet ATEXT_CHARS = initChars("()<>@.,;:\\\"[]");
-
-    private static BitSet initChars(String specials) {
-        BitSet bs = new BitSet(128);
-        for (char ch = 33; ch < 127; ch++) {
-            if (specials.indexOf(ch) == -1) {
-                bs.set(ch);
-            }
-        }
-        return bs;
-    }
-
-    /**
-     * Selects one of the two encodings specified in RFC 2047.
-     */
-    public enum Encoding {
-        /** The B encoding (identical to base64 defined in RFC 2045). */
-        B,
-        /** The Q encoding (similar to quoted-printable defined in RFC 2045). */
-        Q
-    }
-
-    /**
-     * Indicates the intended usage of an encoded word.
-     */
-    public enum Usage {
-        /**
-         * Encoded word is used to replace a 'text' token in any Subject or
-         * Comments header field.
-         */
-        TEXT_TOKEN,
-        /**
-         * Encoded word is used to replace a 'word' entity within a 'phrase',
-         * for example, one that precedes an address in a From, To, or Cc
-         * header.
-         */
-        WORD_ENTITY
-    }
-
-    private EncoderUtil() {
-    }
-
-    /**
-     * Encodes the display-name portion of an address. See <a
-     * href='http://www.faqs.org/rfcs/rfc5322.html'>RFC 5322</a> section 3.4
-     * and <a href='http://www.faqs.org/rfcs/rfc2047.html'>RFC 2047</a> section
-     * 5.3. The specified string should not be folded.
-     * 
-     * @param displayName
-     *            display-name to encode.
-     * @return encoded display-name.
-     */
-    public static String encodeAddressDisplayName(String displayName) {
-        // display-name = phrase
-        // phrase = 1*( encoded-word / word )
-        // word = atom / quoted-string
-        // atom = [CFWS] 1*atext [CFWS]
-        // CFWS = comment or folding white space
-
-        if (isAtomPhrase(displayName)) {
-            return displayName;
-        } else if (hasToBeEncoded(displayName, 0)) {
-            return encodeEncodedWord(displayName, Usage.WORD_ENTITY);
-        } else {
-            return quote(displayName);
-        }
-    }
-
-    /**
-     * Encodes the local part of an address specification as described in RFC
-     * 5322 section 3.4.1. Leading and trailing CFWS should have been removed
-     * before calling this method. The specified string should not contain any
-     * illegal (control or non-ASCII) characters.
-     * 
-     * @param localPart
-     *            the local part to encode
-     * @return the encoded local part.
-     */
-    public static String encodeAddressLocalPart(String localPart) {
-        // local-part = dot-atom / quoted-string
-        // dot-atom = [CFWS] dot-atom-text [CFWS]
-        // CFWS = comment or folding white space
-
-        if (isDotAtomText(localPart)) {
-            return localPart;
-        } else {
-            return quote(localPart);
-        }
-    }
-
-    /**
-     * Encodes the specified strings into a header parameter as described in RFC
-     * 2045 section 5.1 and RFC 2183 section 2. The specified strings should not
-     * contain any illegal (control or non-ASCII) characters.
-     * 
-     * @param name
-     *            parameter name.
-     * @param value
-     *            parameter value.
-     * @return encoded result.
-     */
-    public static String encodeHeaderParameter(String name, String value) {
-        name = name.toLowerCase(Locale.US);
-
-        // value := token / quoted-string
-        if (isToken(value)) {
-            return name + "=" + value;
-        } else {
-            return name + "=" + quote(value);
-        }
-    }
-
-    /**
-     * Shortcut method that encodes the specified text into an encoded-word if
-     * the text has to be encoded.
-     * 
-     * @param text
-     *            text to encode.
-     * @param usage
-     *            whether the encoded-word is to be used to replace a text token
-     *            or a word entity (see RFC 822).
-     * @param usedCharacters
-     *            number of characters already used up (<code>0 <= usedCharacters <= 50</code>).
-     * @return the specified text if encoding is not necessary or an encoded
-     *         word or a sequence of encoded words otherwise.
-     */
-    public static String encodeIfNecessary(String text, Usage usage,
-            int usedCharacters) {
-        if (hasToBeEncoded(text, usedCharacters))
-            return encodeEncodedWord(text, usage, usedCharacters);
-        else
-            return text;
-    }
-
-    /**
-     * Determines if the specified string has to encoded into an encoded-word.
-     * Returns <code>true</code> if the text contains characters that don't
-     * fall into the printable ASCII character set or if the text contains a
-     * 'word' (sequence of non-whitespace characters) longer than 77 characters
-     * (including characters already used up in the line).
-     * 
-     * @param text
-     *            text to analyze.
-     * @param usedCharacters
-     *            number of characters already used up (<code>0 <= usedCharacters <= 50</code>).
-     * @return <code>true</code> if the specified text has to be encoded into
-     *         an encoded-word, <code>false</code> otherwise.
-     */
-    public static boolean hasToBeEncoded(String text, int usedCharacters) {
-        if (text == null)
-            throw new IllegalArgumentException();
-        if (usedCharacters < 0 || usedCharacters > MAX_USED_CHARACTERS)
-            throw new IllegalArgumentException();
-
-        int nonWhiteSpaceCount = usedCharacters;
-
-        for (int idx = 0; idx < text.length(); idx++) {
-            char ch = text.charAt(idx);
-            if (ch == '\t' || ch == ' ') {
-                nonWhiteSpaceCount = 0;
-            } else {
-                nonWhiteSpaceCount++;
-                if (nonWhiteSpaceCount > 77) {
-                    // Line cannot be folded into multiple lines with no more
-                    // than 78 characters each. Encoding as encoded-words makes
-                    // that possible. One character has to be reserved for
-                    // folding white space; that leaves 77 characters.
-                    return true;
-                }
-
-                if (ch < 32 || ch >= 127) {
-                    // non-printable ascii character has to be encoded
-                    return true;
-                }
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * Encodes the specified text into an encoded word or a sequence of encoded
-     * words separated by space. The text is separated into a sequence of
-     * encoded words if it does not fit in a single one.
-     * <p>
-     * The charset to encode the specified text into a byte array and the
-     * encoding to use for the encoded-word are detected automatically.
-     * <p>
-     * This method assumes that zero characters have already been used up in the
-     * current line.
-     * 
-     * @param text
-     *            text to encode.
-     * @param usage
-     *            whether the encoded-word is to be used to replace a text token
-     *            or a word entity (see RFC 822).
-     * @return the encoded word (or sequence of encoded words if the given text
-     *         does not fit in a single encoded word).
-     * @see #hasToBeEncoded(String, int)
-     */
-    public static String encodeEncodedWord(String text, Usage usage) {
-        return encodeEncodedWord(text, usage, 0, null, null);
-    }
-
-    /**
-     * Encodes the specified text into an encoded word or a sequence of encoded
-     * words separated by space. The text is separated into a sequence of
-     * encoded words if it does not fit in a single one.
-     * <p>
-     * The charset to encode the specified text into a byte array and the
-     * encoding to use for the encoded-word are detected automatically.
-     * 
-     * @param text
-     *            text to encode.
-     * @param usage
-     *            whether the encoded-word is to be used to replace a text token
-     *            or a word entity (see RFC 822).
-     * @param usedCharacters
-     *            number of characters already used up (<code>0 <= usedCharacters <= 50</code>).
-     * @return the encoded word (or sequence of encoded words if the given text
-     *         does not fit in a single encoded word).
-     * @see #hasToBeEncoded(String, int)
-     */
-    public static String encodeEncodedWord(String text, Usage usage,
-            int usedCharacters) {
-        return encodeEncodedWord(text, usage, usedCharacters, null, null);
-    }
-
-    /**
-     * Encodes the specified text into an encoded word or a sequence of encoded
-     * words separated by space. The text is separated into a sequence of
-     * encoded words if it does not fit in a single one.
-     * 
-     * @param text
-     *            text to encode.
-     * @param usage
-     *            whether the encoded-word is to be used to replace a text token
-     *            or a word entity (see RFC 822).
-     * @param usedCharacters
-     *            number of characters already used up (<code>0 <= usedCharacters <= 50</code>).
-     * @param charset
-     *            the Java charset that should be used to encode the specified
-     *            string into a byte array. A suitable charset is detected
-     *            automatically if this parameter is <code>null</code>.
-     * @param encoding
-     *            the encoding to use for the encoded-word (either B or Q). A
-     *            suitable encoding is automatically chosen if this parameter is
-     *            <code>null</code>.
-     * @return the encoded word (or sequence of encoded words if the given text
-     *         does not fit in a single encoded word).
-     * @see #hasToBeEncoded(String, int)
-     */
-    public static String encodeEncodedWord(String text, Usage usage,
-            int usedCharacters, Charset charset, Encoding encoding) {
-        if (text == null)
-            throw new IllegalArgumentException();
-        if (usedCharacters < 0 || usedCharacters > MAX_USED_CHARACTERS)
-            throw new IllegalArgumentException();
-
-        if (charset == null)
-            charset = determineCharset(text);
-
-        String mimeCharset = CharsetUtil.toMimeCharset(charset.name());
-        if (mimeCharset == null) {
-            // cannot happen if charset was originally null
-            throw new IllegalArgumentException("Unsupported charset");
-        }
-
-        byte[] bytes = encode(text, charset);
-
-        if (encoding == null)
-            encoding = determineEncoding(bytes, usage);
-
-        if (encoding == Encoding.B) {
-            String prefix = ENC_WORD_PREFIX + mimeCharset + "?B?";
-            return encodeB(prefix, text, usedCharacters, charset, bytes);
-        } else {
-            String prefix = ENC_WORD_PREFIX + mimeCharset + "?Q?";
-            return encodeQ(prefix, text, usage, usedCharacters, charset, bytes);
-        }
-    }
-
-    /**
-     * Encodes the specified byte array using the B encoding defined in RFC
-     * 2047.
-     * 
-     * @param bytes
-     *            byte array to encode.
-     * @return encoded string.
-     */
-    public static String encodeB(byte[] bytes) {
-        StringBuilder sb = new StringBuilder();
-
-        int idx = 0;
-        final int end = bytes.length;
-        for (; idx < end - 2; idx += 3) {
-            int data = (bytes[idx] & 0xff) << 16 | (bytes[idx + 1] & 0xff) << 8
-                    | bytes[idx + 2] & 0xff;
-            sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]);
-            sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]);
-            sb.append((char) BASE64_TABLE[data >> 6 & 0x3f]);
-            sb.append((char) BASE64_TABLE[data & 0x3f]);
-        }
-
-        if (idx == end - 2) {
-            int data = (bytes[idx] & 0xff) << 16 | (bytes[idx + 1] & 0xff) << 8;
-            sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]);
-            sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]);
-            sb.append((char) BASE64_TABLE[data >> 6 & 0x3f]);
-            sb.append((char) BASE64_PAD);
-
-        } else if (idx == end - 1) {
-            int data = (bytes[idx] & 0xff) << 16;
-            sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]);
-            sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]);
-            sb.append((char) BASE64_PAD);
-            sb.append((char) BASE64_PAD);
-        }
-
-        return sb.toString();
-    }
-
-    /**
-     * Encodes the specified byte array using the Q encoding defined in RFC
-     * 2047.
-     * 
-     * @param bytes
-     *            byte array to encode.
-     * @param usage
-     *            whether the encoded-word is to be used to replace a text token
-     *            or a word entity (see RFC 822).
-     * @return encoded string.
-     */
-    public static String encodeQ(byte[] bytes, Usage usage) {
-        BitSet qChars = usage == Usage.TEXT_TOKEN ? Q_REGULAR_CHARS
-                : Q_RESTRICTED_CHARS;
-
-        StringBuilder sb = new StringBuilder();
-
-        final int end = bytes.length;
-        for (int idx = 0; idx < end; idx++) {
-            int v = bytes[idx] & 0xff;
-            if (v == 32) {
-                sb.append('_');
-            } else if (!qChars.get(v)) {
-                sb.append('=');
-                sb.append(hexDigit(v >>> 4));
-                sb.append(hexDigit(v & 0xf));
-            } else {
-                sb.append((char) v);
-            }
-        }
-
-        return sb.toString();
-    }
-
-    /**
-     * Tests whether the specified string is a token as defined in RFC 2045
-     * section 5.1.
-     * 
-     * @param str
-     *            string to test.
-     * @return <code>true</code> if the specified string is a RFC 2045 token,
-     *         <code>false</code> otherwise.
-     */
-    public static boolean isToken(String str) {
-        // token := 1*<any (US-ASCII) CHAR except SPACE, CTLs, or tspecials>
-        // tspecials := "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "\" /
-        // <"> / "/" / "[" / "]" / "?" / "="
-        // CTL := 0.- 31., 127.
-
-        final int length = str.length();
-        if (length == 0)
-            return false;
-
-        for (int idx = 0; idx < length; idx++) {
-            char ch = str.charAt(idx);
-            if (!TOKEN_CHARS.get(ch))
-                return false;
-        }
-
-        return true;
-    }
-
-    private static boolean isAtomPhrase(String str) {
-        // atom = [CFWS] 1*atext [CFWS]
-
-        boolean containsAText = false;
-
-        final int length = str.length();
-        for (int idx = 0; idx < length; idx++) {
-            char ch = str.charAt(idx);
-            if (ATEXT_CHARS.get(ch)) {
-                containsAText = true;
-            } else if (!CharsetUtil.isWhitespace(ch)) {
-                return false;
-            }
-        }
-
-        return containsAText;
-    }
-
-    // RFC 5322 section 3.2.3
-    private static boolean isDotAtomText(String str) {
-        // dot-atom-text = 1*atext *("." 1*atext)
-        // atext = ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" /
-        // "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"
-
-        char prev = '.';
-
-        final int length = str.length();
-        if (length == 0)
-            return false;
-
-        for (int idx = 0; idx < length; idx++) {
-            char ch = str.charAt(idx);
-
-            if (ch == '.') {
-                if (prev == '.' || idx == length - 1)
-                    return false;
-            } else {
-                if (!ATEXT_CHARS.get(ch))
-                    return false;
-            }
-
-            prev = ch;
-        }
-
-        return true;
-    }
-
-    // RFC 5322 section 3.2.4
-    private static String quote(String str) {
-        // quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS]
-        // qcontent = qtext / quoted-pair
-        // qtext = %d33 / %d35-91 / %d93-126
-        // quoted-pair = ("\" (VCHAR / WSP))
-        // VCHAR = %x21-7E
-        // DQUOTE = %x22
-
-        String escaped = str.replaceAll("[\\\\\"]", "\\\\$0");
-        return "\"" + escaped + "\"";
-    }
-
-    private static String encodeB(String prefix, String text,
-            int usedCharacters, Charset charset, byte[] bytes) {
-        int encodedLength = bEncodedLength(bytes);
-
-        int totalLength = prefix.length() + encodedLength
-                + ENC_WORD_SUFFIX.length();
-        if (totalLength <= ENCODED_WORD_MAX_LENGTH - usedCharacters) {
-            return prefix + encodeB(bytes) + ENC_WORD_SUFFIX;
-        } else {
-            int splitOffset = text.offsetByCodePoints(text.length() / 2, -1);
-                                                         
-            String part1 = text.substring(0, splitOffset);
-            byte[] bytes1 = encode(part1, charset);
-            String word1 = encodeB(prefix, part1, usedCharacters, charset,
-                    bytes1);
-
-            String part2 = text.substring(splitOffset);
-            byte[] bytes2 = encode(part2, charset);
-            String word2 = encodeB(prefix, part2, 0, charset, bytes2);
-
-            return word1 + " " + word2;
-        }
-    }
-
-    private static int bEncodedLength(byte[] bytes) {
-        return (bytes.length + 2) / 3 * 4;
-    }
-
-    private static String encodeQ(String prefix, String text, Usage usage,
-            int usedCharacters, Charset charset, byte[] bytes) {
-        int encodedLength = qEncodedLength(bytes, usage);
-
-        int totalLength = prefix.length() + encodedLength
-                + ENC_WORD_SUFFIX.length();
-        if (totalLength <= ENCODED_WORD_MAX_LENGTH - usedCharacters) {
-            return prefix + encodeQ(bytes, usage) + ENC_WORD_SUFFIX;
-        } else {
-            int splitOffset = text.offsetByCodePoints(text.length() / 2, -1);
-
-            String part1 = text.substring(0, splitOffset);
-            byte[] bytes1 = encode(part1, charset);
-            String word1 = encodeQ(prefix, part1, usage, usedCharacters,
-                    charset, bytes1);
-
-            String part2 = text.substring(splitOffset);
-            byte[] bytes2 = encode(part2, charset);
-            String word2 = encodeQ(prefix, part2, usage, 0, charset, bytes2);
-
-            return word1 + " " + word2;
-        }
-    }
-
-    private static int qEncodedLength(byte[] bytes, Usage usage) {
-        BitSet qChars = usage == Usage.TEXT_TOKEN ? Q_REGULAR_CHARS
-                : Q_RESTRICTED_CHARS;
-
-        int count = 0;
-
-        for (int idx = 0; idx < bytes.length; idx++) {
-            int v = bytes[idx] & 0xff;
-            if (v == 32) {
-                count++;
-            } else if (!qChars.get(v)) {
-                count += 3;
-            } else {
-                count++;
-            }
-        }
-
-        return count;
-    }
-
-    private static byte[] encode(String text, Charset charset) {
-        ByteBuffer buffer = charset.encode(text);
-        byte[] bytes = new byte[buffer.limit()];
-        buffer.get(bytes);
-        return bytes;
-    }
-
-    private static Charset determineCharset(String text) {
-        // it is an important property of iso-8859-1 that it directly maps
-        // unicode code points 0000 to 00ff to byte values 00 to ff.
-        boolean ascii = true;
-        final int len = text.length();
-        for (int index = 0; index < len; index++) {
-            char ch = text.charAt(index);
-            if (ch > 0xff) {
-                return CharsetUtil.UTF_8;
-            }
-            if (ch > 0x7f) {
-                ascii = false;
-            }
-        }
-        return ascii ? CharsetUtil.US_ASCII : CharsetUtil.ISO_8859_1;
-    }
-
-    private static Encoding determineEncoding(byte[] bytes, Usage usage) {
-        if (bytes.length == 0)
-            return Encoding.Q;
-
-        BitSet qChars = usage == Usage.TEXT_TOKEN ? Q_REGULAR_CHARS
-                : Q_RESTRICTED_CHARS;
-
-        int qEncoded = 0;
-        for (int i = 0; i < bytes.length; i++) {
-            int v = bytes[i] & 0xff;
-            if (v != 32 && !qChars.get(v)) {
-                qEncoded++;
-            }
-        }
-
-        int percentage = qEncoded * 100 / bytes.length;
-        return percentage > 30 ? Encoding.B : Encoding.Q;
-    }
-
-    private static char hexDigit(int i) {
-        return i < 10 ? (char) (i + '0') : (char) (i - 10 + 'A');
-    }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/Base64InputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/Base64InputStream.java
deleted file mode 100644
index 77f5d7d..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/Base64InputStream.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-/**
- * Modified to improve efficiency by Android   21-Aug-2009
- */
-
-package org.apache.james.mime4j.decoder;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Performs Base-64 decoding on an underlying stream.
- * 
- * 
- * @version $Id: Base64InputStream.java,v 1.3 2004/11/29 13:15:47 ntherning Exp $
- */
-public class Base64InputStream extends InputStream {
-    private final InputStream s;
-    private int outCount = 0;
-    private int outIndex = 0;
-    private final int[] outputBuffer = new int[3];
-    private final byte[] inputBuffer = new byte[4];
-    private boolean done = false;
-
-    public Base64InputStream(InputStream s) {
-        this.s = s;
-    }
-
-    /**
-     * Closes the underlying stream.
-     * 
-     * @throws IOException on I/O errors.
-     */
-    @Override
-    public void close() throws IOException {
-        s.close();
-    }
-    
-    @Override
-    public int read() throws IOException {
-        if (outIndex == outCount) {
-            fillBuffer();
-            if (outIndex == outCount) {
-                return -1;
-            }
-        }
-
-        return outputBuffer[outIndex++];
-    }
-
-    /**
-     * Retrieve data from the underlying stream, decode it,
-     * and put the results in the byteq.
-     * @throws IOException
-     */
-    private void fillBuffer() throws IOException {
-        outCount = 0;
-        outIndex = 0;
-        int inCount = 0;
-
-        int i;
-        // "done" is needed for the two successive '=' at the end
-        while (!done) {
-            switch (i = s.read()) {
-                case -1:
-                    // No more input - just return, let outputBuffer drain out, and be done
-                    return;
-                case '=':
-                    // once we meet the first '=', avoid reading the second '='
-                    done = true;
-                    decodeAndEnqueue(inCount);
-                    return;
-                default:
-                    byte sX = TRANSLATION[i];
-                    if (sX < 0) continue;
-                    inputBuffer[inCount++] = sX;
-                    if (inCount == 4) {
-                        decodeAndEnqueue(inCount);
-                        return;
-                    }
-                    break;
-            }
-        }
-    }
-
-    private void decodeAndEnqueue(int len) {
-        int accum = 0;
-        accum |= inputBuffer[0] << 18;
-        accum |= inputBuffer[1] << 12;
-        accum |= inputBuffer[2] << 6;
-        accum |= inputBuffer[3];
-
-        // There's a bit of duplicated code here because we want to have straight-through operation
-        // for the most common case of len==4
-        if (len == 4) {
-            outputBuffer[0] = (accum >> 16) & 0xFF;
-            outputBuffer[1] = (accum >> 8) & 0xFF;
-            outputBuffer[2] = (accum) & 0xFF;
-            outCount = 3;
-            return;
-        } else if (len == 3) {
-            outputBuffer[0] = (accum >> 16) & 0xFF;
-            outputBuffer[1] = (accum >> 8) & 0xFF;
-            outCount = 2;
-            return;
-        } else {    // len == 2
-            outputBuffer[0] = (accum >> 16) & 0xFF;
-            outCount = 1;
-            return;
-        }
-    }
-
-    private static byte[] TRANSLATION = {
-        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x00 */
-        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x10 */
-        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, /* 0x20 */
-        52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, /* 0x30 */
-        -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /* 0x40 */
-        15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, /* 0x50 */
-        -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /* 0x60 */
-        41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, /* 0x70 */
-        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x80 */
-        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x90 */
-        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xA0 */
-        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xB0 */
-        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xC0 */
-        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xD0 */
-        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xE0 */
-        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1     /* 0xF0 */
-    };
-
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/ByteQueue.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/ByteQueue.java
deleted file mode 100644
index 6d7ccef..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/ByteQueue.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.decoder;
-
-import java.util.Iterator;
-
-public class ByteQueue {
-
-    private UnboundedFifoByteBuffer buf;
-    private int initialCapacity = -1;
-
-    public ByteQueue() {
-        buf = new UnboundedFifoByteBuffer();
-    }
-
-    public ByteQueue(int initialCapacity) {
-        buf = new UnboundedFifoByteBuffer(initialCapacity);
-        this.initialCapacity = initialCapacity;
-    }
-
-    public void enqueue(byte b) {
-        buf.add(b);
-    }
-
-    public byte dequeue() {
-        return buf.remove();
-    }
-
-    public int count() {
-        return buf.size();
-    }
-
-    public void clear() {
-        if (initialCapacity != -1)
-            buf = new UnboundedFifoByteBuffer(initialCapacity);
-        else
-            buf = new UnboundedFifoByteBuffer();
-    }
-
-    public Iterator iterator() {
-        return buf.iterator();
-    }
-
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/DecoderUtil.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/DecoderUtil.java
deleted file mode 100644
index 48fe07d..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/DecoderUtil.java
+++ /dev/null
@@ -1,284 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.decoder;
-
-//BEGIN android-changed: Stubbing out logging
-import org.apache.james.mime4j.Log;
-import org.apache.james.mime4j.LogFactory;
-//END android-changed
-import org.apache.james.mime4j.util.CharsetUtil;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-
-/**
- * Static methods for decoding strings, byte arrays and encoded words.
- *
- * 
- * @version $Id: DecoderUtil.java,v 1.3 2005/02/07 15:33:59 ntherning Exp $
- */
-public class DecoderUtil {
-    private static Log log = LogFactory.getLog(DecoderUtil.class);
-    
-    /**
-     * Decodes a string containing quoted-printable encoded data. 
-     * 
-     * @param s the string to decode.
-     * @return the decoded bytes.
-     */
-    public static byte[] decodeBaseQuotedPrintable(String s) {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        
-        try {
-            byte[] bytes = s.getBytes("US-ASCII");
-            
-            QuotedPrintableInputStream is = new QuotedPrintableInputStream(
-                                               new ByteArrayInputStream(bytes));
-            
-            int b = 0;
-            while ((b = is.read()) != -1) {
-                baos.write(b);
-            }
-        } catch (IOException e) {
-            /*
-             * This should never happen!
-             */
-            log.error(e);
-        }
-        
-        return baos.toByteArray();
-    }
-    
-    /**
-     * Decodes a string containing base64 encoded data. 
-     * 
-     * @param s the string to decode.
-     * @return the decoded bytes.
-     */
-    public static byte[] decodeBase64(String s) {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        
-        try {
-            byte[] bytes = s.getBytes("US-ASCII");
-            
-            Base64InputStream is = new Base64InputStream(
-                                        new ByteArrayInputStream(bytes));
-            
-            int b = 0;
-            while ((b = is.read()) != -1) {
-                baos.write(b);
-            }
-        } catch (IOException e) {
-            /*
-             * This should never happen!
-             */
-            log.error(e);
-        }
-        
-        return baos.toByteArray();
-    }
-    
-    /**
-     * Decodes an encoded word encoded with the 'B' encoding (described in 
-     * RFC 2047) found in a header field body.
-     * 
-     * @param encodedWord the encoded word to decode.
-     * @param charset the Java charset to use.
-     * @return the decoded string.
-     * @throws UnsupportedEncodingException if the given Java charset isn't 
-     *         supported.
-     */
-    public static String decodeB(String encodedWord, String charset) 
-            throws UnsupportedEncodingException {
-        
-        return new String(decodeBase64(encodedWord), charset);
-    }
-    
-    /**
-     * Decodes an encoded word encoded with the 'Q' encoding (described in 
-     * RFC 2047) found in a header field body.
-     * 
-     * @param encodedWord the encoded word to decode.
-     * @param charset the Java charset to use.
-     * @return the decoded string.
-     * @throws UnsupportedEncodingException if the given Java charset isn't 
-     *         supported.
-     */
-    public static String decodeQ(String encodedWord, String charset)
-            throws UnsupportedEncodingException {
-           
-        /*
-         * Replace _ with =20
-         */
-        StringBuffer sb = new StringBuffer();
-        for (int i = 0; i < encodedWord.length(); i++) {
-            char c = encodedWord.charAt(i);
-            if (c == '_') {
-                sb.append("=20");
-            } else {
-                sb.append(c);
-            }
-        }
-        
-        return new String(decodeBaseQuotedPrintable(sb.toString()), charset);
-    }
-    
-    /**
-     * Decodes a string containing encoded words as defined by RFC 2047.
-     * Encoded words in have the form 
-     * =?charset?enc?Encoded word?= where enc is either 'Q' or 'q' for 
-     * quoted-printable and 'B' or 'b' for Base64.
-     * 
-     * ANDROID:  COPIED FROM A NEWER VERSION OF MIME4J
-     * 
-     * @param body the string to decode.
-     * @return the decoded string.
-     */
-    public static String decodeEncodedWords(String body) {
-        
-        // ANDROID:  Most strings will not include "=?" so a quick test can prevent unneeded
-        // object creation.  This could also be handled via lazy creation of the StringBuilder.
-        if (body.indexOf("=?") == -1) {
-            return body;
-        }
-
-        int previousEnd = 0;
-        boolean previousWasEncoded = false;
-
-        StringBuilder sb = new StringBuilder();
-
-        while (true) {
-            int begin = body.indexOf("=?", previousEnd);
-
-            // ANDROID:  The mime4j original version has an error here.  It gets confused if
-            // the encoded string begins with an '=' (just after "?Q?").  This patch seeks forward
-            // to find the two '?' in the "header", before looking for the final "?=".
-            if (begin == -1) {
-                break;
-            }
-            int qm1 = body.indexOf('?', begin + 2);
-            if (qm1 == -1) {
-                break;
-            }
-            int qm2 = body.indexOf('?', qm1 + 1);
-            if (qm2 == -1) {
-                break;
-            }
-            int end = body.indexOf("?=", qm2 + 1);
-            if (end == -1) {
-                break;
-            }
-            end += 2;
-
-            String sep = body.substring(previousEnd, begin);
-
-            String decoded = decodeEncodedWord(body, begin, end);
-            if (decoded == null) {
-                sb.append(sep);
-                sb.append(body.substring(begin, end));
-            } else {
-                if (!previousWasEncoded || !CharsetUtil.isWhitespace(sep)) {
-                    sb.append(sep);
-                }
-                sb.append(decoded);
-            }
-
-            previousEnd = end;
-            previousWasEncoded = decoded != null;
-        }
-
-        if (previousEnd == 0)
-            return body;
-
-        sb.append(body.substring(previousEnd));
-        return sb.toString();
-    }
-
-    // return null on error. Begin is index of '=?' in body.
-    public static String decodeEncodedWord(String body, int begin, int end) {
-        // Skip the '?=' chars in body and scan forward from there for next '?'
-        int qm1 = body.indexOf('?', begin + 2);
-        if (qm1 == -1 || qm1 == end - 2)
-            return null;
-
-        int qm2 = body.indexOf('?', qm1 + 1);
-        if (qm2 == -1 || qm2 == end - 2)
-            return null;
-
-        String mimeCharset = body.substring(begin + 2, qm1);
-        String encoding = body.substring(qm1 + 1, qm2);
-        String encodedText = body.substring(qm2 + 1, end - 2);
-
-        String charset = CharsetUtil.toJavaCharset(mimeCharset);
-        if (charset == null) {
-            if (log.isWarnEnabled()) {
-                log.warn("MIME charset '" + mimeCharset + "' in encoded word '"
-                        + body.substring(begin, end) + "' doesn't have a "
-                        + "corresponding Java charset");
-            }
-            return null;
-        } else if (!CharsetUtil.isDecodingSupported(charset)) {
-            if (log.isWarnEnabled()) {
-                log.warn("Current JDK doesn't support decoding of charset '"
-                        + charset + "' (MIME charset '" + mimeCharset
-                        + "' in encoded word '" + body.substring(begin, end)
-                        + "')");
-            }
-            return null;
-        }
-
-        if (encodedText.length() == 0) {
-            if (log.isWarnEnabled()) {
-                log.warn("Missing encoded text in encoded word: '"
-                        + body.substring(begin, end) + "'");
-            }
-            return null;
-        }
-
-        try {
-            if (encoding.equalsIgnoreCase("Q")) {
-                return DecoderUtil.decodeQ(encodedText, charset);
-            } else if (encoding.equalsIgnoreCase("B")) {
-                return DecoderUtil.decodeB(encodedText, charset);
-            } else {
-                if (log.isWarnEnabled()) {
-                    log.warn("Warning: Unknown encoding in encoded word '"
-                            + body.substring(begin, end) + "'");
-                }
-                return null;
-            }
-        } catch (UnsupportedEncodingException e) {
-            // should not happen because of isDecodingSupported check above
-            if (log.isWarnEnabled()) {
-                log.warn("Unsupported encoding in encoded word '"
-                        + body.substring(begin, end) + "'", e);
-            }
-            return null;
-        } catch (RuntimeException e) {
-            if (log.isWarnEnabled()) {
-                log.warn("Could not decode encoded word '"
-                        + body.substring(begin, end) + "'", e);
-            }
-            return null;
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java
deleted file mode 100644
index e43f398..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java
+++ /dev/null
@@ -1,229 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.decoder;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-//BEGIN android-changed: Stubbing out logging
-import org.apache.james.mime4j.Log;
-import org.apache.james.mime4j.LogFactory;
-//END android-changed
-
-/**
- * Performs Quoted-Printable decoding on an underlying stream.
- * 
- * 
- * 
- * @version $Id: QuotedPrintableInputStream.java,v 1.3 2004/11/29 13:15:47 ntherning Exp $
- */
-public class QuotedPrintableInputStream extends InputStream {
-    private static Log log = LogFactory.getLog(QuotedPrintableInputStream.class);
-    
-    private InputStream stream;
-    ByteQueue byteq = new ByteQueue();
-    ByteQueue pushbackq = new ByteQueue();
-    private byte state = 0;
-
-    public QuotedPrintableInputStream(InputStream stream) {
-        this.stream = stream;
-    }
-    
-    /**
-     * Closes the underlying stream.
-     * 
-     * @throws IOException on I/O errors.
-     */
-    public void close() throws IOException {
-        stream.close();
-    }
-
-    public int read() throws IOException {
-        fillBuffer();
-        if (byteq.count() == 0)
-            return -1;
-        else {
-            byte val = byteq.dequeue();
-            if (val >= 0)
-                return val;
-            else
-                return val & 0xFF;
-        }
-    }
-
-    /**
-     * Pulls bytes out of the underlying stream and places them in the
-     * pushback queue.  This is necessary (vs. reading from the
-     * underlying stream directly) to detect and filter out "transport
-     * padding" whitespace, i.e., all whitespace that appears immediately
-     * before a CRLF.
-     *
-     * @throws IOException Underlying stream threw IOException.
-     */
-    private void populatePushbackQueue() throws IOException {
-        //Debug.verify(pushbackq.count() == 0, "PopulatePushbackQueue called when pushback queue was not empty!");
-
-        if (pushbackq.count() != 0)
-            return;
-
-        while (true) {
-            int i = stream.read();
-            switch (i) {
-                case -1:
-                    // stream is done
-                    pushbackq.clear();  // discard any whitespace preceding EOF
-                    return;
-                case ' ':
-                case '\t':
-                    pushbackq.enqueue((byte)i);
-                    break;
-                case '\r':
-                case '\n':
-                    pushbackq.clear();  // discard any whitespace preceding EOL
-                    pushbackq.enqueue((byte)i);
-                    return;
-                default:
-                    pushbackq.enqueue((byte)i);
-                    return;
-            }
-        }
-    }
-
-    /**
-     * Causes the pushback queue to get populated if it is empty, then
-     * consumes and decodes bytes out of it until one or more bytes are
-     * in the byte queue.  This decoding step performs the actual QP
-     * decoding.
-     *
-     * @throws IOException Underlying stream threw IOException.
-     */
-    private void fillBuffer() throws IOException {
-        byte msdChar = 0;  // first digit of escaped num
-        while (byteq.count() == 0) {
-            if (pushbackq.count() == 0) {
-                populatePushbackQueue();
-                if (pushbackq.count() == 0)
-                    return;
-            }
-
-            byte b = (byte)pushbackq.dequeue();
-
-            switch (state) {
-                case 0:  // start state, no bytes pending
-                    if (b != '=') {
-                        byteq.enqueue(b);
-                        break;  // state remains 0
-                    } else {
-                        state = 1;
-                        break;
-                    }
-                case 1:  // encountered "=" so far
-                    if (b == '\r') {
-                        state = 2;
-                        break;
-                    } else if ((b >= '0' && b <= '9') || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f')) {
-                        state = 3;
-                        msdChar = b;  // save until next digit encountered
-                        break;
-                    } else if (b == '=') {
-                        /*
-                         * Special case when == is encountered.
-                         * Emit one = and stay in this state.
-                         */
-                        if (log.isWarnEnabled()) {
-                            log.warn("Malformed MIME; got ==");
-                        }
-                        byteq.enqueue((byte)'=');
-                        break;
-                    } else {
-                        if (log.isWarnEnabled()) {
-                            log.warn("Malformed MIME; expected \\r or "
-                                    + "[0-9A-Z], got " + b);
-                        }
-                        state = 0;
-                        byteq.enqueue((byte)'=');
-                        byteq.enqueue(b);
-                        break;
-                    }
-                case 2:  // encountered "=\r" so far
-                    if (b == '\n') {
-                        state = 0;
-                        break;
-                    } else {
-                        if (log.isWarnEnabled()) {
-                            log.warn("Malformed MIME; expected " 
-                                    + (int)'\n' + ", got " + b);
-                        }
-                        state = 0;
-                        byteq.enqueue((byte)'=');
-                        byteq.enqueue((byte)'\r');
-                        byteq.enqueue(b);
-                        break;
-                    }
-                case 3:  // encountered =<digit> so far; expecting another <digit> to complete the octet
-                    if ((b >= '0' && b <= '9') || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f')) {
-                        byte msd = asciiCharToNumericValue(msdChar);
-                        byte low = asciiCharToNumericValue(b);
-                        state = 0;
-                        byteq.enqueue((byte)((msd << 4) | low));
-                        break;
-                    } else {
-                        if (log.isWarnEnabled()) {
-                            log.warn("Malformed MIME; expected "
-                                     + "[0-9A-Z], got " + b);
-                        }
-                        state = 0;
-                        byteq.enqueue((byte)'=');
-                        byteq.enqueue(msdChar);
-                        byteq.enqueue(b);
-                        break;
-                    }
-                default:  // should never happen
-                    log.error("Illegal state: " + state);
-                    state = 0;
-                    byteq.enqueue(b);
-                    break;
-            }
-        }
-    }
-
-    /**
-     * Converts '0' => 0, 'A' => 10, etc.
-     * @param c ASCII character value.
-     * @return Numeric value of hexadecimal character.
-     */
-    private byte asciiCharToNumericValue(byte c) {
-        if (c >= '0' && c <= '9') {
-            return (byte)(c - '0');
-        } else if (c >= 'A' && c <= 'Z') {
-            return (byte)(0xA + (c - 'A'));
-        } else if (c >= 'a' && c <= 'z') {
-            return (byte)(0xA + (c - 'a'));
-        } else {
-            /*
-             * This should never happen since all calls to this method
-             * are preceded by a check that c is in [0-9A-Za-z]
-             */
-            throw new IllegalArgumentException((char) c 
-                    + " is not a hexadecimal digit");
-        }
-    }
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java
deleted file mode 100644
index f01194f..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java
+++ /dev/null
@@ -1,272 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.decoder;
-
-import java.util.Iterator;
-import java.util.NoSuchElementException;
-
-/**
- * UnboundedFifoByteBuffer is a very efficient buffer implementation.
- * According to performance testing, it exhibits a constant access time, but it
- * also outperforms ArrayList when used for the same purpose.
- * <p>
- * The removal order of an <code>UnboundedFifoByteBuffer</code> is based on the insertion
- * order; elements are removed in the same order in which they were added.
- * The iteration order is the same as the removal order.
- * <p>
- * The {@link #remove()} and {@link #get()} operations perform in constant time.
- * The {@link #add(Object)} operation performs in amortized constant time.  All
- * other operations perform in linear time or worse.
- * <p>
- * Note that this implementation is not synchronized.  The following can be
- * used to provide synchronized access to your <code>UnboundedFifoByteBuffer</code>:
- * <pre>
- *   Buffer fifo = BufferUtils.synchronizedBuffer(new UnboundedFifoByteBuffer());
- * </pre>
- * <p>
- * This buffer prevents null objects from being added.
- *
- * @since Commons Collections 3.0 (previously in main package v2.1)
- * @version $Revision: 1.1 $ $Date: 2004/08/24 06:52:02 $
- *
- * 
- * 
- * 
- * 
- * 
- */
-class UnboundedFifoByteBuffer {
-
-    protected byte[] buffer;
-    protected int head;
-    protected int tail;
-
-    /**
-     * Constructs an UnboundedFifoByteBuffer with the default number of elements.
-     * It is exactly the same as performing the following:
-     *
-     * <pre>
-     *   new UnboundedFifoByteBuffer(32);
-     * </pre>
-     */
-    public UnboundedFifoByteBuffer() {
-        this(32);
-    }
-
-    /**
-     * Constructs an UnboundedFifoByteBuffer with the specified number of elements.
-     * The integer must be a positive integer.
-     *
-     * @param initialSize  the initial size of the buffer
-     * @throws IllegalArgumentException  if the size is less than 1
-     */
-    public UnboundedFifoByteBuffer(int initialSize) {
-        if (initialSize <= 0) {
-            throw new IllegalArgumentException("The size must be greater than 0");
-        }
-        buffer = new byte[initialSize + 1];
-        head = 0;
-        tail = 0;
-    }
-
-    /**
-     * Returns the number of elements stored in the buffer.
-     *
-     * @return this buffer's size
-     */
-    public int size() {
-        int size = 0;
-
-        if (tail < head) {
-            size = buffer.length - head + tail;
-        } else {
-            size = tail - head;
-        }
-
-        return size;
-    }
-
-    /**
-     * Returns true if this buffer is empty; false otherwise.
-     *
-     * @return true if this buffer is empty
-     */
-    public boolean isEmpty() {
-        return (size() == 0);
-    }
-
-    /**
-     * Adds the given element to this buffer.
-     *
-     * @param b  the byte to add
-     * @return true, always
-     */
-    public boolean add(final byte b) {
-
-        if (size() + 1 >= buffer.length) {
-            byte[] tmp = new byte[((buffer.length - 1) * 2) + 1];
-
-            int j = 0;
-            for (int i = head; i != tail;) {
-                tmp[j] = buffer[i];
-                buffer[i] = 0;
-
-                j++;
-                i++;
-                if (i == buffer.length) {
-                    i = 0;
-                }
-            }
-
-            buffer = tmp;
-            head = 0;
-            tail = j;
-        }
-
-        buffer[tail] = b;
-        tail++;
-        if (tail >= buffer.length) {
-            tail = 0;
-        }
-        return true;
-    }
-
-    /**
-     * Returns the next object in the buffer.
-     *
-     * @return the next object in the buffer
-     * @throws BufferUnderflowException  if this buffer is empty
-     */
-    public byte get() {
-        if (isEmpty()) {
-            throw new IllegalStateException("The buffer is already empty");
-        }
-
-        return buffer[head];
-    }
-
-    /**
-     * Removes the next object from the buffer
-     *
-     * @return the removed object
-     * @throws BufferUnderflowException  if this buffer is empty
-     */
-    public byte remove() {
-        if (isEmpty()) {
-            throw new IllegalStateException("The buffer is already empty");
-        }
-
-        byte element = buffer[head];
-
-        head++;
-        if (head >= buffer.length) {
-            head = 0;
-        }
-
-        return element;
-    }
-
-    /**
-     * Increments the internal index.
-     *
-     * @param index  the index to increment
-     * @return the updated index
-     */
-    private int increment(int index) {
-        index++;
-        if (index >= buffer.length) {
-            index = 0;
-        }
-        return index;
-    }
-
-    /**
-     * Decrements the internal index.
-     *
-     * @param index  the index to decrement
-     * @return the updated index
-     */
-    private int decrement(int index) {
-        index--;
-        if (index < 0) {
-            index = buffer.length - 1;
-        }
-        return index;
-    }
-
-    /**
-     * Returns an iterator over this buffer's elements.
-     *
-     * @return an iterator over this buffer's elements
-     */
-    public Iterator iterator() {
-        return new Iterator() {
-
-            private int index = head;
-            private int lastReturnedIndex = -1;
-
-            public boolean hasNext() {
-                return index != tail;
-
-            }
-
-            public Object next() {
-                if (!hasNext()) {
-                    throw new NoSuchElementException();
-                }
-                lastReturnedIndex = index;
-                index = increment(index);
-                return new Byte(buffer[lastReturnedIndex]);
-            }
-
-            public void remove() {
-                if (lastReturnedIndex == -1) {
-                    throw new IllegalStateException();
-                }
-
-                // First element can be removed quickly
-                if (lastReturnedIndex == head) {
-                    UnboundedFifoByteBuffer.this.remove();
-                    lastReturnedIndex = -1;
-                    return;
-                }
-
-                // Other elements require us to shift the subsequent elements
-                int i = lastReturnedIndex + 1;
-                while (i != tail) {
-                    if (i >= buffer.length) {
-                        buffer[i - 1] = buffer[0];
-                        i = 0;
-                    } else {
-                        buffer[i - 1] = buffer[i];
-                        i++;
-                    }
-                }
-
-                lastReturnedIndex = -1;
-                tail = decrement(tail);
-                buffer[tail] = 0;
-                index = decrement(index);
-            }
-
-        };
-    }
-
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/AddressListField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/AddressListField.java
deleted file mode 100644
index df9f398..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/AddressListField.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field;
-
-//BEGIN android-changed: Stubbing out logging
-import org.apache.james.mime4j.Log;
-import org.apache.james.mime4j.LogFactory;
-//END android-changed
-import org.apache.james.mime4j.field.address.AddressList;
-import org.apache.james.mime4j.field.address.parser.ParseException;
-
-public class AddressListField extends Field {
-    private AddressList addressList;
-    private ParseException parseException;
-
-    protected AddressListField(String name, String body, String raw, AddressList addressList, ParseException parseException) {
-        super(name, body, raw);
-        this.addressList = addressList;
-        this.parseException = parseException;
-    }
-
-    public AddressList getAddressList() {
-        return addressList;
-    }
-
-    public ParseException getParseException() {
-        return parseException;
-    }
-
-    public static class Parser implements FieldParser {
-        private static Log log = LogFactory.getLog(Parser.class);
-
-        public Field parse(final String name, final String body, final String raw) {
-            AddressList addressList = null;
-            ParseException parseException = null;
-            try {
-                addressList = AddressList.parse(body);
-            }
-            catch (ParseException e) {
-                if (log.isDebugEnabled()) {
-                    log.debug("Parsing value '" + body + "': "+ e.getMessage());
-                }
-                parseException = e;
-            }
-            return new AddressListField(name, body, raw, addressList, parseException);
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java
deleted file mode 100644
index 73d8d23..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field;
-
-
-
-/**
- * Represents a <code>Content-Transfer-Encoding</code> field.
- *
- * 
- * @version $Id: ContentTransferEncodingField.java,v 1.2 2004/10/02 12:41:11 ntherning Exp $
- */
-public class ContentTransferEncodingField extends Field {
-    /**
-     * The <code>7bit</code> encoding.
-     */
-    public static final String ENC_7BIT = "7bit";
-    /**
-     * The <code>8bit</code> encoding.
-     */
-    public static final String ENC_8BIT = "8bit";
-    /**
-     * The <code>binary</code> encoding.
-     */
-    public static final String ENC_BINARY = "binary";
-    /**
-     * The <code>quoted-printable</code> encoding.
-     */
-    public static final String ENC_QUOTED_PRINTABLE = "quoted-printable";
-    /**
-     * The <code>base64</code> encoding.
-     */
-    public static final String ENC_BASE64 = "base64";
-    
-    private String encoding;
-    
-    protected ContentTransferEncodingField(String name, String body, String raw, String encoding) {
-        super(name, body, raw);
-        this.encoding = encoding;
-    }
-
-    /**
-     * Gets the encoding defined in this field.
-     * 
-     * @return the encoding or an empty string if not set.
-     */
-    public String getEncoding() {
-        return encoding;
-    }
-    
-    /**
-     * Gets the encoding of the given field if. Returns the default 
-     * <code>7bit</code> if not set or if
-     * <code>f</code> is <code>null</code>.
-     * 
-     * @return the encoding.
-     */
-    public static String getEncoding(ContentTransferEncodingField f) {
-        if (f != null && f.getEncoding().length() != 0) {
-            return f.getEncoding();
-        }
-        return ENC_7BIT;
-    }
-    
-    public static class Parser implements FieldParser {
-        public Field parse(final String name, final String body, final String raw) {
-            final String encoding = body.trim().toLowerCase();
-            return new ContentTransferEncodingField(name, body, raw, encoding);
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTypeField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTypeField.java
deleted file mode 100644
index ad9f7f9..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTypeField.java
+++ /dev/null
@@ -1,259 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field;
-
-import java.io.StringReader;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-//BEGIN android-changed: Stubbing out logging
-import org.apache.james.mime4j.Log;
-import org.apache.james.mime4j.LogFactory;
-//END android-changed
-import org.apache.james.mime4j.field.contenttype.parser.ContentTypeParser;
-import org.apache.james.mime4j.field.contenttype.parser.ParseException;
-import org.apache.james.mime4j.field.contenttype.parser.TokenMgrError;
-
-/**
- * Represents a <code>Content-Type</code> field.
- *
- * <p>TODO: Remove dependency on Java 1.4 regexps</p>
- *
- *
- * @version $Id: ContentTypeField.java,v 1.6 2005/01/27 14:16:31 ntherning Exp $
- */
-public class ContentTypeField extends Field {
-
-    /**
-     * The prefix of all <code>multipart</code> MIME types.
-     */
-    public static final String TYPE_MULTIPART_PREFIX = "multipart/";
-    /**
-     * The <code>multipart/digest</code> MIME type.
-     */
-    public static final String TYPE_MULTIPART_DIGEST = "multipart/digest";
-    /**
-     * The <code>text/plain</code> MIME type.
-     */
-    public static final String TYPE_TEXT_PLAIN = "text/plain";
-    /**
-     * The <code>message/rfc822</code> MIME type.
-     */
-    public static final String TYPE_MESSAGE_RFC822 = "message/rfc822";
-    /**
-     * The name of the <code>boundary</code> parameter.
-     */
-    public static final String PARAM_BOUNDARY = "boundary";
-    /**
-     * The name of the <code>charset</code> parameter.
-     */
-    public static final String PARAM_CHARSET = "charset";
-
-    private String mimeType = "";
-    private Map<String, String> parameters = null;
-    private ParseException parseException;
-
-    protected ContentTypeField(String name, String body, String raw, String mimeType, Map<String, String> parameters, ParseException parseException) {
-        super(name, body, raw);
-        this.mimeType = mimeType;
-        this.parameters = parameters;
-        this.parseException = parseException;
-    }
-
-    /**
-     * Gets the exception that was raised during parsing of
-     * the field value, if any; otherwise, null.
-     */
-    public ParseException getParseException() {
-        return parseException;
-    }
-
-    /**
-     * Gets the MIME type defined in this Content-Type field.
-     *
-     * @return the MIME type or an empty string if not set.
-     */
-    public String getMimeType() {
-        return mimeType;
-    }
-
-    /**
-     * Gets the MIME type defined in the child's
-     * Content-Type field or derives a MIME type from the parent
-     * if child is <code>null</code> or hasn't got a MIME type value set.
-     * If child's MIME type is multipart but no boundary
-     * has been set the MIME type of child will be derived from
-     * the parent.
-     *
-     * @param child the child.
-     * @param parent the parent.
-     * @return the MIME type.
-     */
-    public static String getMimeType(ContentTypeField child,
-                                     ContentTypeField parent) {
-
-        if (child == null || child.getMimeType().length() == 0
-                || child.isMultipart() && child.getBoundary() == null) {
-
-            if (parent != null && parent.isMimeType(TYPE_MULTIPART_DIGEST)) {
-                return TYPE_MESSAGE_RFC822;
-            } else {
-                return TYPE_TEXT_PLAIN;
-            }
-        }
-
-        return child.getMimeType();
-    }
-
-    /**
-     * Gets the value of a parameter. Parameter names are case-insensitive.
-     *
-     * @param name the name of the parameter to get.
-     * @return the parameter value or <code>null</code> if not set.
-     */
-    public String getParameter(String name) {
-        return parameters != null
-                    ? parameters.get(name.toLowerCase())
-                    : null;
-    }
-
-    /**
-     * Gets all parameters.
-     *
-     * @return the parameters.
-     */
-    public Map<String, String> getParameters() {
-        if (parameters != null) {
-            return Collections.unmodifiableMap(parameters);
-        }
-        return Collections.emptyMap();
-    }
-
-    /**
-     * Gets the value of the <code>boundary</code> parameter if set.
-     *
-     * @return the <code>boundary</code> parameter value or <code>null</code>
-     *             if not set.
-     */
-    public String getBoundary() {
-        return getParameter(PARAM_BOUNDARY);
-    }
-
-    /**
-     * Gets the value of the <code>charset</code> parameter if set.
-     *
-     * @return the <code>charset</code> parameter value or <code>null</code>
-     *         if not set.
-     */
-    public String getCharset() {
-        return getParameter(PARAM_CHARSET);
-    }
-
-    /**
-     * Gets the value of the <code>charset</code> parameter if set for the
-     * given field. Returns the default <code>us-ascii</code> if not set or if
-     * <code>f</code> is <code>null</code>.
-     *
-     * @return the <code>charset</code> parameter value.
-     */
-    public static String getCharset(ContentTypeField f) {
-        if (f != null) {
-            if (f.getCharset() != null && f.getCharset().length() > 0) {
-                return f.getCharset();
-            }
-        }
-        return "us-ascii";
-    }
-
-    /**
-     * Determines if the MIME type of this field matches the given one.
-     *
-     * @param mimeType the MIME type to match against.
-     * @return <code>true</code> if the MIME type of this field matches,
-     *         <code>false</code> otherwise.
-     */
-    public boolean isMimeType(String mimeType) {
-        return this.mimeType.equalsIgnoreCase(mimeType);
-    }
-
-    /**
-     * Determines if the MIME type of this field is <code>multipart/*</code>.
-     *
-     * @return <code>true</code> if this field is has a <code>multipart/*</code>
-     *         MIME type, <code>false</code> otherwise.
-     */
-    public boolean isMultipart() {
-        return mimeType.startsWith(TYPE_MULTIPART_PREFIX);
-    }
-
-    public static class Parser implements FieldParser {
-        private static Log log = LogFactory.getLog(Parser.class);
-
-        public Field parse(final String name, final String body, final String raw) {
-            ParseException parseException = null;
-            String mimeType = "";
-            Map<String, String> parameters = null;
-
-            ContentTypeParser parser = new ContentTypeParser(new StringReader(body));
-            try {
-                parser.parseAll();
-            }
-            catch (ParseException e) {
-                if (log.isDebugEnabled()) {
-                    log.debug("Parsing value '" + body + "': "+ e.getMessage());
-                }
-                parseException = e;
-            }
-            catch (TokenMgrError e) {
-                if (log.isDebugEnabled()) {
-                    log.debug("Parsing value '" + body + "': "+ e.getMessage());
-                }
-                parseException = new ParseException(e.getMessage());
-            }
-
-            try {
-                final String type = parser.getType();
-                final String subType = parser.getSubType();
-
-                if (type != null && subType != null) {
-                    mimeType = (type + "/" + parser.getSubType()).toLowerCase();
-
-                    ArrayList<String> paramNames = parser.getParamNames();
-                    ArrayList<String> paramValues = parser.getParamValues();
-
-                    if (paramNames != null && paramValues != null) {
-                        for (int i = 0; i < paramNames.size() && i < paramValues.size(); i++) {
-                            if (parameters == null)
-                                parameters = new HashMap<String, String>((int)(paramNames.size() * 1.3 + 1));
-                            String paramName = paramNames.get(i).toLowerCase();
-                            String paramValue = paramValues.get(i);
-                            parameters.put(paramName, paramValue);
-                        }
-                    }
-                }
-            }
-            catch (NullPointerException npe) {
-            }
-            return new ContentTypeField(name, body, raw, mimeType, parameters, parseException);
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DateTimeField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DateTimeField.java
deleted file mode 100644
index 1e6c8e2..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DateTimeField.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field;
-
-//BEGIN android-changed: Stubbing out logging
-
-import com.android.voicemailomtp.mail.utils.LogUtils;
-
-import org.apache.james.mime4j.Log;
-import org.apache.james.mime4j.LogFactory;
-//END
-import org.apache.james.mime4j.field.datetime.DateTime;
-import org.apache.james.mime4j.field.datetime.parser.ParseException;
-
-import java.util.Date;
-
-public class DateTimeField extends Field {
-    private Date date;
-    private ParseException parseException;
-
-    protected DateTimeField(String name, String body, String raw, Date date, ParseException parseException) {
-        super(name, body, raw);
-        this.date = date;
-        this.parseException = parseException;
-    }
-
-    public Date getDate() {
-        return date;
-    }
-
-    public ParseException getParseException() {
-        return parseException;
-    }
-
-    public static class Parser implements FieldParser {
-        private static Log log = LogFactory.getLog(Parser.class);
-
-        public Field parse(final String name, String body, final String raw) {
-            Date date = null;
-            ParseException parseException = null;
-            //BEGIN android-changed
-            body = LogUtils.cleanUpMimeDate(body);
-            //END android-changed
-            try {
-                date = DateTime.parse(body).getDate();
-            }
-            catch (ParseException e) {
-                if (log.isDebugEnabled()) {
-                    log.debug("Parsing value '" + body + "': "+ e.getMessage());
-                }
-                parseException = e;
-            }
-            return new DateTimeField(name, body, raw, date, parseException);
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DefaultFieldParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DefaultFieldParser.java
deleted file mode 100644
index 3695afe..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DefaultFieldParser.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- *  Copyright 2006 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field;
-
-public class DefaultFieldParser extends DelegatingFieldParser {
-    
-    public DefaultFieldParser() {
-        setFieldParser(Field.CONTENT_TRANSFER_ENCODING, new ContentTransferEncodingField.Parser());
-        setFieldParser(Field.CONTENT_TYPE, new ContentTypeField.Parser());
-        
-        final DateTimeField.Parser dateTimeParser = new DateTimeField.Parser();
-        setFieldParser(Field.DATE, dateTimeParser);
-        setFieldParser(Field.RESENT_DATE, dateTimeParser);
-        
-        final MailboxListField.Parser mailboxListParser = new MailboxListField.Parser();
-        setFieldParser(Field.FROM, mailboxListParser);
-        setFieldParser(Field.RESENT_FROM, mailboxListParser);
-        
-        final MailboxField.Parser mailboxParser = new MailboxField.Parser();
-        setFieldParser(Field.SENDER, mailboxParser);
-        setFieldParser(Field.RESENT_SENDER, mailboxParser);
-        
-        final AddressListField.Parser addressListParser = new AddressListField.Parser();
-        setFieldParser(Field.TO, addressListParser);
-        setFieldParser(Field.RESENT_TO, addressListParser);
-        setFieldParser(Field.CC, addressListParser);
-        setFieldParser(Field.RESENT_CC, addressListParser);
-        setFieldParser(Field.BCC, addressListParser);
-        setFieldParser(Field.RESENT_BCC, addressListParser);
-        setFieldParser(Field.REPLY_TO, addressListParser);
-    }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DelegatingFieldParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DelegatingFieldParser.java
deleted file mode 100644
index 32b69ec..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DelegatingFieldParser.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- *  Copyright 2006 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public class DelegatingFieldParser implements FieldParser {
-
-    private Map<String, FieldParser> parsers = new HashMap<String, FieldParser>();
-    private FieldParser defaultParser = new UnstructuredField.Parser();
-
-    /**
-     * Sets the parser used for the field named <code>name</code>.
-     * @param name the name of the field
-     * @param parser the parser for fields named <code>name</code>
-     */
-    public void setFieldParser(final String name, final FieldParser parser) {
-        parsers.put(name.toLowerCase(), parser);
-    }
-
-    public FieldParser getParser(final String name) {
-        final FieldParser field = parsers.get(name.toLowerCase());
-        if(field==null) {
-            return defaultParser;
-        }
-        return field;
-    }
-
-    public Field parse(final String name, final String body, final String raw) {
-        final FieldParser parser = getParser(name);
-        return parser.parse(name, body, raw);
-    }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/Field.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/Field.java
deleted file mode 100644
index 4dea5c5..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/Field.java
+++ /dev/null
@@ -1,192 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * The base class of all field classes.
- *
- * 
- * @version $Id: Field.java,v 1.6 2004/10/25 07:26:46 ntherning Exp $
- */
-public abstract class Field {
-    public static final String SENDER = "Sender";
-    public static final String FROM = "From";
-    public static final String TO = "To";
-    public static final String CC = "Cc";
-    public static final String BCC = "Bcc";
-    public static final String REPLY_TO = "Reply-To";
-    public static final String RESENT_SENDER = "Resent-Sender";
-    public static final String RESENT_FROM = "Resent-From";
-    public static final String RESENT_TO = "Resent-To";
-    public static final String RESENT_CC = "Resent-Cc";
-    public static final String RESENT_BCC = "Resent-Bcc";
-
-    public static final String DATE = "Date";
-    public static final String RESENT_DATE = "Resent-Date";
-
-    public static final String SUBJECT = "Subject";
-    public static final String CONTENT_TYPE = "Content-Type";
-    public static final String CONTENT_TRANSFER_ENCODING = 
-                                        "Content-Transfer-Encoding";
-    
-    private static final String FIELD_NAME_PATTERN = 
-        "^([\\x21-\\x39\\x3b-\\x7e]+)[ \t]*:";
-    private static final Pattern fieldNamePattern = 
-        Pattern.compile(FIELD_NAME_PATTERN);
-        
-    private static final DefaultFieldParser parser = new DefaultFieldParser();
-    
-    private final String name;
-    private final String body;
-    private final String raw;
-    
-    protected Field(final String name, final String body, final String raw) {
-        this.name = name;
-        this.body = body;
-        this.raw = raw;
-    }
-    
-    /**
-     * Parses the given string and returns an instance of the 
-     * <code>Field</code> class. The type of the class returned depends on
-     * the field name:
-     * <table>
-     *      <tr>
-     *          <td><em>Field name</em></td><td><em>Class returned</em></td>
-     *          <td>Content-Type</td><td>org.apache.james.mime4j.field.ContentTypeField</td>
-     *          <td>other</td><td>org.apache.james.mime4j.field.UnstructuredField</td>
-     *      </tr>
-     * </table>
-     * 
-     * @param s the string to parse.
-     * @return a <code>Field</code> instance.
-     * @throws IllegalArgumentException on parse errors.
-     */
-    public static Field parse(final String raw) {
-        
-        /*
-         * Unfold the field.
-         */
-        final String unfolded = raw.replaceAll("\r|\n", "");
-        
-        /*
-         * Split into name and value.
-         */
-        final Matcher fieldMatcher = fieldNamePattern.matcher(unfolded);
-        if (!fieldMatcher.find()) {
-            throw new IllegalArgumentException("Invalid field in string");
-        }
-        final String name = fieldMatcher.group(1);
-        
-        String body = unfolded.substring(fieldMatcher.end());
-        if (body.length() > 0 && body.charAt(0) == ' ') {
-            body = body.substring(1);
-        }
-        
-        return parser.parse(name, body, raw);
-    }
-    
-    /**
-     * Gets the default parser used to parse fields.
-     * @return the default field parser
-     */
-    public static DefaultFieldParser getParser() {
-        return parser;
-    }
-    
-    /**
-     * Gets the name of the field (<code>Subject</code>, 
-     * <code>From</code>, etc).
-     * 
-     * @return the field name.
-     */
-    public String getName() {
-        return name;
-    }
-    
-    /**
-     * Gets the original raw field string.
-     * 
-     * @return the original raw field string.
-     */
-    public String getRaw() {
-        return raw;
-    }
-    
-    /**
-     * Gets the unfolded, unparsed and possibly encoded (see RFC 2047) field 
-     * body string.
-     * 
-     * @return the unfolded unparsed field body string.
-     */
-    public String getBody() {
-        return body;
-    }
-    
-    /**
-     * Determines if this is a <code>Content-Type</code> field.
-     * 
-     * @return <code>true</code> if this is a <code>Content-Type</code> field,
-     *         <code>false</code> otherwise.
-     */
-    public boolean isContentType() {
-        return CONTENT_TYPE.equalsIgnoreCase(name);
-    }
-    
-    /**
-     * Determines if this is a <code>Subject</code> field.
-     * 
-     * @return <code>true</code> if this is a <code>Subject</code> field,
-     *         <code>false</code> otherwise.
-     */
-    public boolean isSubject() {
-        return SUBJECT.equalsIgnoreCase(name);
-    }
-    
-    /**
-     * Determines if this is a <code>From</code> field.
-     * 
-     * @return <code>true</code> if this is a <code>From</code> field,
-     *         <code>false</code> otherwise.
-     */
-    public boolean isFrom() {
-        return FROM.equalsIgnoreCase(name);
-    }
-    
-    /**
-     * Determines if this is a <code>To</code> field.
-     * 
-     * @return <code>true</code> if this is a <code>To</code> field,
-     *         <code>false</code> otherwise.
-     */
-    public boolean isTo() {
-        return TO.equalsIgnoreCase(name);
-    }
-    
-    /**
-     * @see #getRaw()
-     */
-    public String toString() {
-        return raw;
-    }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/FieldParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/FieldParser.java
deleted file mode 100644
index 78aaf13..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/FieldParser.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- *  Copyright 2006 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field;
-
-public interface FieldParser {
-    
-    Field parse(final String name, final String body, final String raw);
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxField.java
deleted file mode 100644
index f159800..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxField.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field;
-
-//BEGIN android-changed: Stubbing out logging
-import org.apache.james.mime4j.Log;
-import org.apache.james.mime4j.LogFactory;
-//END android-changed
-import org.apache.james.mime4j.field.address.AddressList;
-import org.apache.james.mime4j.field.address.Mailbox;
-import org.apache.james.mime4j.field.address.MailboxList;
-import org.apache.james.mime4j.field.address.parser.ParseException;
-
-public class MailboxField extends Field {
-    private final Mailbox mailbox;
-    private final ParseException parseException;
-
-    protected MailboxField(final String name, final String body, final String raw, final Mailbox mailbox, final ParseException parseException) {
-        super(name, body, raw);
-        this.mailbox = mailbox;
-        this.parseException = parseException;
-    }
-
-    public Mailbox getMailbox() {
-        return mailbox;
-    }
-
-    public ParseException getParseException() {
-        return parseException;
-    }
-    
-    public static class Parser implements FieldParser {
-        private static Log log = LogFactory.getLog(Parser.class);
-
-        public Field parse(final String name, final String body, final String raw) {
-            Mailbox mailbox = null;
-            ParseException parseException = null;
-            try {
-                MailboxList mailboxList = AddressList.parse(body).flatten();
-                if (mailboxList.size() > 0) {
-                    mailbox = mailboxList.get(0);
-                }
-            }
-            catch (ParseException e) {
-                if (log.isDebugEnabled()) {
-                    log.debug("Parsing value '" + body + "': "+ e.getMessage());
-                }
-                parseException = e;
-            }
-            return new MailboxField(name, body, raw, mailbox, parseException);
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxListField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxListField.java
deleted file mode 100644
index 23378d4..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxListField.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field;
-
-//BEGIN android-changed: Stubbing out logging
-import org.apache.james.mime4j.Log;
-import org.apache.james.mime4j.LogFactory;
-//END android-changed
-import org.apache.james.mime4j.field.address.AddressList;
-import org.apache.james.mime4j.field.address.MailboxList;
-import org.apache.james.mime4j.field.address.parser.ParseException;
-
-public class MailboxListField extends Field {
-    
-    private MailboxList mailboxList;
-    private ParseException parseException;
-
-    protected MailboxListField(final String name, final String body, final String raw, final MailboxList mailboxList, final ParseException parseException) {
-        super(name, body, raw);
-        this.mailboxList = mailboxList;
-        this.parseException = parseException;
-    }
-
-    public MailboxList getMailboxList() {
-        return mailboxList;
-    }
-
-    public ParseException getParseException() {
-        return parseException;
-    }
-    
-    public static class Parser implements FieldParser {
-        private static Log log = LogFactory.getLog(Parser.class);
-
-        public Field parse(final String name, final String body, final String raw) {
-            MailboxList mailboxList = null;
-            ParseException parseException = null;
-            try {
-                mailboxList = AddressList.parse(body).flatten();
-            }
-            catch (ParseException e) {
-                if (log.isDebugEnabled()) {
-                    log.debug("Parsing value '" + body + "': "+ e.getMessage());
-                }
-                parseException = e;
-            }
-            return new MailboxListField(name, body, raw, mailboxList, parseException);
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/UnstructuredField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/UnstructuredField.java
deleted file mode 100644
index 6084e44..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/UnstructuredField.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field;
-
-import org.apache.james.mime4j.decoder.DecoderUtil;
-
-
-/**
- * Simple unstructured field such as <code>Subject</code>.
- *
- * 
- * @version $Id: UnstructuredField.java,v 1.3 2004/10/25 07:26:46 ntherning Exp $
- */
-public class UnstructuredField extends Field {
-    private String value;
-    
-    protected UnstructuredField(String name, String body, String raw, String value) {
-        super(name, body, raw);
-        this.value = value;
-    }
-
-    public String getValue() {
-        return value;
-    }
-
-    public static class Parser implements FieldParser {
-        public Field parse(final String name, final String body, final String raw) {
-            final String value = DecoderUtil.decodeEncodedWords(body);
-            return new UnstructuredField(name, body, raw, value);
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Address.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Address.java
deleted file mode 100644
index 3e24e91..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Address.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field.address;
-
-import java.util.ArrayList;
-
-/**
- * The abstract base for classes that represent RFC2822 addresses.
- * This includes groups and mailboxes.
- *
- * Currently, no public methods are introduced on this class.
- *
- *
- */
-public abstract class Address {
-
-	/**
-	 * Adds any mailboxes represented by this address
-	 * into the given ArrayList. Note that this method
-	 * has default (package) access, so a doAddMailboxesTo
-	 * method is needed to allow the behavior to be
-	 * overridden by subclasses.
-	 */
-	final void addMailboxesTo(ArrayList<Address> results) {
-		doAddMailboxesTo(results);
-	}
-
-	/**
-	 * Adds any mailboxes represented by this address
-	 * into the given ArrayList. Must be overridden by
-	 * concrete subclasses.
-	 */
-	protected abstract void doAddMailboxesTo(ArrayList<Address> results);
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/AddressList.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/AddressList.java
deleted file mode 100644
index 1829e79..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/AddressList.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field.address;
-
-import org.apache.james.mime4j.field.address.parser.AddressListParser;
-import org.apache.james.mime4j.field.address.parser.ParseException;
-
-import java.io.StringReader;
-import java.util.ArrayList;
-
-/**
- * An immutable, random-access list of Address objects.
- *
- *
- */
-public class AddressList {
-
-	private ArrayList<Address> addresses;
-
-	/**
-	 * @param addresses An ArrayList that contains only Address objects.
-	 * @param dontCopy true iff it is not possible for the addresses ArrayList to be modified by someone else.
-	 */
-	public AddressList(ArrayList<Address> addresses, boolean dontCopy) {
-		if (addresses != null)
-			this.addresses = (dontCopy ? addresses : new ArrayList<Address>(addresses));
-		else
-			this.addresses = new ArrayList<Address>(0);
-	}
-
-	/**
-	 * The number of elements in this list.
-	 */
-	public int size() {
-		return addresses.size();
-	}
-
-	/**
-	 * Gets an address.
-	 */
-	public Address get(int index) {
-		if (0 > index || size() <= index)
-			throw new IndexOutOfBoundsException();
-		return addresses.get(index);
-	}
-
-	/**
-	 * Returns a flat list of all mailboxes represented
-	 * in this address list. Use this if you don't care
-	 * about grouping.
-	 */
-	public MailboxList flatten() {
-		// in the common case, all addresses are mailboxes
-		boolean groupDetected = false;
-		for (int i = 0; i < size(); i++) {
-			if (!(get(i) instanceof Mailbox)) {
-				groupDetected = true;
-				break;
-			}
-		}
-
-		if (!groupDetected)
-			return new MailboxList(addresses, true);
-
-		ArrayList<Address> results = new ArrayList<Address>();
-		for (int i = 0; i < size(); i++) {
-			Address addr = get(i);
-			addr.addMailboxesTo(results);
-		}
-
-		// copy-on-construct this time, because subclasses
-		// could have held onto a reference to the results
-		return new MailboxList(results, false);
-	}
-
-	/**
-	 * Dumps a representation of this address list to
-	 * stdout, for debugging purposes.
-	 */
-	public void print() {
-		for (int i = 0; i < size(); i++) {
-			Address addr = get(i);
-			System.out.println(addr.toString());
-		}
-	}
-
-	/**
-	 * Parse the address list string, such as the value
-	 * of a From, To, Cc, Bcc, Sender, or Reply-To
-	 * header.
-	 *
-	 * The string MUST be unfolded already.
-	 */
-	public static AddressList parse(String rawAddressList) throws ParseException {
-		AddressListParser parser = new AddressListParser(new StringReader(rawAddressList));
-		return Builder.getInstance().buildAddressList(parser.parse());
-	}
-
-	/**
-	 * Test console.
-	 */
-	public static void main(String[] args) throws Exception {
-		java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(System.in));
-		while (true) {
-			try {
-				System.out.print("> ");
-				String line = reader.readLine();
-				if (line.length() == 0 || line.toLowerCase().equals("exit") || line.toLowerCase().equals("quit")) {
-					System.out.println("Goodbye.");
-					return;
-				}
-				AddressList list = parse(line);
-				list.print();
-			}
-			catch(Exception e) {
-				e.printStackTrace();
-				Thread.sleep(300);
-			}
-		}
-	}
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Builder.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Builder.java
deleted file mode 100644
index 3bcd15b..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Builder.java
+++ /dev/null
@@ -1,243 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field.address;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-
-import org.apache.james.mime4j.decoder.DecoderUtil;
-import org.apache.james.mime4j.field.address.parser.ASTaddr_spec;
-import org.apache.james.mime4j.field.address.parser.ASTaddress;
-import org.apache.james.mime4j.field.address.parser.ASTaddress_list;
-import org.apache.james.mime4j.field.address.parser.ASTangle_addr;
-import org.apache.james.mime4j.field.address.parser.ASTdomain;
-import org.apache.james.mime4j.field.address.parser.ASTgroup_body;
-import org.apache.james.mime4j.field.address.parser.ASTlocal_part;
-import org.apache.james.mime4j.field.address.parser.ASTmailbox;
-import org.apache.james.mime4j.field.address.parser.ASTname_addr;
-import org.apache.james.mime4j.field.address.parser.ASTphrase;
-import org.apache.james.mime4j.field.address.parser.ASTroute;
-import org.apache.james.mime4j.field.address.parser.Node;
-import org.apache.james.mime4j.field.address.parser.SimpleNode;
-import org.apache.james.mime4j.field.address.parser.Token;
-
-/**
- * Transforms the JJTree-generated abstract syntax tree
- * into a graph of org.apache.james.mime4j.field.address objects.
- *
- *
- */
-class Builder {
-
-	private static Builder singleton = new Builder();
-
-	public static Builder getInstance() {
-		return singleton;
-	}
-
-
-
-	public AddressList buildAddressList(ASTaddress_list node) {
-		ArrayList<Address> list = new ArrayList<Address>();
-		for (int i = 0; i < node.jjtGetNumChildren(); i++) {
-			ASTaddress childNode = (ASTaddress) node.jjtGetChild(i);
-			Address address = buildAddress(childNode);
-			list.add(address);
-		}
-		return new AddressList(list, true);
-	}
-
-	private Address buildAddress(ASTaddress node) {
-		ChildNodeIterator it = new ChildNodeIterator(node);
-		Node n = it.nextNode();
-		if (n instanceof ASTaddr_spec) {
-			return buildAddrSpec((ASTaddr_spec)n);
-		}
-		else if (n instanceof ASTangle_addr) {
-			return buildAngleAddr((ASTangle_addr)n);
-		}
-		else if (n instanceof ASTphrase) {
-			String name = buildString((ASTphrase)n, false);
-			Node n2 = it.nextNode();
-			if (n2 instanceof ASTgroup_body) {
-				return new Group(name, buildGroupBody((ASTgroup_body)n2));
-			}
-			else if (n2 instanceof ASTangle_addr) {
-                name = DecoderUtil.decodeEncodedWords(name);
-				return new NamedMailbox(name, buildAngleAddr((ASTangle_addr)n2));
-			}
-			else {
-				throw new IllegalStateException();
-			}
-		}
-		else {
-			throw new IllegalStateException();
-		}
-	}
-
-
-
-	private MailboxList buildGroupBody(ASTgroup_body node) {
-		ArrayList<Address> results = new ArrayList<Address>();
-		ChildNodeIterator it = new ChildNodeIterator(node);
-		while (it.hasNext()) {
-			Node n = it.nextNode();
-			if (n instanceof ASTmailbox)
-				results.add(buildMailbox((ASTmailbox)n));
-			else
-				throw new IllegalStateException();
-		}
-		return new MailboxList(results, true);
-	}
-
-	private Mailbox buildMailbox(ASTmailbox node) {
-		ChildNodeIterator it = new ChildNodeIterator(node);
-		Node n = it.nextNode();
-		if (n instanceof ASTaddr_spec) {
-			return buildAddrSpec((ASTaddr_spec)n);
-		}
-		else if (n instanceof ASTangle_addr) {
-			return buildAngleAddr((ASTangle_addr)n);
-		}
-		else if (n instanceof ASTname_addr) {
-			return buildNameAddr((ASTname_addr)n);
-		}
-		else {
-			throw new IllegalStateException();
-		}
-	}
-
-	private NamedMailbox buildNameAddr(ASTname_addr node) {
-		ChildNodeIterator it = new ChildNodeIterator(node);
-		Node n = it.nextNode();
-		String name;
-		if (n instanceof ASTphrase) {
-			name = buildString((ASTphrase)n, false);
-		}
-		else {
-			throw new IllegalStateException();
-		}
-
-		n = it.nextNode();
-		if (n instanceof ASTangle_addr) {
-            name = DecoderUtil.decodeEncodedWords(name);
-			return new NamedMailbox(name, buildAngleAddr((ASTangle_addr) n));
-		}
-		else {
-			throw new IllegalStateException();
-		}
-	}
-
-	private Mailbox buildAngleAddr(ASTangle_addr node) {
-		ChildNodeIterator it = new ChildNodeIterator(node);
-		DomainList route = null;
-		Node n = it.nextNode();
-		if (n instanceof ASTroute) {
-			route = buildRoute((ASTroute)n);
-			n = it.nextNode();
-		}
-		else if (n instanceof ASTaddr_spec)
-			; // do nothing
-		else
-			throw new IllegalStateException();
-
-		if (n instanceof ASTaddr_spec)
-			return buildAddrSpec(route, (ASTaddr_spec)n);
-		else
-			throw new IllegalStateException();
-	}
-
-	private DomainList buildRoute(ASTroute node) {
-		ArrayList<String> results = new ArrayList<String>(node.jjtGetNumChildren());
-		ChildNodeIterator it = new ChildNodeIterator(node);
-		while (it.hasNext()) {
-			Node n = it.nextNode();
-			if (n instanceof ASTdomain)
-				results.add(buildString((ASTdomain)n, true));
-			else
-				throw new IllegalStateException();
-		}
-		return new DomainList(results, true);
-	}
-
-	private Mailbox buildAddrSpec(ASTaddr_spec node) {
-		return buildAddrSpec(null, node);
-	}
-	private Mailbox buildAddrSpec(DomainList route, ASTaddr_spec node) {
-		ChildNodeIterator it = new ChildNodeIterator(node);
-		String localPart = buildString((ASTlocal_part)it.nextNode(), true);
-		String domain = buildString((ASTdomain)it.nextNode(), true);
-		return new Mailbox(route, localPart, domain);
-	}
-
-
-	private String buildString(SimpleNode node, boolean stripSpaces) {
-		Token head = node.firstToken;
-		Token tail = node.lastToken;
-		StringBuffer out = new StringBuffer();
-
-		while (head != tail) {
-			out.append(head.image);
-			head = head.next;
-			if (!stripSpaces)
-				addSpecials(out, head.specialToken);
-		}
-		out.append(tail.image);
-
-		return out.toString();
-	}
-
-	private void addSpecials(StringBuffer out, Token specialToken) {
-		if (specialToken != null) {
-			addSpecials(out, specialToken.specialToken);
-			out.append(specialToken.image);
-		}
-	}
-
-	private static class ChildNodeIterator implements Iterator<Node> {
-
-		private SimpleNode simpleNode;
-		private int index;
-		private int len;
-
-		public ChildNodeIterator(SimpleNode simpleNode) {
-			this.simpleNode = simpleNode;
-			this.len = simpleNode.jjtGetNumChildren();
-			this.index = 0;
-		}
-
-		public void remove() {
-			throw new UnsupportedOperationException();
-		}
-
-		public boolean hasNext() {
-			return index < len;
-		}
-
-		public Node next() {
-			return nextNode();
-		}
-
-		public Node nextNode() {
-			return simpleNode.jjtGetChild(index++);
-		}
-
-	}
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/DomainList.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/DomainList.java
deleted file mode 100644
index 49b0f3b..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/DomainList.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field.address;
-
-import java.util.ArrayList;
-
-/**
- * An immutable, random-access list of Strings (that
- * are supposedly domain names or domain literals).
- *
- *
- */
-public class DomainList {
-	private ArrayList<String> domains;
-
-	/**
-	 * @param domains An ArrayList that contains only String objects.
-	 * @param dontCopy true iff it is not possible for the domains ArrayList to be modified by someone else.
-	 */
-	public DomainList(ArrayList<String> domains, boolean dontCopy) {
-		if (domains != null)
-			this.domains = (dontCopy ? domains : new ArrayList<String>(domains));
-		else
-			this.domains = new ArrayList<String>(0);
-	}
-
-	/**
-	 * The number of elements in this list.
-	 */
-	public int size() {
-		return domains.size();
-	}
-
-	/**
-	 * Gets the domain name or domain literal at the
-	 * specified index.
-	 * @throws IndexOutOfBoundsException If index is &lt; 0 or &gt;= size().
-	 */
-	public String get(int index) {
-		if (0 > index || size() <= index)
-			throw new IndexOutOfBoundsException();
-		return domains.get(index);
-	}
-
-	/**
-	 * Returns the list of domains formatted as a route
-	 * string (not including the trailing ':').
-	 */
-	public String toRouteString() {
-		StringBuffer out = new StringBuffer();
-		for (int i = 0; i < domains.size(); i++) {
-			out.append("@");
-			out.append(get(i));
-			if (i + 1 < domains.size())
-				out.append(",");
-		}
-		return out.toString();
-	}
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Group.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Group.java
deleted file mode 100644
index c0ab7f7..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Group.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field.address;
-
-import java.util.ArrayList;
-
-/**
- * A named group of zero or more mailboxes.
- *
- *
- */
-public class Group extends Address {
-	private String name;
-	private MailboxList mailboxList;
-
-	/**
-	 * @param name The group name.
-	 * @param mailboxes The mailboxes in this group.
-	 */
-	public Group(String name, MailboxList mailboxes) {
-		this.name = name;
-		this.mailboxList = mailboxes;
-	}
-
-	/**
-	 * Returns the group name.
-	 */
-	public String getName() {
-		return name;
-	}
-
-	/**
-	 * Returns the mailboxes in this group.
-	 */
-	public MailboxList getMailboxes() {
-		return mailboxList;
-	}
-
-	@Override
-	public String toString() {
-		StringBuffer buf = new StringBuffer();
-		buf.append(name);
-		buf.append(":");
-		for (int i = 0; i < mailboxList.size(); i++) {
-			buf.append(mailboxList.get(i).toString());
-			if (i + 1 < mailboxList.size())
-				buf.append(",");
-		}
-		buf.append(";");
-		return buf.toString();
-	}
-
-	@Override
-	protected void doAddMailboxesTo(ArrayList<Address> results) {
-		for (int i = 0; i < mailboxList.size(); i++)
-			results.add(mailboxList.get(i));
-	}
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Mailbox.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Mailbox.java
deleted file mode 100644
index 25f2548..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Mailbox.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field.address;
-
-import java.util.ArrayList;
-
-/**
- * Represents a single e-mail address.
- *
- *
- */
-public class Mailbox extends Address {
-	private DomainList route;
-	private String localPart;
-	private String domain;
-
-	/**
-	 * Creates a mailbox without a route. Routes are obsolete.
-	 * @param localPart The part of the e-mail address to the left of the "@".
-	 * @param domain The part of the e-mail address to the right of the "@".
-	 */
-	public Mailbox(String localPart, String domain) {
-		this(null, localPart, domain);
-	}
-
-	/**
-	 * Creates a mailbox with a route. Routes are obsolete.
-	 * @param route The zero or more domains that make up the route. Can be null.
-	 * @param localPart The part of the e-mail address to the left of the "@".
-	 * @param domain The part of the e-mail address to the right of the "@".
-	 */
-	public Mailbox(DomainList route, String localPart, String domain) {
-		this.route = route;
-		this.localPart = localPart;
-		this.domain = domain;
-	}
-
-	/**
-	 * Returns the route list.
-	 */
-	public DomainList getRoute() {
-		return route;
-	}
-
-	/**
-	 * Returns the left part of the e-mail address
-	 * (before "@").
-	 */
-	public String getLocalPart() {
-		return localPart;
-	}
-
-	/**
-	 * Returns the right part of the e-mail address
-	 * (after "@").
-	 */
-	public String getDomain() {
-		return domain;
-	}
-
-	/**
-	 * Formats the address as a string, not including
-	 * the route.
-	 *
-	 * @see #getAddressString(boolean)
-	 */
-	public String getAddressString() {
-		return getAddressString(false);
-	}
-
-	/**
-	 * Note that this value may not be usable
-	 * for transport purposes, only display purposes.
-	 *
-	 * For example, if the unparsed address was
-	 *
-	 *   <"Joe Cheng"@joecheng.com>
-	 *
-	 * this method would return
-	 *
-	 *   <Joe Cheng@joecheng.com>
-	 *
-	 * which is not valid for transport; the local part
-	 * would need to be re-quoted.
-	 *
-	 * @param includeRoute true if the route should be included if it exists.
-	 */
-	public String getAddressString(boolean includeRoute) {
-		return "<" + (!includeRoute || route == null ? "" : route.toRouteString() + ":")
-			+ localPart
-			+ (domain == null ? "" : "@")
-			+ domain + ">";
-	}
-
-	@Override
-	protected final void doAddMailboxesTo(ArrayList<Address> results) {
-		results.add(this);
-	}
-
-	@Override
-	public String toString() {
-		return getAddressString();
-	}
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/MailboxList.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/MailboxList.java
deleted file mode 100644
index 2c9efb3..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/MailboxList.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field.address;
-
-import java.util.ArrayList;
-
-/**
- * An immutable, random-access list of Mailbox objects.
- *
- *
- */
-public class MailboxList {
-
-	private ArrayList<Address> mailboxes;
-
-	/**
-	 * @param mailboxes An ArrayList that contains only Mailbox objects.
-	 * @param dontCopy true iff it is not possible for the mailboxes ArrayList to be modified by someone else.
-	 */
-	public MailboxList(ArrayList<Address> mailboxes, boolean dontCopy) {
-		if (mailboxes != null)
-			this.mailboxes = (dontCopy ? mailboxes : new ArrayList<Address>(mailboxes));
-		else
-			this.mailboxes = new ArrayList<Address>(0);
-	}
-
-	/**
-	 * The number of elements in this list.
-	 */
-	public int size() {
-		return mailboxes.size();
-	}
-
-	/**
-	 * Gets an address.
-	 */
-	public Mailbox get(int index) {
-		if (0 > index || size() <= index)
-			throw new IndexOutOfBoundsException();
-		return (Mailbox)mailboxes.get(index);
-	}
-
-	/**
-	 * Dumps a representation of this mailbox list to
-	 * stdout, for debugging purposes.
-	 */
-	public void print() {
-		for (int i = 0; i < size(); i++) {
-			Mailbox mailbox = get(i);
-			System.out.println(mailbox.toString());
-		}
-	}
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/NamedMailbox.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/NamedMailbox.java
deleted file mode 100644
index 4b83060..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/NamedMailbox.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field.address;
-
-/**
- * A Mailbox that has a name/description.
- *
- *
- */
-public class NamedMailbox extends Mailbox {
-	private String name;
-
-	/**
-	 * @see Mailbox#Mailbox(String, String)
-	 */
-	public NamedMailbox(String name, String localPart, String domain) {
-		super(localPart, domain);
-		this.name = name;
-	}
-
-	/**
-	 * @see Mailbox#Mailbox(DomainList, String, String)
-	 */
-	public NamedMailbox(String name, DomainList route, String localPart, String domain) {
-		super(route, localPart, domain);
-		this.name = name;
-	}
-
-	/**
-	 * Creates a named mailbox based on an unnamed mailbox.
-	 */
-	public NamedMailbox(String name, Mailbox baseMailbox) {
-		super(baseMailbox.getRoute(), baseMailbox.getLocalPart(), baseMailbox.getDomain());
-		this.name = name;
-	}
-
-	/**
-	 * Returns the name of the mailbox.
-	 */
-	public String getName() {
-		return this.name;
-	}
-
-	/**
-	 * Same features (or problems) as Mailbox.getAddressString(boolean),
-	 * only more so.
-	 *
-	 * @see Mailbox#getAddressString(boolean)
-	 */
-	@Override
-	public String getAddressString(boolean includeRoute) {
-		return (name == null ? "" : name + " ") + super.getAddressString(includeRoute);
-	}
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java
deleted file mode 100644
index 4d56d00..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Generated By:JJTree: Do not edit this line. ASTaddr_spec.java */
-
-package org.apache.james.mime4j.field.address.parser;
-
-public class ASTaddr_spec extends SimpleNode {
-  public ASTaddr_spec(int id) {
-    super(id);
-  }
-
-  public ASTaddr_spec(AddressListParser p, int id) {
-    super(p, id);
-  }
-
-
-  /** Accept the visitor. **/
-  public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
-    return visitor.visit(this, data);
-  }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java
deleted file mode 100644
index 47bdeda..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Generated By:JJTree: Do not edit this line. ASTaddress.java */
-
-package org.apache.james.mime4j.field.address.parser;
-
-public class ASTaddress extends SimpleNode {
-  public ASTaddress(int id) {
-    super(id);
-  }
-
-  public ASTaddress(AddressListParser p, int id) {
-    super(p, id);
-  }
-
-
-  /** Accept the visitor. **/
-  public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
-    return visitor.visit(this, data);
-  }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java
deleted file mode 100644
index 737840e..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Generated By:JJTree: Do not edit this line. ASTaddress_list.java */
-
-package org.apache.james.mime4j.field.address.parser;
-
-public class ASTaddress_list extends SimpleNode {
-  public ASTaddress_list(int id) {
-    super(id);
-  }
-
-  public ASTaddress_list(AddressListParser p, int id) {
-    super(p, id);
-  }
-
-
-  /** Accept the visitor. **/
-  public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
-    return visitor.visit(this, data);
-  }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java
deleted file mode 100644
index 8cb8f42..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Generated By:JJTree: Do not edit this line. ASTangle_addr.java */
-
-package org.apache.james.mime4j.field.address.parser;
-
-public class ASTangle_addr extends SimpleNode {
-  public ASTangle_addr(int id) {
-    super(id);
-  }
-
-  public ASTangle_addr(AddressListParser p, int id) {
-    super(p, id);
-  }
-
-
-  /** Accept the visitor. **/
-  public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
-    return visitor.visit(this, data);
-  }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java
deleted file mode 100644
index b526643..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Generated By:JJTree: Do not edit this line. ASTdomain.java */
-
-package org.apache.james.mime4j.field.address.parser;
-
-public class ASTdomain extends SimpleNode {
-  public ASTdomain(int id) {
-    super(id);
-  }
-
-  public ASTdomain(AddressListParser p, int id) {
-    super(p, id);
-  }
-
-
-  /** Accept the visitor. **/
-  public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
-    return visitor.visit(this, data);
-  }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java
deleted file mode 100644
index f6017b9..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Generated By:JJTree: Do not edit this line. ASTgroup_body.java */
-
-package org.apache.james.mime4j.field.address.parser;
-
-public class ASTgroup_body extends SimpleNode {
-  public ASTgroup_body(int id) {
-    super(id);
-  }
-
-  public ASTgroup_body(AddressListParser p, int id) {
-    super(p, id);
-  }
-
-
-  /** Accept the visitor. **/
-  public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
-    return visitor.visit(this, data);
-  }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java
deleted file mode 100644
index 5c244fa..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Generated By:JJTree: Do not edit this line. ASTlocal_part.java */
-
-package org.apache.james.mime4j.field.address.parser;
-
-public class ASTlocal_part extends SimpleNode {
-  public ASTlocal_part(int id) {
-    super(id);
-  }
-
-  public ASTlocal_part(AddressListParser p, int id) {
-    super(p, id);
-  }
-
-
-  /** Accept the visitor. **/
-  public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
-    return visitor.visit(this, data);
-  }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java
deleted file mode 100644
index aeb469d..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Generated By:JJTree: Do not edit this line. ASTmailbox.java */
-
-package org.apache.james.mime4j.field.address.parser;
-
-public class ASTmailbox extends SimpleNode {
-  public ASTmailbox(int id) {
-    super(id);
-  }
-
-  public ASTmailbox(AddressListParser p, int id) {
-    super(p, id);
-  }
-
-
-  /** Accept the visitor. **/
-  public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
-    return visitor.visit(this, data);
-  }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java
deleted file mode 100644
index 846c731..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Generated By:JJTree: Do not edit this line. ASTname_addr.java */
-
-package org.apache.james.mime4j.field.address.parser;
-
-public class ASTname_addr extends SimpleNode {
-  public ASTname_addr(int id) {
-    super(id);
-  }
-
-  public ASTname_addr(AddressListParser p, int id) {
-    super(p, id);
-  }
-
-
-  /** Accept the visitor. **/
-  public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
-    return visitor.visit(this, data);
-  }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java
deleted file mode 100644
index 7d711c5..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Generated By:JJTree: Do not edit this line. ASTphrase.java */
-
-package org.apache.james.mime4j.field.address.parser;
-
-public class ASTphrase extends SimpleNode {
-  public ASTphrase(int id) {
-    super(id);
-  }
-
-  public ASTphrase(AddressListParser p, int id) {
-    super(p, id);
-  }
-
-
-  /** Accept the visitor. **/
-  public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
-    return visitor.visit(this, data);
-  }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTroute.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTroute.java
deleted file mode 100644
index 54ea115..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTroute.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Generated By:JJTree: Do not edit this line. ASTroute.java */
-
-package org.apache.james.mime4j.field.address.parser;
-
-public class ASTroute extends SimpleNode {
-  public ASTroute(int id) {
-    super(id);
-  }
-
-  public ASTroute(AddressListParser p, int id) {
-    super(p, id);
-  }
-
-
-  /** Accept the visitor. **/
-  public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
-    return visitor.visit(this, data);
-  }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java
deleted file mode 100644
index 8094df0..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java
+++ /dev/null
@@ -1,977 +0,0 @@
-/* Generated By:JJTree&JavaCC: Do not edit this line. AddressListParser.java */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.address.parser;
-
-public class AddressListParser/*@bgen(jjtree)*/implements AddressListParserTreeConstants, AddressListParserConstants {/*@bgen(jjtree)*/
-  protected JJTAddressListParserState jjtree = new JJTAddressListParserState();public static void main(String args[]) throws ParseException {
-                while (true) {
-                    try {
-                                AddressListParser parser = new AddressListParser(System.in);
-                        parser.parseLine();
-                        ((SimpleNode)parser.jjtree.rootNode()).dump("> ");
-                    } catch (Exception x) {
-                                x.printStackTrace();
-                                return;
-                    }
-                }
-    }
-
-    private static void log(String msg) {
-        System.out.print(msg);
-    }
-
-    public ASTaddress_list parse() throws ParseException {
-        try {
-            parseAll();
-            return (ASTaddress_list)jjtree.rootNode();
-        } catch (TokenMgrError tme) {
-            throw new ParseException(tme.getMessage());
-        }
-    }
-
-
-    void jjtreeOpenNodeScope(Node n) {
-        ((SimpleNode)n).firstToken = getToken(1);
-    }
-
-    void jjtreeCloseNodeScope(Node n) {
-        ((SimpleNode)n).lastToken = getToken(0);
-    }
-
-  final public void parseLine() throws ParseException {
-    address_list();
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case 1:
-      jj_consume_token(1);
-      break;
-    default:
-      jj_la1[0] = jj_gen;
-      ;
-    }
-    jj_consume_token(2);
-  }
-
-  final public void parseAll() throws ParseException {
-    address_list();
-    jj_consume_token(0);
-  }
-
-  final public void address_list() throws ParseException {
- /*@bgen(jjtree) address_list */
-  ASTaddress_list jjtn000 = new ASTaddress_list(JJTADDRESS_LIST);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-    try {
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case 6:
-      case DOTATOM:
-      case QUOTEDSTRING:
-        address();
-        break;
-      default:
-        jj_la1[1] = jj_gen;
-        ;
-      }
-      label_1:
-      while (true) {
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case 3:
-          ;
-          break;
-        default:
-          jj_la1[2] = jj_gen;
-          break label_1;
-        }
-        jj_consume_token(3);
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case 6:
-        case DOTATOM:
-        case QUOTEDSTRING:
-          address();
-          break;
-        default:
-          jj_la1[3] = jj_gen;
-          ;
-        }
-      }
-    } catch (Throwable jjte000) {
-          if (jjtc000) {
-            jjtree.clearNodeScope(jjtn000);
-            jjtc000 = false;
-          } else {
-            jjtree.popNode();
-          }
-          if (jjte000 instanceof RuntimeException) {
-            {if (true) throw (RuntimeException)jjte000;}
-          }
-          if (jjte000 instanceof ParseException) {
-            {if (true) throw (ParseException)jjte000;}
-          }
-          {if (true) throw (Error)jjte000;}
-    } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-    }
-  }
-
-  final public void address() throws ParseException {
- /*@bgen(jjtree) address */
-  ASTaddress jjtn000 = new ASTaddress(JJTADDRESS);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-    try {
-      if (jj_2_1(2147483647)) {
-        addr_spec();
-      } else {
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case 6:
-          angle_addr();
-          break;
-        case DOTATOM:
-        case QUOTEDSTRING:
-          phrase();
-          switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-          case 4:
-            group_body();
-            break;
-          case 6:
-            angle_addr();
-            break;
-          default:
-            jj_la1[4] = jj_gen;
-            jj_consume_token(-1);
-            throw new ParseException();
-          }
-          break;
-        default:
-          jj_la1[5] = jj_gen;
-          jj_consume_token(-1);
-          throw new ParseException();
-        }
-      }
-    } catch (Throwable jjte000) {
-          if (jjtc000) {
-            jjtree.clearNodeScope(jjtn000);
-            jjtc000 = false;
-          } else {
-            jjtree.popNode();
-          }
-          if (jjte000 instanceof RuntimeException) {
-            {if (true) throw (RuntimeException)jjte000;}
-          }
-          if (jjte000 instanceof ParseException) {
-            {if (true) throw (ParseException)jjte000;}
-          }
-          {if (true) throw (Error)jjte000;}
-    } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-    }
-  }
-
-  final public void mailbox() throws ParseException {
- /*@bgen(jjtree) mailbox */
-  ASTmailbox jjtn000 = new ASTmailbox(JJTMAILBOX);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-    try {
-      if (jj_2_2(2147483647)) {
-        addr_spec();
-      } else {
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case 6:
-          angle_addr();
-          break;
-        case DOTATOM:
-        case QUOTEDSTRING:
-          name_addr();
-          break;
-        default:
-          jj_la1[6] = jj_gen;
-          jj_consume_token(-1);
-          throw new ParseException();
-        }
-      }
-    } catch (Throwable jjte000) {
-          if (jjtc000) {
-            jjtree.clearNodeScope(jjtn000);
-            jjtc000 = false;
-          } else {
-            jjtree.popNode();
-          }
-          if (jjte000 instanceof RuntimeException) {
-            {if (true) throw (RuntimeException)jjte000;}
-          }
-          if (jjte000 instanceof ParseException) {
-            {if (true) throw (ParseException)jjte000;}
-          }
-          {if (true) throw (Error)jjte000;}
-    } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-    }
-  }
-
-  final public void name_addr() throws ParseException {
- /*@bgen(jjtree) name_addr */
-  ASTname_addr jjtn000 = new ASTname_addr(JJTNAME_ADDR);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-    try {
-      phrase();
-      angle_addr();
-    } catch (Throwable jjte000) {
-          if (jjtc000) {
-            jjtree.clearNodeScope(jjtn000);
-            jjtc000 = false;
-          } else {
-            jjtree.popNode();
-          }
-          if (jjte000 instanceof RuntimeException) {
-            {if (true) throw (RuntimeException)jjte000;}
-          }
-          if (jjte000 instanceof ParseException) {
-            {if (true) throw (ParseException)jjte000;}
-          }
-          {if (true) throw (Error)jjte000;}
-    } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-    }
-  }
-
-  final public void group_body() throws ParseException {
- /*@bgen(jjtree) group_body */
-  ASTgroup_body jjtn000 = new ASTgroup_body(JJTGROUP_BODY);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-    try {
-      jj_consume_token(4);
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case 6:
-      case DOTATOM:
-      case QUOTEDSTRING:
-        mailbox();
-        break;
-      default:
-        jj_la1[7] = jj_gen;
-        ;
-      }
-      label_2:
-      while (true) {
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case 3:
-          ;
-          break;
-        default:
-          jj_la1[8] = jj_gen;
-          break label_2;
-        }
-        jj_consume_token(3);
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case 6:
-        case DOTATOM:
-        case QUOTEDSTRING:
-          mailbox();
-          break;
-        default:
-          jj_la1[9] = jj_gen;
-          ;
-        }
-      }
-      jj_consume_token(5);
-    } catch (Throwable jjte000) {
-          if (jjtc000) {
-            jjtree.clearNodeScope(jjtn000);
-            jjtc000 = false;
-          } else {
-            jjtree.popNode();
-          }
-          if (jjte000 instanceof RuntimeException) {
-            {if (true) throw (RuntimeException)jjte000;}
-          }
-          if (jjte000 instanceof ParseException) {
-            {if (true) throw (ParseException)jjte000;}
-          }
-          {if (true) throw (Error)jjte000;}
-    } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-    }
-  }
-
-  final public void angle_addr() throws ParseException {
- /*@bgen(jjtree) angle_addr */
-  ASTangle_addr jjtn000 = new ASTangle_addr(JJTANGLE_ADDR);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-    try {
-      jj_consume_token(6);
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case 8:
-        route();
-        break;
-      default:
-        jj_la1[10] = jj_gen;
-        ;
-      }
-      addr_spec();
-      jj_consume_token(7);
-    } catch (Throwable jjte000) {
-          if (jjtc000) {
-            jjtree.clearNodeScope(jjtn000);
-            jjtc000 = false;
-          } else {
-            jjtree.popNode();
-          }
-          if (jjte000 instanceof RuntimeException) {
-            {if (true) throw (RuntimeException)jjte000;}
-          }
-          if (jjte000 instanceof ParseException) {
-            {if (true) throw (ParseException)jjte000;}
-          }
-          {if (true) throw (Error)jjte000;}
-    } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-    }
-  }
-
-  final public void route() throws ParseException {
- /*@bgen(jjtree) route */
-  ASTroute jjtn000 = new ASTroute(JJTROUTE);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-    try {
-      jj_consume_token(8);
-      domain();
-      label_3:
-      while (true) {
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case 3:
-        case 8:
-          ;
-          break;
-        default:
-          jj_la1[11] = jj_gen;
-          break label_3;
-        }
-        label_4:
-        while (true) {
-          switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-          case 3:
-            ;
-            break;
-          default:
-            jj_la1[12] = jj_gen;
-            break label_4;
-          }
-          jj_consume_token(3);
-        }
-        jj_consume_token(8);
-        domain();
-      }
-      jj_consume_token(4);
-    } catch (Throwable jjte000) {
-          if (jjtc000) {
-            jjtree.clearNodeScope(jjtn000);
-            jjtc000 = false;
-          } else {
-            jjtree.popNode();
-          }
-          if (jjte000 instanceof RuntimeException) {
-            {if (true) throw (RuntimeException)jjte000;}
-          }
-          if (jjte000 instanceof ParseException) {
-            {if (true) throw (ParseException)jjte000;}
-          }
-          {if (true) throw (Error)jjte000;}
-    } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-    }
-  }
-
-  final public void phrase() throws ParseException {
- /*@bgen(jjtree) phrase */
-  ASTphrase jjtn000 = new ASTphrase(JJTPHRASE);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-    try {
-      label_5:
-      while (true) {
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case DOTATOM:
-          jj_consume_token(DOTATOM);
-          break;
-        case QUOTEDSTRING:
-          jj_consume_token(QUOTEDSTRING);
-          break;
-        default:
-          jj_la1[13] = jj_gen;
-          jj_consume_token(-1);
-          throw new ParseException();
-        }
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case DOTATOM:
-        case QUOTEDSTRING:
-          ;
-          break;
-        default:
-          jj_la1[14] = jj_gen;
-          break label_5;
-        }
-      }
-    } finally {
-  if (jjtc000) {
-    jjtree.closeNodeScope(jjtn000, true);
-    jjtreeCloseNodeScope(jjtn000);
-  }
-    }
-  }
-
-  final public void addr_spec() throws ParseException {
- /*@bgen(jjtree) addr_spec */
-  ASTaddr_spec jjtn000 = new ASTaddr_spec(JJTADDR_SPEC);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-    try {
-      local_part();
-      jj_consume_token(8);
-      domain();
-    } catch (Throwable jjte000) {
-          if (jjtc000) {
-            jjtree.clearNodeScope(jjtn000);
-            jjtc000 = false;
-          } else {
-            jjtree.popNode();
-          }
-          if (jjte000 instanceof RuntimeException) {
-            {if (true) throw (RuntimeException)jjte000;}
-          }
-          if (jjte000 instanceof ParseException) {
-            {if (true) throw (ParseException)jjte000;}
-          }
-          {if (true) throw (Error)jjte000;}
-    } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-    }
-  }
-
-  final public void local_part() throws ParseException {
- /*@bgen(jjtree) local_part */
-  ASTlocal_part jjtn000 = new ASTlocal_part(JJTLOCAL_PART);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);Token t;
-    try {
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case DOTATOM:
-        t = jj_consume_token(DOTATOM);
-        break;
-      case QUOTEDSTRING:
-        t = jj_consume_token(QUOTEDSTRING);
-        break;
-      default:
-        jj_la1[15] = jj_gen;
-        jj_consume_token(-1);
-        throw new ParseException();
-      }
-      label_6:
-      while (true) {
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case 9:
-        case DOTATOM:
-        case QUOTEDSTRING:
-          ;
-          break;
-        default:
-          jj_la1[16] = jj_gen;
-          break label_6;
-        }
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case 9:
-          t = jj_consume_token(9);
-          break;
-        default:
-          jj_la1[17] = jj_gen;
-          ;
-        }
-                        if (t.image.charAt(t.image.length() - 1) != '.' || t.kind == AddressListParserConstants.QUOTEDSTRING)
-                                {if (true) throw new ParseException("Words in local part must be separated by '.'");}
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case DOTATOM:
-          t = jj_consume_token(DOTATOM);
-          break;
-        case QUOTEDSTRING:
-          t = jj_consume_token(QUOTEDSTRING);
-          break;
-        default:
-          jj_la1[18] = jj_gen;
-          jj_consume_token(-1);
-          throw new ParseException();
-        }
-      }
-    } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-    }
-  }
-
-  final public void domain() throws ParseException {
- /*@bgen(jjtree) domain */
-  ASTdomain jjtn000 = new ASTdomain(JJTDOMAIN);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);Token t;
-    try {
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case DOTATOM:
-        t = jj_consume_token(DOTATOM);
-        label_7:
-        while (true) {
-          switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-          case 9:
-          case DOTATOM:
-            ;
-            break;
-          default:
-            jj_la1[19] = jj_gen;
-            break label_7;
-          }
-          switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-          case 9:
-            t = jj_consume_token(9);
-            break;
-          default:
-            jj_la1[20] = jj_gen;
-            ;
-          }
-                                if (t.image.charAt(t.image.length() - 1) != '.')
-                                        {if (true) throw new ParseException("Atoms in domain names must be separated by '.'");}
-          t = jj_consume_token(DOTATOM);
-        }
-        break;
-      case DOMAINLITERAL:
-        jj_consume_token(DOMAINLITERAL);
-        break;
-      default:
-        jj_la1[21] = jj_gen;
-        jj_consume_token(-1);
-        throw new ParseException();
-      }
-    } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-    }
-  }
-
-  final private boolean jj_2_1(int xla) {
-    jj_la = xla; jj_lastpos = jj_scanpos = token;
-    try { return !jj_3_1(); }
-    catch(LookaheadSuccess ls) { return true; }
-    finally { jj_save(0, xla); }
-  }
-
-  final private boolean jj_2_2(int xla) {
-    jj_la = xla; jj_lastpos = jj_scanpos = token;
-    try { return !jj_3_2(); }
-    catch(LookaheadSuccess ls) { return true; }
-    finally { jj_save(1, xla); }
-  }
-
-  final private boolean jj_3R_11() {
-    Token xsp;
-    xsp = jj_scanpos;
-    if (jj_scan_token(9)) jj_scanpos = xsp;
-    xsp = jj_scanpos;
-    if (jj_scan_token(14)) {
-    jj_scanpos = xsp;
-    if (jj_scan_token(31)) return true;
-    }
-    return false;
-  }
-
-  final private boolean jj_3R_13() {
-    Token xsp;
-    xsp = jj_scanpos;
-    if (jj_scan_token(9)) jj_scanpos = xsp;
-    if (jj_scan_token(DOTATOM)) return true;
-    return false;
-  }
-
-  final private boolean jj_3R_8() {
-    if (jj_3R_9()) return true;
-    if (jj_scan_token(8)) return true;
-    if (jj_3R_10()) return true;
-    return false;
-  }
-
-  final private boolean jj_3_1() {
-    if (jj_3R_8()) return true;
-    return false;
-  }
-
-  final private boolean jj_3R_12() {
-    if (jj_scan_token(DOTATOM)) return true;
-    Token xsp;
-    while (true) {
-      xsp = jj_scanpos;
-      if (jj_3R_13()) { jj_scanpos = xsp; break; }
-    }
-    return false;
-  }
-
-  final private boolean jj_3R_10() {
-    Token xsp;
-    xsp = jj_scanpos;
-    if (jj_3R_12()) {
-    jj_scanpos = xsp;
-    if (jj_scan_token(18)) return true;
-    }
-    return false;
-  }
-
-  final private boolean jj_3_2() {
-    if (jj_3R_8()) return true;
-    return false;
-  }
-
-  final private boolean jj_3R_9() {
-    Token xsp;
-    xsp = jj_scanpos;
-    if (jj_scan_token(14)) {
-    jj_scanpos = xsp;
-    if (jj_scan_token(31)) return true;
-    }
-    while (true) {
-      xsp = jj_scanpos;
-      if (jj_3R_11()) { jj_scanpos = xsp; break; }
-    }
-    return false;
-  }
-
-  public AddressListParserTokenManager token_source;
-  SimpleCharStream jj_input_stream;
-  public Token token, jj_nt;
-  private int jj_ntk;
-  private Token jj_scanpos, jj_lastpos;
-  private int jj_la;
-  public boolean lookingAhead = false;
-  private boolean jj_semLA;
-  private int jj_gen;
-  final private int[] jj_la1 = new int[22];
-  static private int[] jj_la1_0;
-  static private int[] jj_la1_1;
-  static {
-      jj_la1_0();
-      jj_la1_1();
-   }
-   private static void jj_la1_0() {
-      jj_la1_0 = new int[] {0x2,0x80004040,0x8,0x80004040,0x50,0x80004040,0x80004040,0x80004040,0x8,0x80004040,0x100,0x108,0x8,0x80004000,0x80004000,0x80004000,0x80004200,0x200,0x80004000,0x4200,0x200,0x44000,};
-   }
-   private static void jj_la1_1() {
-      jj_la1_1 = new int[] {0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,};
-   }
-  final private JJCalls[] jj_2_rtns = new JJCalls[2];
-  private boolean jj_rescan = false;
-  private int jj_gc = 0;
-
-  public AddressListParser(java.io.InputStream stream) {
-     this(stream, null);
-  }
-  public AddressListParser(java.io.InputStream stream, String encoding) {
-    try { jj_input_stream = new SimpleCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
-    token_source = new AddressListParserTokenManager(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 22; i++) jj_la1[i] = -1;
-    for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls();
-  }
-
-  public void ReInit(java.io.InputStream stream) {
-     ReInit(stream, null);
-  }
-  public void ReInit(java.io.InputStream stream, String encoding) {
-    try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
-    token_source.ReInit(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jjtree.reset();
-    jj_gen = 0;
-    for (int i = 0; i < 22; i++) jj_la1[i] = -1;
-    for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls();
-  }
-
-  public AddressListParser(java.io.Reader stream) {
-    jj_input_stream = new SimpleCharStream(stream, 1, 1);
-    token_source = new AddressListParserTokenManager(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 22; i++) jj_la1[i] = -1;
-    for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls();
-  }
-
-  public void ReInit(java.io.Reader stream) {
-    jj_input_stream.ReInit(stream, 1, 1);
-    token_source.ReInit(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jjtree.reset();
-    jj_gen = 0;
-    for (int i = 0; i < 22; i++) jj_la1[i] = -1;
-    for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls();
-  }
-
-  public AddressListParser(AddressListParserTokenManager tm) {
-    token_source = tm;
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 22; i++) jj_la1[i] = -1;
-    for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls();
-  }
-
-  public void ReInit(AddressListParserTokenManager tm) {
-    token_source = tm;
-    token = new Token();
-    jj_ntk = -1;
-    jjtree.reset();
-    jj_gen = 0;
-    for (int i = 0; i < 22; i++) jj_la1[i] = -1;
-    for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls();
-  }
-
-  final private Token jj_consume_token(int kind) throws ParseException {
-    Token oldToken;
-    if ((oldToken = token).next != null) token = token.next;
-    else token = token.next = token_source.getNextToken();
-    jj_ntk = -1;
-    if (token.kind == kind) {
-      jj_gen++;
-      if (++jj_gc > 100) {
-        jj_gc = 0;
-        for (int i = 0; i < jj_2_rtns.length; i++) {
-          JJCalls c = jj_2_rtns[i];
-          while (c != null) {
-            if (c.gen < jj_gen) c.first = null;
-            c = c.next;
-          }
-        }
-      }
-      return token;
-    }
-    token = oldToken;
-    jj_kind = kind;
-    throw generateParseException();
-  }
-
-  static private final class LookaheadSuccess extends java.lang.Error { }
-  final private LookaheadSuccess jj_ls = new LookaheadSuccess();
-  final private boolean jj_scan_token(int kind) {
-    if (jj_scanpos == jj_lastpos) {
-      jj_la--;
-      if (jj_scanpos.next == null) {
-        jj_lastpos = jj_scanpos = jj_scanpos.next = token_source.getNextToken();
-      } else {
-        jj_lastpos = jj_scanpos = jj_scanpos.next;
-      }
-    } else {
-      jj_scanpos = jj_scanpos.next;
-    }
-    if (jj_rescan) {
-      int i = 0; Token tok = token;
-      while (tok != null && tok != jj_scanpos) { i++; tok = tok.next; }
-      if (tok != null) jj_add_error_token(kind, i);
-    }
-    if (jj_scanpos.kind != kind) return true;
-    if (jj_la == 0 && jj_scanpos == jj_lastpos) throw jj_ls;
-    return false;
-  }
-
-  final public Token getNextToken() {
-    if (token.next != null) token = token.next;
-    else token = token.next = token_source.getNextToken();
-    jj_ntk = -1;
-    jj_gen++;
-    return token;
-  }
-
-  final public Token getToken(int index) {
-    Token t = lookingAhead ? jj_scanpos : token;
-    for (int i = 0; i < index; i++) {
-      if (t.next != null) t = t.next;
-      else t = t.next = token_source.getNextToken();
-    }
-    return t;
-  }
-
-  final private int jj_ntk() {
-    if ((jj_nt=token.next) == null)
-      return (jj_ntk = (token.next=token_source.getNextToken()).kind);
-    else
-      return (jj_ntk = jj_nt.kind);
-  }
-
-  private java.util.Vector<int[]> jj_expentries = new java.util.Vector<int[]>();
-  private int[] jj_expentry;
-  private int jj_kind = -1;
-  private int[] jj_lasttokens = new int[100];
-  private int jj_endpos;
-
-  private void jj_add_error_token(int kind, int pos) {
-    if (pos >= 100) return;
-    if (pos == jj_endpos + 1) {
-      jj_lasttokens[jj_endpos++] = kind;
-    } else if (jj_endpos != 0) {
-      jj_expentry = new int[jj_endpos];
-      for (int i = 0; i < jj_endpos; i++) {
-        jj_expentry[i] = jj_lasttokens[i];
-      }
-      boolean exists = false;
-      for (java.util.Enumeration<int[]> e = jj_expentries.elements(); e.hasMoreElements();) {
-        int[] oldentry = e.nextElement();
-        if (oldentry.length == jj_expentry.length) {
-          exists = true;
-          for (int i = 0; i < jj_expentry.length; i++) {
-            if (oldentry[i] != jj_expentry[i]) {
-              exists = false;
-              break;
-            }
-          }
-          if (exists) break;
-        }
-      }
-      if (!exists) jj_expentries.addElement(jj_expentry);
-      if (pos != 0) jj_lasttokens[(jj_endpos = pos) - 1] = kind;
-    }
-  }
-
-  public ParseException generateParseException() {
-    jj_expentries.removeAllElements();
-    boolean[] la1tokens = new boolean[34];
-    for (int i = 0; i < 34; i++) {
-      la1tokens[i] = false;
-    }
-    if (jj_kind >= 0) {
-      la1tokens[jj_kind] = true;
-      jj_kind = -1;
-    }
-    for (int i = 0; i < 22; i++) {
-      if (jj_la1[i] == jj_gen) {
-        for (int j = 0; j < 32; j++) {
-          if ((jj_la1_0[i] & (1<<j)) != 0) {
-            la1tokens[j] = true;
-          }
-          if ((jj_la1_1[i] & (1<<j)) != 0) {
-            la1tokens[32+j] = true;
-          }
-        }
-      }
-    }
-    for (int i = 0; i < 34; i++) {
-      if (la1tokens[i]) {
-        jj_expentry = new int[1];
-        jj_expentry[0] = i;
-        jj_expentries.addElement(jj_expentry);
-      }
-    }
-    jj_endpos = 0;
-    jj_rescan_token();
-    jj_add_error_token(0, 0);
-    int[][] exptokseq = new int[jj_expentries.size()][];
-    for (int i = 0; i < jj_expentries.size(); i++) {
-      exptokseq[i] = jj_expentries.elementAt(i);
-    }
-    return new ParseException(token, exptokseq, tokenImage);
-  }
-
-  final public void enable_tracing() {
-  }
-
-  final public void disable_tracing() {
-  }
-
-  final private void jj_rescan_token() {
-    jj_rescan = true;
-    for (int i = 0; i < 2; i++) {
-    try {
-      JJCalls p = jj_2_rtns[i];
-      do {
-        if (p.gen > jj_gen) {
-          jj_la = p.arg; jj_lastpos = jj_scanpos = p.first;
-          switch (i) {
-            case 0: jj_3_1(); break;
-            case 1: jj_3_2(); break;
-          }
-        }
-        p = p.next;
-      } while (p != null);
-      } catch(LookaheadSuccess ls) { }
-    }
-    jj_rescan = false;
-  }
-
-  final private void jj_save(int index, int xla) {
-    JJCalls p = jj_2_rtns[index];
-    while (p.gen > jj_gen) {
-      if (p.next == null) { p = p.next = new JJCalls(); break; }
-      p = p.next;
-    }
-    p.gen = jj_gen + xla - jj_la; p.first = token; p.arg = xla;
-  }
-
-  static final class JJCalls {
-    int gen;
-    Token first;
-    int arg;
-    JJCalls next;
-  }
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj
deleted file mode 100644
index c14277b..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj
+++ /dev/null
@@ -1,595 +0,0 @@
-/*@bgen(jjtree) Generated By:JJTree: Do not edit this line. /Users/jason/Projects/apache-mime4j-0.3/target/generated-sources/jjtree/org/apache/james/mime4j/field/address/parser/AddressListParser.jj */
-/*@egen*//****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-
-/**
- * RFC2822 address list parser.
- *
- * Created 9/17/2004
- * by Joe Cheng <code@joecheng.com>
- */
-
-options {
-	STATIC=false;
-	LOOKAHEAD=1;                                                                                                                               
-	//DEBUG_PARSER=true;
-	//DEBUG_TOKEN_MANAGER=true;
-}
-
-PARSER_BEGIN(AddressListParser)
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.address.parser;
-
-public class AddressListParser/*@bgen(jjtree)*/implements AddressListParserTreeConstants/*@egen*/ {/*@bgen(jjtree)*/
-  protected JJTAddressListParserState jjtree = new JJTAddressListParserState();
-
-/*@egen*/
-    public static void main(String args[]) throws ParseException {
-		while (true) {
-		    try {
-				AddressListParser parser = new AddressListParser(System.in);
-		    	parser.parseLine();
-		    	((SimpleNode)parser.jjtree.rootNode()).dump("> ");
-		    } catch (Exception x) {
-				x.printStackTrace();
-				return;
-		    }
-		}
-    }
-    
-    private static void log(String msg) {
-    	System.out.print(msg);
-    }
-    
-    public ASTaddress_list parse() throws ParseException {
-        try {
-    	    parseAll();
-    	    return (ASTaddress_list)jjtree.rootNode();
-    	} catch (TokenMgrError tme) {
-    	    throw new ParseException(tme.getMessage());
-    	}
-    }
-    
-    
-    void jjtreeOpenNodeScope(Node n) {
-    	((SimpleNode)n).firstToken = getToken(1);
-    }
-    
-    void jjtreeCloseNodeScope(Node n) {
-    	((SimpleNode)n).lastToken = getToken(0);
-    }
-}
-
-PARSER_END(AddressListParser)
-
-void parseLine()       :
-{}
-{
-	address_list() ["\r"] "\n"
-}
-
-void parseAll()       :
-{}
-{
-	address_list() <EOF>
-}
-
-void address_list() :
-{/*@bgen(jjtree) address_list */
-  ASTaddress_list jjtn000 = new ASTaddress_list(JJTADDRESS_LIST);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-/*@egen*/}
-{/*@bgen(jjtree) address_list */
-        try {
-/*@egen*/
-	[ address() ]
-	(
-		","
-		[ address() ]
-	)*/*@bgen(jjtree)*/
-        } catch (Throwable jjte000) {
-          if (jjtc000) {
-            jjtree.clearNodeScope(jjtn000);
-            jjtc000 = false;
-          } else {
-            jjtree.popNode();
-          }
-          if (jjte000 instanceof RuntimeException) {
-            throw (RuntimeException)jjte000;
-          }
-          if (jjte000 instanceof ParseException) {
-            throw (ParseException)jjte000;
-          }
-          throw (Error)jjte000;
-        } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-        }
-/*@egen*/
-}
-
-void address() :
-{/*@bgen(jjtree) address */
-  ASTaddress jjtn000 = new ASTaddress(JJTADDRESS);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-/*@egen*/}
-{/*@bgen(jjtree) address */
-        try {
-/*@egen*/
-	LOOKAHEAD(2147483647)
-	addr_spec()
-|	angle_addr()
-|	( phrase() (group_body() | angle_addr()) )/*@bgen(jjtree)*/
-        } catch (Throwable jjte000) {
-          if (jjtc000) {
-            jjtree.clearNodeScope(jjtn000);
-            jjtc000 = false;
-          } else {
-            jjtree.popNode();
-          }
-          if (jjte000 instanceof RuntimeException) {
-            throw (RuntimeException)jjte000;
-          }
-          if (jjte000 instanceof ParseException) {
-            throw (ParseException)jjte000;
-          }
-          throw (Error)jjte000;
-        } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-        }
-/*@egen*/
-}
-
-void mailbox() :
-{/*@bgen(jjtree) mailbox */
-  ASTmailbox jjtn000 = new ASTmailbox(JJTMAILBOX);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-/*@egen*/}
-{/*@bgen(jjtree) mailbox */
-        try {
-/*@egen*/
-	LOOKAHEAD(2147483647)
-	addr_spec()
-|	angle_addr()
-|	name_addr()/*@bgen(jjtree)*/
-        } catch (Throwable jjte000) {
-          if (jjtc000) {
-            jjtree.clearNodeScope(jjtn000);
-            jjtc000 = false;
-          } else {
-            jjtree.popNode();
-          }
-          if (jjte000 instanceof RuntimeException) {
-            throw (RuntimeException)jjte000;
-          }
-          if (jjte000 instanceof ParseException) {
-            throw (ParseException)jjte000;
-          }
-          throw (Error)jjte000;
-        } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-        }
-/*@egen*/
-}
-
-void name_addr() :
-{/*@bgen(jjtree) name_addr */
-  ASTname_addr jjtn000 = new ASTname_addr(JJTNAME_ADDR);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-/*@egen*/}
-{/*@bgen(jjtree) name_addr */
-        try {
-/*@egen*/
-	phrase() angle_addr()/*@bgen(jjtree)*/
-        } catch (Throwable jjte000) {
-          if (jjtc000) {
-            jjtree.clearNodeScope(jjtn000);
-            jjtc000 = false;
-          } else {
-            jjtree.popNode();
-          }
-          if (jjte000 instanceof RuntimeException) {
-            throw (RuntimeException)jjte000;
-          }
-          if (jjte000 instanceof ParseException) {
-            throw (ParseException)jjte000;
-          }
-          throw (Error)jjte000;
-        } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-        }
-/*@egen*/
-}
-
-void group_body() :
-{/*@bgen(jjtree) group_body */
-  ASTgroup_body jjtn000 = new ASTgroup_body(JJTGROUP_BODY);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-/*@egen*/}
-{/*@bgen(jjtree) group_body */
-        try {
-/*@egen*/
-	":"
-	[ mailbox() ]
-	(
-		","
-		[ mailbox() ]
-	)*
-	";"/*@bgen(jjtree)*/
-        } catch (Throwable jjte000) {
-          if (jjtc000) {
-            jjtree.clearNodeScope(jjtn000);
-            jjtc000 = false;
-          } else {
-            jjtree.popNode();
-          }
-          if (jjte000 instanceof RuntimeException) {
-            throw (RuntimeException)jjte000;
-          }
-          if (jjte000 instanceof ParseException) {
-            throw (ParseException)jjte000;
-          }
-          throw (Error)jjte000;
-        } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-        }
-/*@egen*/
-}
-
-void angle_addr() :
-{/*@bgen(jjtree) angle_addr */
-  ASTangle_addr jjtn000 = new ASTangle_addr(JJTANGLE_ADDR);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-/*@egen*/}
-{/*@bgen(jjtree) angle_addr */
-        try {
-/*@egen*/
-	"<" [ route() ] addr_spec() ">"/*@bgen(jjtree)*/
-        } catch (Throwable jjte000) {
-          if (jjtc000) {
-            jjtree.clearNodeScope(jjtn000);
-            jjtc000 = false;
-          } else {
-            jjtree.popNode();
-          }
-          if (jjte000 instanceof RuntimeException) {
-            throw (RuntimeException)jjte000;
-          }
-          if (jjte000 instanceof ParseException) {
-            throw (ParseException)jjte000;
-          }
-          throw (Error)jjte000;
-        } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-        }
-/*@egen*/
-}
-
-void route() :
-{/*@bgen(jjtree) route */
-  ASTroute jjtn000 = new ASTroute(JJTROUTE);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-/*@egen*/}
-{/*@bgen(jjtree) route */
-        try {
-/*@egen*/
-	"@" domain() ( (",")* "@" domain() )* ":"/*@bgen(jjtree)*/
-        } catch (Throwable jjte000) {
-          if (jjtc000) {
-            jjtree.clearNodeScope(jjtn000);
-            jjtc000 = false;
-          } else {
-            jjtree.popNode();
-          }
-          if (jjte000 instanceof RuntimeException) {
-            throw (RuntimeException)jjte000;
-          }
-          if (jjte000 instanceof ParseException) {
-            throw (ParseException)jjte000;
-          }
-          throw (Error)jjte000;
-        } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-        }
-/*@egen*/
-}
-
-void phrase() :
-{/*@bgen(jjtree) phrase */
-  ASTphrase jjtn000 = new ASTphrase(JJTPHRASE);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-/*@egen*/}
-{/*@bgen(jjtree) phrase */
-try {
-/*@egen*/
-(	<DOTATOM>
-|	<QUOTEDSTRING>
-)+/*@bgen(jjtree)*/
-} finally {
-  if (jjtc000) {
-    jjtree.closeNodeScope(jjtn000, true);
-    jjtreeCloseNodeScope(jjtn000);
-  }
-}
-/*@egen*/
-}
-
-void addr_spec() :
-{/*@bgen(jjtree) addr_spec */
-  ASTaddr_spec jjtn000 = new ASTaddr_spec(JJTADDR_SPEC);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-/*@egen*/}
-{/*@bgen(jjtree) addr_spec */
-        try {
-/*@egen*/
-	( local_part() "@" domain() )/*@bgen(jjtree)*/
-        } catch (Throwable jjte000) {
-          if (jjtc000) {
-            jjtree.clearNodeScope(jjtn000);
-            jjtc000 = false;
-          } else {
-            jjtree.popNode();
-          }
-          if (jjte000 instanceof RuntimeException) {
-            throw (RuntimeException)jjte000;
-          }
-          if (jjte000 instanceof ParseException) {
-            throw (ParseException)jjte000;
-          }
-          throw (Error)jjte000;
-        } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-        }
-/*@egen*/
-}
-
-void local_part() :
-{/*@bgen(jjtree) local_part */
-  ASTlocal_part jjtn000 = new ASTlocal_part(JJTLOCAL_PART);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-/*@egen*/ Token t; }
-{/*@bgen(jjtree) local_part */
-        try {
-/*@egen*/
-	( t=<DOTATOM> | t=<QUOTEDSTRING> )
-	(	[t="."]
-		{
-			if (t.image.charAt(t.image.length() - 1) != '.' || t.kind == AddressListParserConstants.QUOTEDSTRING)
-				throw new ParseException("Words in local part must be separated by '.'");
-		}
-		(	t=<DOTATOM> | t=<QUOTEDSTRING> )
-	)*/*@bgen(jjtree)*/
-        } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-        }
-/*@egen*/
-}
-
-void domain() :
-{/*@bgen(jjtree) domain */
-  ASTdomain jjtn000 = new ASTdomain(JJTDOMAIN);
-  boolean jjtc000 = true;
-  jjtree.openNodeScope(jjtn000);
-  jjtreeOpenNodeScope(jjtn000);
-/*@egen*/ Token t; }
-{/*@bgen(jjtree) domain */
-        try {
-/*@egen*/
-	(	t=<DOTATOM>
-		(	[t="."]
-			{
-				if (t.image.charAt(t.image.length() - 1) != '.')
-					throw new ParseException("Atoms in domain names must be separated by '.'");
-			}
-			t=<DOTATOM>
-		)*
-	)
-|	<DOMAINLITERAL>/*@bgen(jjtree)*/
-        } finally {
-          if (jjtc000) {
-            jjtree.closeNodeScope(jjtn000, true);
-            jjtreeCloseNodeScope(jjtn000);
-          }
-        }
-/*@egen*/
-}
-
-SPECIAL_TOKEN :
-{
- 	< WS: ( [" ", "\t"] )+ >
-}
-
-TOKEN :
-{
-	< #ALPHA: ["a" - "z", "A" - "Z"] >
-|	< #DIGIT: ["0" - "9"] >
-|	< #ATEXT: ( <ALPHA> | <DIGIT>
-			  | "!" | "#" | "$" | "%"
-			  | "&" | "'" | "*" | "+"
-			  | "-" | "/" | "=" | "?"
-			  | "^" | "_" | "`" | "{"
-			  | "|" | "}" | "~"
-			  )>
-|	< DOTATOM: <ATEXT> ( <ATEXT> | "." )* >
-}
-
-TOKEN_MGR_DECLS :
-{
-	// Keeps track of how many levels of comment nesting
-	// we've encountered.  This is only used when the 2nd
-	// level is reached, for example ((this)), not (this).
-	// This is because the outermost level must be treated
-	// specially anyway, because the outermost ")" has a 
-	// different token type than inner ")" instances.
-	static int commentNest;
-}
-
-MORE :
-{
-	// domain literal
-	"[" : INDOMAINLITERAL
-}
-
-<INDOMAINLITERAL>
-MORE :
-{
-	< <QUOTEDPAIR>> { image.deleteCharAt(image.length() - 2); }
-|	< ~["[", "]", "\\"] >
-}
-
-<INDOMAINLITERAL>
-TOKEN :
-{
-	< DOMAINLITERAL: "]" > { matchedToken.image = image.toString(); }: DEFAULT
-}
-
-MORE :
-{
-	// starts a comment
-	"(" : INCOMMENT
-}
-
-<INCOMMENT>
-SKIP :
-{
-	// ends a comment
-	< COMMENT: ")" > : DEFAULT
-	// if this is ever changed to not be a SKIP, need
-	// to make sure matchedToken.token = token.toString()
-	// is called.
-}
-
-<INCOMMENT>
-MORE :
-{
-	< <QUOTEDPAIR>> { image.deleteCharAt(image.length() - 2); }
-|	"(" { commentNest = 1; } : NESTED_COMMENT
-|	< <ANY>>
-}
-
-<NESTED_COMMENT>
-MORE :
-{
-	< <QUOTEDPAIR>> { image.deleteCharAt(image.length() - 2); }
-|	"(" { ++commentNest; }
-|	")" { --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT); }
-|	< <ANY>>
-}
-
-
-// QUOTED STRINGS
-
-MORE :
-{
-	"\"" { image.deleteCharAt(image.length() - 1); } : INQUOTEDSTRING
-}
-
-<INQUOTEDSTRING>
-MORE :
-{
-	< <QUOTEDPAIR>> { image.deleteCharAt(image.length() - 2); }
-|	< (~["\"", "\\"])+ >
-}
-
-<INQUOTEDSTRING>
-TOKEN :
-{
-	< QUOTEDSTRING: "\"" > { matchedToken.image = image.substring(0, image.length() - 1); } : DEFAULT
-}
-
-// GLOBALS
-
-<*>
-TOKEN :
-{
-	< #QUOTEDPAIR: "\\" <ANY> >
-|	< #ANY: ~[] >
-}
-
-// ERROR!
-/*
-
-<*>
-TOKEN :
-{
-	< UNEXPECTED_CHAR: <ANY> >
-}
-
-*/
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java
deleted file mode 100644
index 006a082..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/* Generated By:JJTree&JavaCC: Do not edit this line. AddressListParserConstants.java */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.address.parser;
-
-public interface AddressListParserConstants {
-
-  int EOF = 0;
-  int WS = 10;
-  int ALPHA = 11;
-  int DIGIT = 12;
-  int ATEXT = 13;
-  int DOTATOM = 14;
-  int DOMAINLITERAL = 18;
-  int COMMENT = 20;
-  int QUOTEDSTRING = 31;
-  int QUOTEDPAIR = 32;
-  int ANY = 33;
-
-  int DEFAULT = 0;
-  int INDOMAINLITERAL = 1;
-  int INCOMMENT = 2;
-  int NESTED_COMMENT = 3;
-  int INQUOTEDSTRING = 4;
-
-  String[] tokenImage = {
-    "<EOF>",
-    "\"\\r\"",
-    "\"\\n\"",
-    "\",\"",
-    "\":\"",
-    "\";\"",
-    "\"<\"",
-    "\">\"",
-    "\"@\"",
-    "\".\"",
-    "<WS>",
-    "<ALPHA>",
-    "<DIGIT>",
-    "<ATEXT>",
-    "<DOTATOM>",
-    "\"[\"",
-    "<token of kind 16>",
-    "<token of kind 17>",
-    "\"]\"",
-    "\"(\"",
-    "\")\"",
-    "<token of kind 21>",
-    "\"(\"",
-    "<token of kind 23>",
-    "<token of kind 24>",
-    "\"(\"",
-    "\")\"",
-    "<token of kind 27>",
-    "\"\\\"\"",
-    "<token of kind 29>",
-    "<token of kind 30>",
-    "\"\\\"\"",
-    "<QUOTEDPAIR>",
-    "<ANY>",
-  };
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java
deleted file mode 100644
index d2dd88d..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java
+++ /dev/null
@@ -1,1009 +0,0 @@
-/* Generated By:JJTree&JavaCC: Do not edit this line. AddressListParserTokenManager.java */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.address.parser;
-
-public class AddressListParserTokenManager implements AddressListParserConstants
-{
-        // Keeps track of how many levels of comment nesting
-        // we've encountered.  This is only used when the 2nd
-        // level is reached, for example ((this)), not (this).
-        // This is because the outermost level must be treated
-        // specially anyway, because the outermost ")" has a 
-        // different token type than inner ")" instances.
-        static int commentNest;
-  public  java.io.PrintStream debugStream = System.out;
-  public  void setDebugStream(java.io.PrintStream ds) { debugStream = ds; }
-private final int jjStopStringLiteralDfa_0(int pos, long active0)
-{
-   switch (pos)
-   {
-      default :
-         return -1;
-   }
-}
-private final int jjStartNfa_0(int pos, long active0)
-{
-   return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0), pos + 1);
-}
-private final int jjStopAtPos(int pos, int kind)
-{
-   jjmatchedKind = kind;
-   jjmatchedPos = pos;
-   return pos + 1;
-}
-private final int jjStartNfaWithStates_0(int pos, int kind, int state)
-{
-   jjmatchedKind = kind;
-   jjmatchedPos = pos;
-   try { curChar = input_stream.readChar(); }
-   catch(java.io.IOException e) { return pos + 1; }
-   return jjMoveNfa_0(state, pos + 1);
-}
-private final int jjMoveStringLiteralDfa0_0()
-{
-   switch(curChar)
-   {
-      case 10:
-         return jjStopAtPos(0, 2);
-      case 13:
-         return jjStopAtPos(0, 1);
-      case 34:
-         return jjStopAtPos(0, 28);
-      case 40:
-         return jjStopAtPos(0, 19);
-      case 44:
-         return jjStopAtPos(0, 3);
-      case 46:
-         return jjStopAtPos(0, 9);
-      case 58:
-         return jjStopAtPos(0, 4);
-      case 59:
-         return jjStopAtPos(0, 5);
-      case 60:
-         return jjStopAtPos(0, 6);
-      case 62:
-         return jjStopAtPos(0, 7);
-      case 64:
-         return jjStopAtPos(0, 8);
-      case 91:
-         return jjStopAtPos(0, 15);
-      default :
-         return jjMoveNfa_0(1, 0);
-   }
-}
-private final void jjCheckNAdd(int state)
-{
-   if (jjrounds[state] != jjround)
-   {
-      jjstateSet[jjnewStateCnt++] = state;
-      jjrounds[state] = jjround;
-   }
-}
-private final void jjAddStates(int start, int end)
-{
-   do {
-      jjstateSet[jjnewStateCnt++] = jjnextStates[start];
-   } while (start++ != end);
-}
-private final void jjCheckNAddTwoStates(int state1, int state2)
-{
-   jjCheckNAdd(state1);
-   jjCheckNAdd(state2);
-}
-private final void jjCheckNAddStates(int start, int end)
-{
-   do {
-      jjCheckNAdd(jjnextStates[start]);
-   } while (start++ != end);
-}
-private final void jjCheckNAddStates(int start)
-{
-   jjCheckNAdd(jjnextStates[start]);
-   jjCheckNAdd(jjnextStates[start + 1]);
-}
-private final int jjMoveNfa_0(int startState, int curPos)
-{
-   int[] nextStates;
-   int startsAt = 0;
-   jjnewStateCnt = 3;
-   int i = 1;
-   jjstateSet[0] = startState;
-   int j, kind = 0x7fffffff;
-   for (;;)
-   {
-      if (++jjround == 0x7fffffff)
-         ReInitRounds();
-      if (curChar < 64)
-      {
-         long l = 1L << curChar;
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 1:
-                  if ((0xa3ffacfa00000000L & l) != 0L)
-                  {
-                     if (kind > 14)
-                        kind = 14;
-                     jjCheckNAdd(2);
-                  }
-                  else if ((0x100000200L & l) != 0L)
-                  {
-                     if (kind > 10)
-                        kind = 10;
-                     jjCheckNAdd(0);
-                  }
-                  break;
-               case 0:
-                  if ((0x100000200L & l) == 0L)
-                     break;
-                  kind = 10;
-                  jjCheckNAdd(0);
-                  break;
-               case 2:
-                  if ((0xa3ffecfa00000000L & l) == 0L)
-                     break;
-                  if (kind > 14)
-                     kind = 14;
-                  jjCheckNAdd(2);
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else if (curChar < 128)
-      {
-         long l = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 1:
-               case 2:
-                  if ((0x7fffffffc7fffffeL & l) == 0L)
-                     break;
-                  if (kind > 14)
-                     kind = 14;
-                  jjCheckNAdd(2);
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else
-      {
-         int i2 = (curChar & 0xff) >> 6;
-         long l2 = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      if (kind != 0x7fffffff)
-      {
-         jjmatchedKind = kind;
-         jjmatchedPos = curPos;
-         kind = 0x7fffffff;
-      }
-      ++curPos;
-      if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
-         return curPos;
-      try { curChar = input_stream.readChar(); }
-      catch(java.io.IOException e) { return curPos; }
-   }
-}
-private final int jjStopStringLiteralDfa_2(int pos, long active0)
-{
-   switch (pos)
-   {
-      default :
-         return -1;
-   }
-}
-private final int jjStartNfa_2(int pos, long active0)
-{
-   return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1);
-}
-private final int jjStartNfaWithStates_2(int pos, int kind, int state)
-{
-   jjmatchedKind = kind;
-   jjmatchedPos = pos;
-   try { curChar = input_stream.readChar(); }
-   catch(java.io.IOException e) { return pos + 1; }
-   return jjMoveNfa_2(state, pos + 1);
-}
-private final int jjMoveStringLiteralDfa0_2()
-{
-   switch(curChar)
-   {
-      case 40:
-         return jjStopAtPos(0, 22);
-      case 41:
-         return jjStopAtPos(0, 20);
-      default :
-         return jjMoveNfa_2(0, 0);
-   }
-}
-static final long[] jjbitVec0 = {
-   0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL
-};
-private final int jjMoveNfa_2(int startState, int curPos)
-{
-   int[] nextStates;
-   int startsAt = 0;
-   jjnewStateCnt = 3;
-   int i = 1;
-   jjstateSet[0] = startState;
-   int j, kind = 0x7fffffff;
-   for (;;)
-   {
-      if (++jjround == 0x7fffffff)
-         ReInitRounds();
-      if (curChar < 64)
-      {
-         long l = 1L << curChar;
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if (kind > 23)
-                     kind = 23;
-                  break;
-               case 1:
-                  if (kind > 21)
-                     kind = 21;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else if (curChar < 128)
-      {
-         long l = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if (kind > 23)
-                     kind = 23;
-                  if (curChar == 92)
-                     jjstateSet[jjnewStateCnt++] = 1;
-                  break;
-               case 1:
-                  if (kind > 21)
-                     kind = 21;
-                  break;
-               case 2:
-                  if (kind > 23)
-                     kind = 23;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else
-      {
-         int i2 = (curChar & 0xff) >> 6;
-         long l2 = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if ((jjbitVec0[i2] & l2) != 0L && kind > 23)
-                     kind = 23;
-                  break;
-               case 1:
-                  if ((jjbitVec0[i2] & l2) != 0L && kind > 21)
-                     kind = 21;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      if (kind != 0x7fffffff)
-      {
-         jjmatchedKind = kind;
-         jjmatchedPos = curPos;
-         kind = 0x7fffffff;
-      }
-      ++curPos;
-      if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
-         return curPos;
-      try { curChar = input_stream.readChar(); }
-      catch(java.io.IOException e) { return curPos; }
-   }
-}
-private final int jjStopStringLiteralDfa_4(int pos, long active0)
-{
-   switch (pos)
-   {
-      default :
-         return -1;
-   }
-}
-private final int jjStartNfa_4(int pos, long active0)
-{
-   return jjMoveNfa_4(jjStopStringLiteralDfa_4(pos, active0), pos + 1);
-}
-private final int jjStartNfaWithStates_4(int pos, int kind, int state)
-{
-   jjmatchedKind = kind;
-   jjmatchedPos = pos;
-   try { curChar = input_stream.readChar(); }
-   catch(java.io.IOException e) { return pos + 1; }
-   return jjMoveNfa_4(state, pos + 1);
-}
-private final int jjMoveStringLiteralDfa0_4()
-{
-   switch(curChar)
-   {
-      case 34:
-         return jjStopAtPos(0, 31);
-      default :
-         return jjMoveNfa_4(0, 0);
-   }
-}
-private final int jjMoveNfa_4(int startState, int curPos)
-{
-   int[] nextStates;
-   int startsAt = 0;
-   jjnewStateCnt = 3;
-   int i = 1;
-   jjstateSet[0] = startState;
-   int j, kind = 0x7fffffff;
-   for (;;)
-   {
-      if (++jjround == 0x7fffffff)
-         ReInitRounds();
-      if (curChar < 64)
-      {
-         long l = 1L << curChar;
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-               case 2:
-                  if ((0xfffffffbffffffffL & l) == 0L)
-                     break;
-                  if (kind > 30)
-                     kind = 30;
-                  jjCheckNAdd(2);
-                  break;
-               case 1:
-                  if (kind > 29)
-                     kind = 29;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else if (curChar < 128)
-      {
-         long l = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if ((0xffffffffefffffffL & l) != 0L)
-                  {
-                     if (kind > 30)
-                        kind = 30;
-                     jjCheckNAdd(2);
-                  }
-                  else if (curChar == 92)
-                     jjstateSet[jjnewStateCnt++] = 1;
-                  break;
-               case 1:
-                  if (kind > 29)
-                     kind = 29;
-                  break;
-               case 2:
-                  if ((0xffffffffefffffffL & l) == 0L)
-                     break;
-                  if (kind > 30)
-                     kind = 30;
-                  jjCheckNAdd(2);
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else
-      {
-         int i2 = (curChar & 0xff) >> 6;
-         long l2 = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-               case 2:
-                  if ((jjbitVec0[i2] & l2) == 0L)
-                     break;
-                  if (kind > 30)
-                     kind = 30;
-                  jjCheckNAdd(2);
-                  break;
-               case 1:
-                  if ((jjbitVec0[i2] & l2) != 0L && kind > 29)
-                     kind = 29;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      if (kind != 0x7fffffff)
-      {
-         jjmatchedKind = kind;
-         jjmatchedPos = curPos;
-         kind = 0x7fffffff;
-      }
-      ++curPos;
-      if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
-         return curPos;
-      try { curChar = input_stream.readChar(); }
-      catch(java.io.IOException e) { return curPos; }
-   }
-}
-private final int jjStopStringLiteralDfa_3(int pos, long active0)
-{
-   switch (pos)
-   {
-      default :
-         return -1;
-   }
-}
-private final int jjStartNfa_3(int pos, long active0)
-{
-   return jjMoveNfa_3(jjStopStringLiteralDfa_3(pos, active0), pos + 1);
-}
-private final int jjStartNfaWithStates_3(int pos, int kind, int state)
-{
-   jjmatchedKind = kind;
-   jjmatchedPos = pos;
-   try { curChar = input_stream.readChar(); }
-   catch(java.io.IOException e) { return pos + 1; }
-   return jjMoveNfa_3(state, pos + 1);
-}
-private final int jjMoveStringLiteralDfa0_3()
-{
-   switch(curChar)
-   {
-      case 40:
-         return jjStopAtPos(0, 25);
-      case 41:
-         return jjStopAtPos(0, 26);
-      default :
-         return jjMoveNfa_3(0, 0);
-   }
-}
-private final int jjMoveNfa_3(int startState, int curPos)
-{
-   int[] nextStates;
-   int startsAt = 0;
-   jjnewStateCnt = 3;
-   int i = 1;
-   jjstateSet[0] = startState;
-   int j, kind = 0x7fffffff;
-   for (;;)
-   {
-      if (++jjround == 0x7fffffff)
-         ReInitRounds();
-      if (curChar < 64)
-      {
-         long l = 1L << curChar;
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if (kind > 27)
-                     kind = 27;
-                  break;
-               case 1:
-                  if (kind > 24)
-                     kind = 24;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else if (curChar < 128)
-      {
-         long l = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if (kind > 27)
-                     kind = 27;
-                  if (curChar == 92)
-                     jjstateSet[jjnewStateCnt++] = 1;
-                  break;
-               case 1:
-                  if (kind > 24)
-                     kind = 24;
-                  break;
-               case 2:
-                  if (kind > 27)
-                     kind = 27;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else
-      {
-         int i2 = (curChar & 0xff) >> 6;
-         long l2 = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if ((jjbitVec0[i2] & l2) != 0L && kind > 27)
-                     kind = 27;
-                  break;
-               case 1:
-                  if ((jjbitVec0[i2] & l2) != 0L && kind > 24)
-                     kind = 24;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      if (kind != 0x7fffffff)
-      {
-         jjmatchedKind = kind;
-         jjmatchedPos = curPos;
-         kind = 0x7fffffff;
-      }
-      ++curPos;
-      if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
-         return curPos;
-      try { curChar = input_stream.readChar(); }
-      catch(java.io.IOException e) { return curPos; }
-   }
-}
-private final int jjStopStringLiteralDfa_1(int pos, long active0)
-{
-   switch (pos)
-   {
-      default :
-         return -1;
-   }
-}
-private final int jjStartNfa_1(int pos, long active0)
-{
-   return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1);
-}
-private final int jjStartNfaWithStates_1(int pos, int kind, int state)
-{
-   jjmatchedKind = kind;
-   jjmatchedPos = pos;
-   try { curChar = input_stream.readChar(); }
-   catch(java.io.IOException e) { return pos + 1; }
-   return jjMoveNfa_1(state, pos + 1);
-}
-private final int jjMoveStringLiteralDfa0_1()
-{
-   switch(curChar)
-   {
-      case 93:
-         return jjStopAtPos(0, 18);
-      default :
-         return jjMoveNfa_1(0, 0);
-   }
-}
-private final int jjMoveNfa_1(int startState, int curPos)
-{
-   int[] nextStates;
-   int startsAt = 0;
-   jjnewStateCnt = 3;
-   int i = 1;
-   jjstateSet[0] = startState;
-   int j, kind = 0x7fffffff;
-   for (;;)
-   {
-      if (++jjround == 0x7fffffff)
-         ReInitRounds();
-      if (curChar < 64)
-      {
-         long l = 1L << curChar;
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if (kind > 17)
-                     kind = 17;
-                  break;
-               case 1:
-                  if (kind > 16)
-                     kind = 16;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else if (curChar < 128)
-      {
-         long l = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if ((0xffffffffc7ffffffL & l) != 0L)
-                  {
-                     if (kind > 17)
-                        kind = 17;
-                  }
-                  else if (curChar == 92)
-                     jjstateSet[jjnewStateCnt++] = 1;
-                  break;
-               case 1:
-                  if (kind > 16)
-                     kind = 16;
-                  break;
-               case 2:
-                  if ((0xffffffffc7ffffffL & l) != 0L && kind > 17)
-                     kind = 17;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else
-      {
-         int i2 = (curChar & 0xff) >> 6;
-         long l2 = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if ((jjbitVec0[i2] & l2) != 0L && kind > 17)
-                     kind = 17;
-                  break;
-               case 1:
-                  if ((jjbitVec0[i2] & l2) != 0L && kind > 16)
-                     kind = 16;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      if (kind != 0x7fffffff)
-      {
-         jjmatchedKind = kind;
-         jjmatchedPos = curPos;
-         kind = 0x7fffffff;
-      }
-      ++curPos;
-      if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
-         return curPos;
-      try { curChar = input_stream.readChar(); }
-      catch(java.io.IOException e) { return curPos; }
-   }
-}
-static final int[] jjnextStates = {
-};
-public static final String[] jjstrLiteralImages = {
-"", "\15", "\12", "\54", "\72", "\73", "\74", "\76", "\100", "\56", null, null, 
-null, null, null, null, null, null, null, null, null, null, null, null, null, null, 
-null, null, null, null, null, null, null, null, };
-public static final String[] lexStateNames = {
-   "DEFAULT", 
-   "INDOMAINLITERAL", 
-   "INCOMMENT", 
-   "NESTED_COMMENT", 
-   "INQUOTEDSTRING", 
-};
-public static final int[] jjnewLexState = {
-   -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1, -1, 0, 2, 0, -1, 3, -1, -1, 
-   -1, -1, -1, 4, -1, -1, 0, -1, -1, 
-};
-static final long[] jjtoToken = {
-   0x800443ffL, 
-};
-static final long[] jjtoSkip = {
-   0x100400L, 
-};
-static final long[] jjtoSpecial = {
-   0x400L, 
-};
-static final long[] jjtoMore = {
-   0x7feb8000L, 
-};
-protected SimpleCharStream input_stream;
-private final int[] jjrounds = new int[3];
-private final int[] jjstateSet = new int[6];
-StringBuffer image;
-int jjimageLen;
-int lengthOfMatch;
-protected char curChar;
-public AddressListParserTokenManager(SimpleCharStream stream){
-   if (SimpleCharStream.staticFlag)
-      throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer.");
-   input_stream = stream;
-}
-public AddressListParserTokenManager(SimpleCharStream stream, int lexState){
-   this(stream);
-   SwitchTo(lexState);
-}
-public void ReInit(SimpleCharStream stream)
-{
-   jjmatchedPos = jjnewStateCnt = 0;
-   curLexState = defaultLexState;
-   input_stream = stream;
-   ReInitRounds();
-}
-private final void ReInitRounds()
-{
-   int i;
-   jjround = 0x80000001;
-   for (i = 3; i-- > 0;)
-      jjrounds[i] = 0x80000000;
-}
-public void ReInit(SimpleCharStream stream, int lexState)
-{
-   ReInit(stream);
-   SwitchTo(lexState);
-}
-public void SwitchTo(int lexState)
-{
-   if (lexState >= 5 || lexState < 0)
-      throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE);
-   else
-      curLexState = lexState;
-}
-
-protected Token jjFillToken()
-{
-   Token t = Token.newToken(jjmatchedKind);
-   t.kind = jjmatchedKind;
-   String im = jjstrLiteralImages[jjmatchedKind];
-   t.image = (im == null) ? input_stream.GetImage() : im;
-   t.beginLine = input_stream.getBeginLine();
-   t.beginColumn = input_stream.getBeginColumn();
-   t.endLine = input_stream.getEndLine();
-   t.endColumn = input_stream.getEndColumn();
-   return t;
-}
-
-int curLexState = 0;
-int defaultLexState = 0;
-int jjnewStateCnt;
-int jjround;
-int jjmatchedPos;
-int jjmatchedKind;
-
-public Token getNextToken() 
-{
-  int kind;
-  Token specialToken = null;
-  Token matchedToken;
-  int curPos = 0;
-
-  EOFLoop :
-  for (;;)
-  {   
-   try   
-   {     
-      curChar = input_stream.BeginToken();
-   }     
-   catch(java.io.IOException e)
-   {        
-      jjmatchedKind = 0;
-      matchedToken = jjFillToken();
-      matchedToken.specialToken = specialToken;
-      return matchedToken;
-   }
-   image = null;
-   jjimageLen = 0;
-
-   for (;;)
-   {
-     switch(curLexState)
-     {
-       case 0:
-         jjmatchedKind = 0x7fffffff;
-         jjmatchedPos = 0;
-         curPos = jjMoveStringLiteralDfa0_0();
-         break;
-       case 1:
-         jjmatchedKind = 0x7fffffff;
-         jjmatchedPos = 0;
-         curPos = jjMoveStringLiteralDfa0_1();
-         break;
-       case 2:
-         jjmatchedKind = 0x7fffffff;
-         jjmatchedPos = 0;
-         curPos = jjMoveStringLiteralDfa0_2();
-         break;
-       case 3:
-         jjmatchedKind = 0x7fffffff;
-         jjmatchedPos = 0;
-         curPos = jjMoveStringLiteralDfa0_3();
-         break;
-       case 4:
-         jjmatchedKind = 0x7fffffff;
-         jjmatchedPos = 0;
-         curPos = jjMoveStringLiteralDfa0_4();
-         break;
-     }
-     if (jjmatchedKind != 0x7fffffff)
-     {
-        if (jjmatchedPos + 1 < curPos)
-           input_stream.backup(curPos - jjmatchedPos - 1);
-        if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
-        {
-           matchedToken = jjFillToken();
-           matchedToken.specialToken = specialToken;
-           TokenLexicalActions(matchedToken);
-       if (jjnewLexState[jjmatchedKind] != -1)
-         curLexState = jjnewLexState[jjmatchedKind];
-           return matchedToken;
-        }
-        else if ((jjtoSkip[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
-        {
-           if ((jjtoSpecial[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
-           {
-              matchedToken = jjFillToken();
-              if (specialToken == null)
-                 specialToken = matchedToken;
-              else
-              {
-                 matchedToken.specialToken = specialToken;
-                 specialToken = (specialToken.next = matchedToken);
-              }
-           }
-         if (jjnewLexState[jjmatchedKind] != -1)
-           curLexState = jjnewLexState[jjmatchedKind];
-           continue EOFLoop;
-        }
-        MoreLexicalActions();
-      if (jjnewLexState[jjmatchedKind] != -1)
-        curLexState = jjnewLexState[jjmatchedKind];
-        curPos = 0;
-        jjmatchedKind = 0x7fffffff;
-        try {
-           curChar = input_stream.readChar();
-           continue;
-        }
-        catch (java.io.IOException e1) { }
-     }
-     int error_line = input_stream.getEndLine();
-     int error_column = input_stream.getEndColumn();
-     String error_after = null;
-     boolean EOFSeen = false;
-     try { input_stream.readChar(); input_stream.backup(1); }
-     catch (java.io.IOException e1) {
-        EOFSeen = true;
-        error_after = curPos <= 1 ? "" : input_stream.GetImage();
-        if (curChar == '\n' || curChar == '\r') {
-           error_line++;
-           error_column = 0;
-        }
-        else
-           error_column++;
-     }
-     if (!EOFSeen) {
-        input_stream.backup(1);
-        error_after = curPos <= 1 ? "" : input_stream.GetImage();
-     }
-     throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR);
-   }
-  }
-}
-
-void MoreLexicalActions()
-{
-   jjimageLen += (lengthOfMatch = jjmatchedPos + 1);
-   switch(jjmatchedKind)
-   {
-      case 16 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-                          image.deleteCharAt(image.length() - 2);
-         break;
-      case 21 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-                          image.deleteCharAt(image.length() - 2);
-         break;
-      case 22 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-              commentNest = 1;
-         break;
-      case 24 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-                          image.deleteCharAt(image.length() - 2);
-         break;
-      case 25 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-              ++commentNest;
-         break;
-      case 26 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-              --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT);
-         break;
-      case 28 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-               image.deleteCharAt(image.length() - 1);
-         break;
-      case 29 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-                          image.deleteCharAt(image.length() - 2);
-         break;
-      default : 
-         break;
-   }
-}
-void TokenLexicalActions(Token matchedToken)
-{
-   switch(jjmatchedKind)
-   {
-      case 18 :
-        if (image == null)
-            image = new StringBuffer();
-            image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1)));
-                                 matchedToken.image = image.toString();
-         break;
-      case 31 :
-        if (image == null)
-            image = new StringBuffer();
-            image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1)));
-                                 matchedToken.image = image.substring(0, image.length() - 1);
-         break;
-      default : 
-         break;
-   }
-}
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java
deleted file mode 100644
index 5987f19..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/* Generated By:JJTree: Do not edit this line. /Users/jason/Projects/apache-mime4j-0.3/target/generated-sources/jjtree/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java */
-
-package org.apache.james.mime4j.field.address.parser;
-
-public interface AddressListParserTreeConstants
-{
-  public int JJTVOID = 0;
-  public int JJTADDRESS_LIST = 1;
-  public int JJTADDRESS = 2;
-  public int JJTMAILBOX = 3;
-  public int JJTNAME_ADDR = 4;
-  public int JJTGROUP_BODY = 5;
-  public int JJTANGLE_ADDR = 6;
-  public int JJTROUTE = 7;
-  public int JJTPHRASE = 8;
-  public int JJTADDR_SPEC = 9;
-  public int JJTLOCAL_PART = 10;
-  public int JJTDOMAIN = 11;
-
-
-  public String[] jjtNodeName = {
-    "void",
-    "address_list",
-    "address",
-    "mailbox",
-    "name_addr",
-    "group_body",
-    "angle_addr",
-    "route",
-    "phrase",
-    "addr_spec",
-    "local_part",
-    "domain",
-  };
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java
deleted file mode 100644
index 8ec2fe7..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Generated By:JJTree: Do not edit this line. /Users/jason/Projects/apache-mime4j-0.3/target/generated-sources/jjtree/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java */
-
-package org.apache.james.mime4j.field.address.parser;
-
-public interface AddressListParserVisitor
-{
-  public Object visit(SimpleNode node, Object data);
-  public Object visit(ASTaddress_list node, Object data);
-  public Object visit(ASTaddress node, Object data);
-  public Object visit(ASTmailbox node, Object data);
-  public Object visit(ASTname_addr node, Object data);
-  public Object visit(ASTgroup_body node, Object data);
-  public Object visit(ASTangle_addr node, Object data);
-  public Object visit(ASTroute node, Object data);
-  public Object visit(ASTphrase node, Object data);
-  public Object visit(ASTaddr_spec node, Object data);
-  public Object visit(ASTlocal_part node, Object data);
-  public Object visit(ASTdomain node, Object data);
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/BaseNode.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/BaseNode.java
deleted file mode 100644
index 7809746..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/BaseNode.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field.address.parser;
-
-import org.apache.james.mime4j.field.address.parser.Node;
-import org.apache.james.mime4j.field.address.parser.Token;
-
-public abstract class BaseNode implements Node {
-  
-  public Token firstToken;
-  public Token lastToken;
-
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java
deleted file mode 100644
index 08b5c5b..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/* Generated By:JJTree: Do not edit this line. /Users/jason/Projects/apache-mime4j-0.3/target/generated-sources/jjtree/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java */
-
-package org.apache.james.mime4j.field.address.parser;
-
-class JJTAddressListParserState {
-  private java.util.Stack<Node> nodes;
-  private java.util.Stack<Integer> marks;
-
-  private int sp;		// number of nodes on stack
-  private int mk;		// current mark
-  private boolean node_created;
-
-  JJTAddressListParserState() {
-    nodes = new java.util.Stack<Node>();
-    marks = new java.util.Stack<Integer>();
-    sp = 0;
-    mk = 0;
-  }
-
-  /* Determines whether the current node was actually closed and
-     pushed.  This should only be called in the final user action of a
-     node scope.  */
-  boolean nodeCreated() {
-    return node_created;
-  }
-
-  /* Call this to reinitialize the node stack.  It is called
-     automatically by the parser's ReInit() method. */
-  void reset() {
-    nodes.removeAllElements();
-    marks.removeAllElements();
-    sp = 0;
-    mk = 0;
-  }
-
-  /* Returns the root node of the AST.  It only makes sense to call
-     this after a successful parse. */
-  Node rootNode() {
-    return nodes.elementAt(0);
-  }
-
-  /* Pushes a node on to the stack. */
-  void pushNode(Node n) {
-    nodes.push(n);
-    ++sp;
-  }
-
-  /* Returns the node on the top of the stack, and remove it from the
-     stack.  */
-  Node popNode() {
-    if (--sp < mk) {
-      mk = marks.pop().intValue();
-    }
-    return nodes.pop();
-  }
-
-  /* Returns the node currently on the top of the stack. */
-  Node peekNode() {
-    return nodes.peek();
-  }
-
-  /* Returns the number of children on the stack in the current node
-     scope. */
-  int nodeArity() {
-    return sp - mk;
-  }
-
-
-  void clearNodeScope(Node n) {
-    while (sp > mk) {
-      popNode();
-    }
-    mk = marks.pop().intValue();
-  }
-
-
-  void openNodeScope(Node n) {
-    marks.push(new Integer(mk));
-    mk = sp;
-    n.jjtOpen();
-  }
-
-
-  /* A definite node is constructed from a specified number of
-     children.  That number of nodes are popped from the stack and
-     made the children of the definite node.  Then the definite node
-     is pushed on to the stack. */
-  void closeNodeScope(Node n, int num) {
-    mk = marks.pop().intValue();
-    while (num-- > 0) {
-      Node c = popNode();
-      c.jjtSetParent(n);
-      n.jjtAddChild(c, num);
-    }
-    n.jjtClose();
-    pushNode(n);
-    node_created = true;
-  }
-
-
-  /* A conditional node is constructed if its condition is true.  All
-     the nodes that have been pushed since the node was opened are
-     made children of the the conditional node, which is then pushed
-     on to the stack.  If the condition is false the node is not
-     constructed and they are left on the stack. */
-  void closeNodeScope(Node n, boolean condition) {
-    if (condition) {
-      int a = nodeArity();
-      mk = marks.pop().intValue();
-      while (a-- > 0) {
-	Node c = popNode();
-	c.jjtSetParent(n);
-	n.jjtAddChild(c, a);
-      }
-      n.jjtClose();
-      pushNode(n);
-      node_created = true;
-    } else {
-      mk = marks.pop().intValue();
-      node_created = false;
-    }
-  }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Node.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Node.java
deleted file mode 100644
index 1588920..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Node.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/* Generated By:JJTree: Do not edit this line. Node.java */
-
-package org.apache.james.mime4j.field.address.parser;
-
-/* All AST nodes must implement this interface.  It provides basic
-   machinery for constructing the parent and child relationships
-   between nodes. */
-
-public interface Node {
-
-  /** This method is called after the node has been made the current
-    node.  It indicates that child nodes can now be added to it. */
-  public void jjtOpen();
-
-  /** This method is called after all the child nodes have been
-    added. */
-  public void jjtClose();
-
-  /** This pair of methods are used to inform the node of its
-    parent. */
-  public void jjtSetParent(Node n);
-  public Node jjtGetParent();
-
-  /** This method tells the node to add its argument to the node's
-    list of children.  */
-  public void jjtAddChild(Node n, int i);
-
-  /** This method returns a child node.  The children are numbered
-     from zero, left to right. */
-  public Node jjtGetChild(int i);
-
-  /** Return the number of children the node has. */
-  public int jjtGetNumChildren();
-
-  /** Accept the visitor. **/
-  public Object jjtAccept(AddressListParserVisitor visitor, Object data);
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ParseException.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ParseException.java
deleted file mode 100644
index e20146f..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ParseException.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.0 */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.address.parser;
-
-/**
- * This exception is thrown when parse errors are encountered.
- * You can explicitly create objects of this exception type by
- * calling the method generateParseException in the generated
- * parser.
- *
- * You can modify this class to customize your error reporting
- * mechanisms so long as you retain the public fields.
- */
-public class ParseException extends Exception {
-
-  /**
-   * This constructor is used by the method "generateParseException"
-   * in the generated parser.  Calling this constructor generates
-   * a new object of this type with the fields "currentToken",
-   * "expectedTokenSequences", and "tokenImage" set.  The boolean
-   * flag "specialConstructor" is also set to true to indicate that
-   * this constructor was used to create this object.
-   * This constructor calls its super class with the empty string
-   * to force the "toString" method of parent class "Throwable" to
-   * print the error message in the form:
-   *     ParseException: <result of getMessage>
-   */
-  public ParseException(Token currentTokenVal,
-                        int[][] expectedTokenSequencesVal,
-                        String[] tokenImageVal
-                       )
-  {
-    super("");
-    specialConstructor = true;
-    currentToken = currentTokenVal;
-    expectedTokenSequences = expectedTokenSequencesVal;
-    tokenImage = tokenImageVal;
-  }
-
-  /**
-   * The following constructors are for use by you for whatever
-   * purpose you can think of.  Constructing the exception in this
-   * manner makes the exception behave in the normal way - i.e., as
-   * documented in the class "Throwable".  The fields "errorToken",
-   * "expectedTokenSequences", and "tokenImage" do not contain
-   * relevant information.  The JavaCC generated code does not use
-   * these constructors.
-   */
-
-  public ParseException() {
-    super();
-    specialConstructor = false;
-  }
-
-  public ParseException(String message) {
-    super(message);
-    specialConstructor = false;
-  }
-
-  /**
-   * This variable determines which constructor was used to create
-   * this object and thereby affects the semantics of the
-   * "getMessage" method (see below).
-   */
-  protected boolean specialConstructor;
-
-  /**
-   * This is the last token that has been consumed successfully.  If
-   * this object has been created due to a parse error, the token
-   * followng this token will (therefore) be the first error token.
-   */
-  public Token currentToken;
-
-  /**
-   * Each entry in this array is an array of integers.  Each array
-   * of integers represents a sequence of tokens (by their ordinal
-   * values) that is expected at this point of the parse.
-   */
-  public int[][] expectedTokenSequences;
-
-  /**
-   * This is a reference to the "tokenImage" array of the generated
-   * parser within which the parse error occurred.  This array is
-   * defined in the generated ...Constants interface.
-   */
-  public String[] tokenImage;
-
-  /**
-   * This method has the standard behavior when this object has been
-   * created using the standard constructors.  Otherwise, it uses
-   * "currentToken" and "expectedTokenSequences" to generate a parse
-   * error message and returns it.  If this object has been created
-   * due to a parse error, and you do not catch it (it gets thrown
-   * from the parser), then this method is called during the printing
-   * of the final stack trace, and hence the correct error message
-   * gets displayed.
-   */
-  public String getMessage() {
-    if (!specialConstructor) {
-      return super.getMessage();
-    }
-    StringBuffer expected = new StringBuffer();
-    int maxSize = 0;
-    for (int i = 0; i < expectedTokenSequences.length; i++) {
-      if (maxSize < expectedTokenSequences[i].length) {
-        maxSize = expectedTokenSequences[i].length;
-      }
-      for (int j = 0; j < expectedTokenSequences[i].length; j++) {
-        expected.append(tokenImage[expectedTokenSequences[i][j]]).append(" ");
-      }
-      if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) {
-        expected.append("...");
-      }
-      expected.append(eol).append("    ");
-    }
-    String retval = "Encountered \"";
-    Token tok = currentToken.next;
-    for (int i = 0; i < maxSize; i++) {
-      if (i != 0) retval += " ";
-      if (tok.kind == 0) {
-        retval += tokenImage[0];
-        break;
-      }
-      retval += add_escapes(tok.image);
-      tok = tok.next; 
-    }
-    retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn;
-    retval += "." + eol;
-    if (expectedTokenSequences.length == 1) {
-      retval += "Was expecting:" + eol + "    ";
-    } else {
-      retval += "Was expecting one of:" + eol + "    ";
-    }
-    retval += expected.toString();
-    return retval;
-  }
-
-  /**
-   * The end of line string for this machine.
-   */
-  protected String eol = System.getProperty("line.separator", "\n");
- 
-  /**
-   * Used to convert raw characters to their escaped version
-   * when these raw version cannot be used as part of an ASCII
-   * string literal.
-   */
-  protected String add_escapes(String str) {
-      StringBuffer retval = new StringBuffer();
-      char ch;
-      for (int i = 0; i < str.length(); i++) {
-        switch (str.charAt(i))
-        {
-           case 0 :
-              continue;
-           case '\b':
-              retval.append("\\b");
-              continue;
-           case '\t':
-              retval.append("\\t");
-              continue;
-           case '\n':
-              retval.append("\\n");
-              continue;
-           case '\f':
-              retval.append("\\f");
-              continue;
-           case '\r':
-              retval.append("\\r");
-              continue;
-           case '\"':
-              retval.append("\\\"");
-              continue;
-           case '\'':
-              retval.append("\\\'");
-              continue;
-           case '\\':
-              retval.append("\\\\");
-              continue;
-           default:
-              if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
-                 String s = "0000" + Integer.toString(ch, 16);
-                 retval.append("\\u" + s.substring(s.length() - 4, s.length()));
-              } else {
-                 retval.append(ch);
-              }
-              continue;
-        }
-      }
-      return retval.toString();
-   }
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java
deleted file mode 100644
index c9ba0b4..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java
+++ /dev/null
@@ -1,454 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 4.0 */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.address.parser;
-
-/**
- * An implementation of interface CharStream, where the stream is assumed to
- * contain only ASCII characters (without unicode processing).
- */
-
-public class SimpleCharStream
-{
-  public static final boolean staticFlag = false;
-  int bufsize;
-  int available;
-  int tokenBegin;
-  public int bufpos = -1;
-  protected int bufline[];
-  protected int bufcolumn[];
-
-  protected int column = 0;
-  protected int line = 1;
-
-  protected boolean prevCharIsCR = false;
-  protected boolean prevCharIsLF = false;
-
-  protected java.io.Reader inputStream;
-
-  protected char[] buffer;
-  protected int maxNextCharInd = 0;
-  protected int inBuf = 0;
-  protected int tabSize = 8;
-
-  protected void setTabSize(int i) { tabSize = i; }
-  protected int getTabSize(int i) { return tabSize; }
-
-
-  protected void ExpandBuff(boolean wrapAround)
-  {
-     char[] newbuffer = new char[bufsize + 2048];
-     int newbufline[] = new int[bufsize + 2048];
-     int newbufcolumn[] = new int[bufsize + 2048];
-
-     try
-     {
-        if (wrapAround)
-        {
-           System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin);
-           System.arraycopy(buffer, 0, newbuffer,
-                                             bufsize - tokenBegin, bufpos);
-           buffer = newbuffer;
-
-           System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin);
-           System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos);
-           bufline = newbufline;
-
-           System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin);
-           System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos);
-           bufcolumn = newbufcolumn;
-
-           maxNextCharInd = (bufpos += (bufsize - tokenBegin));
-        }
-        else
-        {
-           System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin);
-           buffer = newbuffer;
-
-           System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin);
-           bufline = newbufline;
-
-           System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin);
-           bufcolumn = newbufcolumn;
-
-           maxNextCharInd = (bufpos -= tokenBegin);
-        }
-     }
-     catch (Throwable t)
-     {
-        throw new Error(t.getMessage());
-     }
-
-
-     bufsize += 2048;
-     available = bufsize;
-     tokenBegin = 0;
-  }
-
-  protected void FillBuff() throws java.io.IOException
-  {
-     if (maxNextCharInd == available)
-     {
-        if (available == bufsize)
-        {
-           if (tokenBegin > 2048)
-           {
-              bufpos = maxNextCharInd = 0;
-              available = tokenBegin;
-           }
-           else if (tokenBegin < 0)
-              bufpos = maxNextCharInd = 0;
-           else
-              ExpandBuff(false);
-        }
-        else if (available > tokenBegin)
-           available = bufsize;
-        else if ((tokenBegin - available) < 2048)
-           ExpandBuff(true);
-        else
-           available = tokenBegin;
-     }
-
-     int i;
-     try {
-        if ((i = inputStream.read(buffer, maxNextCharInd,
-                                    available - maxNextCharInd)) == -1)
-        {
-           inputStream.close();
-           throw new java.io.IOException();
-        }
-        else
-           maxNextCharInd += i;
-        return;
-     }
-     catch(java.io.IOException e) {
-        --bufpos;
-        backup(0);
-        if (tokenBegin == -1)
-           tokenBegin = bufpos;
-        throw e;
-     }
-  }
-
-  public char BeginToken() throws java.io.IOException
-  {
-     tokenBegin = -1;
-     char c = readChar();
-     tokenBegin = bufpos;
-
-     return c;
-  }
-
-  protected void UpdateLineColumn(char c)
-  {
-     column++;
-
-     if (prevCharIsLF)
-     {
-        prevCharIsLF = false;
-        line += (column = 1);
-     }
-     else if (prevCharIsCR)
-     {
-        prevCharIsCR = false;
-        if (c == '\n')
-        {
-           prevCharIsLF = true;
-        }
-        else
-           line += (column = 1);
-     }
-
-     switch (c)
-     {
-        case '\r' :
-           prevCharIsCR = true;
-           break;
-        case '\n' :
-           prevCharIsLF = true;
-           break;
-        case '\t' :
-           column--;
-           column += (tabSize - (column % tabSize));
-           break;
-        default :
-           break;
-     }
-
-     bufline[bufpos] = line;
-     bufcolumn[bufpos] = column;
-  }
-
-  public char readChar() throws java.io.IOException
-  {
-     if (inBuf > 0)
-     {
-        --inBuf;
-
-        if (++bufpos == bufsize)
-           bufpos = 0;
-
-        return buffer[bufpos];
-     }
-
-     if (++bufpos >= maxNextCharInd)
-        FillBuff();
-
-     char c = buffer[bufpos];
-
-     UpdateLineColumn(c);
-     return (c);
-  }
-
-  /**
-   * @deprecated 
-   * @see #getEndColumn
-   */
-  @Deprecated
-  public int getColumn() {
-     return bufcolumn[bufpos];
-  }
-
-  /**
-   * @deprecated 
-   * @see #getEndLine
-   */
-  @Deprecated
-  public int getLine() {
-     return bufline[bufpos];
-  }
-
-  public int getEndColumn() {
-     return bufcolumn[bufpos];
-  }
-
-  public int getEndLine() {
-     return bufline[bufpos];
-  }
-
-  public int getBeginColumn() {
-     return bufcolumn[tokenBegin];
-  }
-
-  public int getBeginLine() {
-     return bufline[tokenBegin];
-  }
-
-  public void backup(int amount) {
-
-    inBuf += amount;
-    if ((bufpos -= amount) < 0)
-       bufpos += bufsize;
-  }
-
-  public SimpleCharStream(java.io.Reader dstream, int startline,
-  int startcolumn, int buffersize)
-  {
-    inputStream = dstream;
-    line = startline;
-    column = startcolumn - 1;
-
-    available = bufsize = buffersize;
-    buffer = new char[buffersize];
-    bufline = new int[buffersize];
-    bufcolumn = new int[buffersize];
-  }
-
-  public SimpleCharStream(java.io.Reader dstream, int startline,
-                          int startcolumn)
-  {
-     this(dstream, startline, startcolumn, 4096);
-  }
-
-  public SimpleCharStream(java.io.Reader dstream)
-  {
-     this(dstream, 1, 1, 4096);
-  }
-  public void ReInit(java.io.Reader dstream, int startline,
-  int startcolumn, int buffersize)
-  {
-    inputStream = dstream;
-    line = startline;
-    column = startcolumn - 1;
-
-    if (buffer == null || buffersize != buffer.length)
-    {
-      available = bufsize = buffersize;
-      buffer = new char[buffersize];
-      bufline = new int[buffersize];
-      bufcolumn = new int[buffersize];
-    }
-    prevCharIsLF = prevCharIsCR = false;
-    tokenBegin = inBuf = maxNextCharInd = 0;
-    bufpos = -1;
-  }
-
-  public void ReInit(java.io.Reader dstream, int startline,
-                     int startcolumn)
-  {
-     ReInit(dstream, startline, startcolumn, 4096);
-  }
-
-  public void ReInit(java.io.Reader dstream)
-  {
-     ReInit(dstream, 1, 1, 4096);
-  }
-  public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline,
-  int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException
-  {
-     this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize);
-  }
-
-  public SimpleCharStream(java.io.InputStream dstream, int startline,
-  int startcolumn, int buffersize)
-  {
-     this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize);
-  }
-
-  public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline,
-                          int startcolumn) throws java.io.UnsupportedEncodingException
-  {
-     this(dstream, encoding, startline, startcolumn, 4096);
-  }
-
-  public SimpleCharStream(java.io.InputStream dstream, int startline,
-                          int startcolumn)
-  {
-     this(dstream, startline, startcolumn, 4096);
-  }
-
-  public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException
-  {
-     this(dstream, encoding, 1, 1, 4096);
-  }
-
-  public SimpleCharStream(java.io.InputStream dstream)
-  {
-     this(dstream, 1, 1, 4096);
-  }
-
-  public void ReInit(java.io.InputStream dstream, String encoding, int startline,
-                          int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException
-  {
-     ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize);
-  }
-
-  public void ReInit(java.io.InputStream dstream, int startline,
-                          int startcolumn, int buffersize)
-  {
-     ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize);
-  }
-
-  public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException
-  {
-     ReInit(dstream, encoding, 1, 1, 4096);
-  }
-
-  public void ReInit(java.io.InputStream dstream)
-  {
-     ReInit(dstream, 1, 1, 4096);
-  }
-  public void ReInit(java.io.InputStream dstream, String encoding, int startline,
-                     int startcolumn) throws java.io.UnsupportedEncodingException
-  {
-     ReInit(dstream, encoding, startline, startcolumn, 4096);
-  }
-  public void ReInit(java.io.InputStream dstream, int startline,
-                     int startcolumn)
-  {
-     ReInit(dstream, startline, startcolumn, 4096);
-  }
-  public String GetImage()
-  {
-     if (bufpos >= tokenBegin)
-        return new String(buffer, tokenBegin, bufpos - tokenBegin + 1);
-     else
-        return new String(buffer, tokenBegin, bufsize - tokenBegin) +
-                              new String(buffer, 0, bufpos + 1);
-  }
-
-  public char[] GetSuffix(int len)
-  {
-     char[] ret = new char[len];
-
-     if ((bufpos + 1) >= len)
-        System.arraycopy(buffer, bufpos - len + 1, ret, 0, len);
-     else
-     {
-        System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0,
-                                                          len - bufpos - 1);
-        System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1);
-     }
-
-     return ret;
-  }
-
-  public void Done()
-  {
-     buffer = null;
-     bufline = null;
-     bufcolumn = null;
-  }
-
-  /**
-   * Method to adjust line and column numbers for the start of a token.
-   */
-  public void adjustBeginLineColumn(int newLine, int newCol)
-  {
-     int start = tokenBegin;
-     int len;
-
-     if (bufpos >= tokenBegin)
-     {
-        len = bufpos - tokenBegin + inBuf + 1;
-     }
-     else
-     {
-        len = bufsize - tokenBegin + bufpos + 1 + inBuf;
-     }
-
-     int i = 0, j = 0, k = 0;
-     int nextColDiff = 0, columnDiff = 0;
-
-     while (i < len &&
-            bufline[j = start % bufsize] == bufline[k = ++start % bufsize])
-     {
-        bufline[j] = newLine;
-        nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j];
-        bufcolumn[j] = newCol + columnDiff;
-        columnDiff = nextColDiff;
-        i++;
-     } 
-
-     if (i < len)
-     {
-        bufline[j] = newLine++;
-        bufcolumn[j] = newCol + columnDiff;
-
-        while (i++ < len)
-        {
-           if (bufline[j = start % bufsize] != bufline[++start % bufsize])
-              bufline[j] = newLine++;
-           else
-              bufline[j] = newLine;
-        }
-     }
-
-     line = bufline[j];
-     column = bufcolumn[j];
-  }
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java
deleted file mode 100644
index 9bf537e..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/* Generated By:JJTree: Do not edit this line. SimpleNode.java */
-
-package org.apache.james.mime4j.field.address.parser;
-
-public class SimpleNode extends org.apache.james.mime4j.field.address.parser.BaseNode implements Node {
-  protected Node parent;
-  protected Node[] children;
-  protected int id;
-  protected AddressListParser parser;
-
-  public SimpleNode(int i) {
-    id = i;
-  }
-
-  public SimpleNode(AddressListParser p, int i) {
-    this(i);
-    parser = p;
-  }
-
-  public void jjtOpen() {
-  }
-
-  public void jjtClose() {
-  }
-  
-  public void jjtSetParent(Node n) { parent = n; }
-  public Node jjtGetParent() { return parent; }
-
-  public void jjtAddChild(Node n, int i) {
-    if (children == null) {
-      children = new Node[i + 1];
-    } else if (i >= children.length) {
-      Node c[] = new Node[i + 1];
-      System.arraycopy(children, 0, c, 0, children.length);
-      children = c;
-    }
-    children[i] = n;
-  }
-
-  public Node jjtGetChild(int i) {
-    return children[i];
-  }
-
-  public int jjtGetNumChildren() {
-    return (children == null) ? 0 : children.length;
-  }
-
-  /** Accept the visitor. **/
-  public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
-    return visitor.visit(this, data);
-  }
-
-  /** Accept the visitor. **/
-  public Object childrenAccept(AddressListParserVisitor visitor, Object data) {
-    if (children != null) {
-      for (int i = 0; i < children.length; ++i) {
-        children[i].jjtAccept(visitor, data);
-      }
-    }
-    return data;
-  }
-
-  /* You can override these two methods in subclasses of SimpleNode to
-     customize the way the node appears when the tree is dumped.  If
-     your output uses more than one line you should override
-     toString(String), otherwise overriding toString() is probably all
-     you need to do. */
-
-  public String toString() { return AddressListParserTreeConstants.jjtNodeName[id]; }
-  public String toString(String prefix) { return prefix + toString(); }
-
-  /* Override this method if you want to customize how the node dumps
-     out its children. */
-
-  public void dump(String prefix) {
-    System.out.println(toString(prefix));
-    if (children != null) {
-      for (int i = 0; i < children.length; ++i) {
-	SimpleNode n = (SimpleNode)children[i];
-	if (n != null) {
-	  n.dump(prefix + " ");
-	}
-      }
-    }
-  }
-}
-
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Token.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Token.java
deleted file mode 100644
index 2382e8e..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Token.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. Token.java Version 3.0 */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.address.parser;
-
-/**
- * Describes the input token stream.
- */
-
-public class Token {
-
-  /**
-   * An integer that describes the kind of this token.  This numbering
-   * system is determined by JavaCCParser, and a table of these numbers is
-   * stored in the file ...Constants.java.
-   */
-  public int kind;
-
-  /**
-   * beginLine and beginColumn describe the position of the first character
-   * of this token; endLine and endColumn describe the position of the
-   * last character of this token.
-   */
-  public int beginLine, beginColumn, endLine, endColumn;
-
-  /**
-   * The string image of the token.
-   */
-  public String image;
-
-  /**
-   * A reference to the next regular (non-special) token from the input
-   * stream.  If this is the last token from the input stream, or if the
-   * token manager has not read tokens beyond this one, this field is
-   * set to null.  This is true only if this token is also a regular
-   * token.  Otherwise, see below for a description of the contents of
-   * this field.
-   */
-  public Token next;
-
-  /**
-   * This field is used to access special tokens that occur prior to this
-   * token, but after the immediately preceding regular (non-special) token.
-   * If there are no such special tokens, this field is set to null.
-   * When there are more than one such special token, this field refers
-   * to the last of these special tokens, which in turn refers to the next
-   * previous special token through its specialToken field, and so on
-   * until the first special token (whose specialToken field is null).
-   * The next fields of special tokens refer to other special tokens that
-   * immediately follow it (without an intervening regular token).  If there
-   * is no such token, this field is null.
-   */
-  public Token specialToken;
-
-  /**
-   * Returns the image.
-   */
-  public String toString()
-  {
-     return image;
-  }
-
-  /**
-   * Returns a new Token object, by default. However, if you want, you
-   * can create and return subclass objects based on the value of ofKind.
-   * Simply add the cases to the switch for all those special cases.
-   * For example, if you have a subclass of Token called IDToken that
-   * you want to create if ofKind is ID, simlpy add something like :
-   *
-   *    case MyParserConstants.ID : return new IDToken();
-   *
-   * to the following switch statement. Then you can cast matchedToken
-   * variable to the appropriate type and use it in your lexical actions.
-   */
-  public static final Token newToken(int ofKind)
-  {
-     switch(ofKind)
-     {
-       default : return new Token();
-     }
-  }
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java
deleted file mode 100644
index 0299c85..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 3.0 */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.address.parser;
-
-public class TokenMgrError extends Error
-{
-   /*
-    * Ordinals for various reasons why an Error of this type can be thrown.
-    */
-
-   /**
-    * Lexical error occured.
-    */
-   static final int LEXICAL_ERROR = 0;
-
-   /**
-    * An attempt wass made to create a second instance of a static token manager.
-    */
-   static final int STATIC_LEXER_ERROR = 1;
-
-   /**
-    * Tried to change to an invalid lexical state.
-    */
-   static final int INVALID_LEXICAL_STATE = 2;
-
-   /**
-    * Detected (and bailed out of) an infinite loop in the token manager.
-    */
-   static final int LOOP_DETECTED = 3;
-
-   /**
-    * Indicates the reason why the exception is thrown. It will have
-    * one of the above 4 values.
-    */
-   int errorCode;
-
-   /**
-    * Replaces unprintable characters by their espaced (or unicode escaped)
-    * equivalents in the given string
-    */
-   protected static final String addEscapes(String str) {
-      StringBuffer retval = new StringBuffer();
-      char ch;
-      for (int i = 0; i < str.length(); i++) {
-        switch (str.charAt(i))
-        {
-           case 0 :
-              continue;
-           case '\b':
-              retval.append("\\b");
-              continue;
-           case '\t':
-              retval.append("\\t");
-              continue;
-           case '\n':
-              retval.append("\\n");
-              continue;
-           case '\f':
-              retval.append("\\f");
-              continue;
-           case '\r':
-              retval.append("\\r");
-              continue;
-           case '\"':
-              retval.append("\\\"");
-              continue;
-           case '\'':
-              retval.append("\\\'");
-              continue;
-           case '\\':
-              retval.append("\\\\");
-              continue;
-           default:
-              if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
-                 String s = "0000" + Integer.toString(ch, 16);
-                 retval.append("\\u" + s.substring(s.length() - 4, s.length()));
-              } else {
-                 retval.append(ch);
-              }
-              continue;
-        }
-      }
-      return retval.toString();
-   }
-
-   /**
-    * Returns a detailed message for the Error when it is thrown by the
-    * token manager to indicate a lexical error.
-    * Parameters : 
-    *    EOFSeen     : indicates if EOF caused the lexicl error
-    *    curLexState : lexical state in which this error occured
-    *    errorLine   : line number when the error occured
-    *    errorColumn : column number when the error occured
-    *    errorAfter  : prefix that was seen before this error occured
-    *    curchar     : the offending character
-    * Note: You can customize the lexical error message by modifying this method.
-    */
-   protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) {
-      return("Lexical error at line " +
-           errorLine + ", column " +
-           errorColumn + ".  Encountered: " +
-           (EOFSeen ? "<EOF> " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") +
-           "after : \"" + addEscapes(errorAfter) + "\"");
-   }
-
-   /**
-    * You can also modify the body of this method to customize your error messages.
-    * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not
-    * of end-users concern, so you can return something like : 
-    *
-    *     "Internal Error : Please file a bug report .... "
-    *
-    * from this method for such cases in the release version of your parser.
-    */
-   public String getMessage() {
-      return super.getMessage();
-   }
-
-   /*
-    * Constructors of various flavors follow.
-    */
-
-   public TokenMgrError() {
-   }
-
-   public TokenMgrError(String message, int reason) {
-      super(message);
-      errorCode = reason;
-   }
-
-   public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) {
-      this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason);
-   }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java
deleted file mode 100644
index cacf3af..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java
+++ /dev/null
@@ -1,268 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. ContentTypeParser.java */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.contenttype.parser;
-
-import java.util.ArrayList;
-import java.util.Vector;
-
-public class ContentTypeParser implements ContentTypeParserConstants {
-
-        private String type;
-        private String subtype;
-        private ArrayList<String> paramNames = new ArrayList<String>();
-        private ArrayList<String> paramValues = new ArrayList<String>();
-
-        public String getType() { return type; }
-        public String getSubType() { return subtype; }
-        public ArrayList<String> getParamNames() { return paramNames; }
-        public ArrayList<String> getParamValues() { return paramValues; }
-
-    public static void main(String args[]) throws ParseException {
-        while (true) {
-            try {
-                ContentTypeParser parser = new ContentTypeParser(System.in);
-                parser.parseLine();
-            } catch (Exception x) {
-                x.printStackTrace();
-                return;
-            }
-        }
-    }
-
-  final public void parseLine() throws ParseException {
-    parse();
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case 1:
-      jj_consume_token(1);
-      break;
-    default:
-      jj_la1[0] = jj_gen;
-      ;
-    }
-    jj_consume_token(2);
-  }
-
-  final public void parseAll() throws ParseException {
-    parse();
-    jj_consume_token(0);
-  }
-
-  final public void parse() throws ParseException {
-        Token type;
-        Token subtype;
-    type = jj_consume_token(ATOKEN);
-    jj_consume_token(3);
-    subtype = jj_consume_token(ATOKEN);
-                this.type = type.image;
-                this.subtype = subtype.image;
-    label_1:
-    while (true) {
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case 4:
-        ;
-        break;
-      default:
-        jj_la1[1] = jj_gen;
-        break label_1;
-      }
-      jj_consume_token(4);
-      parameter();
-    }
-  }
-
-  final public void parameter() throws ParseException {
-        Token attrib;
-        String val;
-    attrib = jj_consume_token(ATOKEN);
-    jj_consume_token(5);
-    val = value();
-                paramNames.add(attrib.image);
-                paramValues.add(val);
-  }
-
-  final public String value() throws ParseException {
- Token t;
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case ATOKEN:
-      t = jj_consume_token(ATOKEN);
-      break;
-    case QUOTEDSTRING:
-      t = jj_consume_token(QUOTEDSTRING);
-      break;
-    default:
-      jj_la1[2] = jj_gen;
-      jj_consume_token(-1);
-      throw new ParseException();
-    }
-          {if (true) return t.image;}
-    throw new Error("Missing return statement in function");
-  }
-
-  public ContentTypeParserTokenManager token_source;
-  SimpleCharStream jj_input_stream;
-  public Token token, jj_nt;
-  private int jj_ntk;
-  private int jj_gen;
-  final private int[] jj_la1 = new int[3];
-  static private int[] jj_la1_0;
-  static {
-      jj_la1_0();
-   }
-   private static void jj_la1_0() {
-      jj_la1_0 = new int[] {0x2,0x10,0x280000,};
-   }
-
-  public ContentTypeParser(java.io.InputStream stream) {
-     this(stream, null);
-  }
-  public ContentTypeParser(java.io.InputStream stream, String encoding) {
-    try { jj_input_stream = new SimpleCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
-    token_source = new ContentTypeParserTokenManager(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 3; i++) jj_la1[i] = -1;
-  }
-
-  public void ReInit(java.io.InputStream stream) {
-     ReInit(stream, null);
-  }
-  public void ReInit(java.io.InputStream stream, String encoding) {
-    try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
-    token_source.ReInit(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 3; i++) jj_la1[i] = -1;
-  }
-
-  public ContentTypeParser(java.io.Reader stream) {
-    jj_input_stream = new SimpleCharStream(stream, 1, 1);
-    token_source = new ContentTypeParserTokenManager(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 3; i++) jj_la1[i] = -1;
-  }
-
-  public void ReInit(java.io.Reader stream) {
-    jj_input_stream.ReInit(stream, 1, 1);
-    token_source.ReInit(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 3; i++) jj_la1[i] = -1;
-  }
-
-  public ContentTypeParser(ContentTypeParserTokenManager tm) {
-    token_source = tm;
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 3; i++) jj_la1[i] = -1;
-  }
-
-  public void ReInit(ContentTypeParserTokenManager tm) {
-    token_source = tm;
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 3; i++) jj_la1[i] = -1;
-  }
-
-  final private Token jj_consume_token(int kind) throws ParseException {
-    Token oldToken;
-    if ((oldToken = token).next != null) token = token.next;
-    else token = token.next = token_source.getNextToken();
-    jj_ntk = -1;
-    if (token.kind == kind) {
-      jj_gen++;
-      return token;
-    }
-    token = oldToken;
-    jj_kind = kind;
-    throw generateParseException();
-  }
-
-  final public Token getNextToken() {
-    if (token.next != null) token = token.next;
-    else token = token.next = token_source.getNextToken();
-    jj_ntk = -1;
-    jj_gen++;
-    return token;
-  }
-
-  final public Token getToken(int index) {
-    Token t = token;
-    for (int i = 0; i < index; i++) {
-      if (t.next != null) t = t.next;
-      else t = t.next = token_source.getNextToken();
-    }
-    return t;
-  }
-
-  final private int jj_ntk() {
-    if ((jj_nt=token.next) == null)
-      return (jj_ntk = (token.next=token_source.getNextToken()).kind);
-    else
-      return (jj_ntk = jj_nt.kind);
-  }
-
-  private Vector<int[]> jj_expentries = new Vector<int[]>();
-  private int[] jj_expentry;
-  private int jj_kind = -1;
-
-  public ParseException generateParseException() {
-    jj_expentries.removeAllElements();
-    boolean[] la1tokens = new boolean[24];
-    for (int i = 0; i < 24; i++) {
-      la1tokens[i] = false;
-    }
-    if (jj_kind >= 0) {
-      la1tokens[jj_kind] = true;
-      jj_kind = -1;
-    }
-    for (int i = 0; i < 3; i++) {
-      if (jj_la1[i] == jj_gen) {
-        for (int j = 0; j < 32; j++) {
-          if ((jj_la1_0[i] & (1<<j)) != 0) {
-            la1tokens[j] = true;
-          }
-        }
-      }
-    }
-    for (int i = 0; i < 24; i++) {
-      if (la1tokens[i]) {
-        jj_expentry = new int[1];
-        jj_expentry[0] = i;
-        jj_expentries.addElement(jj_expentry);
-      }
-    }
-    int[][] exptokseq = new int[jj_expentries.size()][];
-    for (int i = 0; i < jj_expentries.size(); i++) {
-      exptokseq[i] = jj_expentries.elementAt(i);
-    }
-    return new ParseException(token, exptokseq, tokenImage);
-  }
-
-  final public void enable_tracing() {
-  }
-
-  final public void disable_tracing() {
-  }
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java
deleted file mode 100644
index d933d80..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. ContentTypeParserConstants.java */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.contenttype.parser;
-
-public interface ContentTypeParserConstants {
-
-  int EOF = 0;
-  int WS = 6;
-  int COMMENT = 8;
-  int QUOTEDSTRING = 19;
-  int DIGITS = 20;
-  int ATOKEN = 21;
-  int QUOTEDPAIR = 22;
-  int ANY = 23;
-
-  int DEFAULT = 0;
-  int INCOMMENT = 1;
-  int NESTED_COMMENT = 2;
-  int INQUOTEDSTRING = 3;
-
-  String[] tokenImage = {
-    "<EOF>",
-    "\"\\r\"",
-    "\"\\n\"",
-    "\"/\"",
-    "\";\"",
-    "\"=\"",
-    "<WS>",
-    "\"(\"",
-    "\")\"",
-    "<token of kind 9>",
-    "\"(\"",
-    "<token of kind 11>",
-    "<token of kind 12>",
-    "\"(\"",
-    "\")\"",
-    "<token of kind 15>",
-    "\"\\\"\"",
-    "<token of kind 17>",
-    "<token of kind 18>",
-    "\"\\\"\"",
-    "<DIGITS>",
-    "<ATOKEN>",
-    "<QUOTEDPAIR>",
-    "<ANY>",
-  };
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java
deleted file mode 100644
index 25b7aba..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java
+++ /dev/null
@@ -1,877 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. ContentTypeParserTokenManager.java */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.contenttype.parser;
-import java.util.ArrayList;
-
-public class ContentTypeParserTokenManager implements ContentTypeParserConstants
-{
-        // Keeps track of how many levels of comment nesting
-        // we've encountered.  This is only used when the 2nd
-        // level is reached, for example ((this)), not (this).
-        // This is because the outermost level must be treated
-        // specially anyway, because the outermost ")" has a
-        // different token type than inner ")" instances.
-        static int commentNest;
-  public  java.io.PrintStream debugStream = System.out;
-  public  void setDebugStream(java.io.PrintStream ds) { debugStream = ds; }
-private final int jjStopStringLiteralDfa_0(int pos, long active0)
-{
-   switch (pos)
-   {
-      default :
-         return -1;
-   }
-}
-private final int jjStartNfa_0(int pos, long active0)
-{
-   return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0), pos + 1);
-}
-private final int jjStopAtPos(int pos, int kind)
-{
-   jjmatchedKind = kind;
-   jjmatchedPos = pos;
-   return pos + 1;
-}
-private final int jjStartNfaWithStates_0(int pos, int kind, int state)
-{
-   jjmatchedKind = kind;
-   jjmatchedPos = pos;
-   try { curChar = input_stream.readChar(); }
-   catch(java.io.IOException e) { return pos + 1; }
-   return jjMoveNfa_0(state, pos + 1);
-}
-private final int jjMoveStringLiteralDfa0_0()
-{
-   switch(curChar)
-   {
-      case 10:
-         return jjStartNfaWithStates_0(0, 2, 2);
-      case 13:
-         return jjStartNfaWithStates_0(0, 1, 2);
-      case 34:
-         return jjStopAtPos(0, 16);
-      case 40:
-         return jjStopAtPos(0, 7);
-      case 47:
-         return jjStopAtPos(0, 3);
-      case 59:
-         return jjStopAtPos(0, 4);
-      case 61:
-         return jjStopAtPos(0, 5);
-      default :
-         return jjMoveNfa_0(3, 0);
-   }
-}
-private final void jjCheckNAdd(int state)
-{
-   if (jjrounds[state] != jjround)
-   {
-      jjstateSet[jjnewStateCnt++] = state;
-      jjrounds[state] = jjround;
-   }
-}
-private final void jjAddStates(int start, int end)
-{
-   do {
-      jjstateSet[jjnewStateCnt++] = jjnextStates[start];
-   } while (start++ != end);
-}
-private final void jjCheckNAddTwoStates(int state1, int state2)
-{
-   jjCheckNAdd(state1);
-   jjCheckNAdd(state2);
-}
-private final void jjCheckNAddStates(int start, int end)
-{
-   do {
-      jjCheckNAdd(jjnextStates[start]);
-   } while (start++ != end);
-}
-private final void jjCheckNAddStates(int start)
-{
-   jjCheckNAdd(jjnextStates[start]);
-   jjCheckNAdd(jjnextStates[start + 1]);
-}
-static final long[] jjbitVec0 = {
-   0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL
-};
-private final int jjMoveNfa_0(int startState, int curPos)
-{
-   int[] nextStates;
-   int startsAt = 0;
-   jjnewStateCnt = 3;
-   int i = 1;
-   jjstateSet[0] = startState;
-   int j, kind = 0x7fffffff;
-   for (;;)
-   {
-      if (++jjround == 0x7fffffff)
-         ReInitRounds();
-      if (curChar < 64)
-      {
-         long l = 1L << curChar;
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 3:
-                  if ((0x3ff6cfafffffdffL & l) != 0L)
-                  {
-                     if (kind > 21)
-                        kind = 21;
-                     jjCheckNAdd(2);
-                  }
-                  else if ((0x100000200L & l) != 0L)
-                  {
-                     if (kind > 6)
-                        kind = 6;
-                     jjCheckNAdd(0);
-                  }
-                  if ((0x3ff000000000000L & l) != 0L)
-                  {
-                     if (kind > 20)
-                        kind = 20;
-                     jjCheckNAdd(1);
-                  }
-                  break;
-               case 0:
-                  if ((0x100000200L & l) == 0L)
-                     break;
-                  kind = 6;
-                  jjCheckNAdd(0);
-                  break;
-               case 1:
-                  if ((0x3ff000000000000L & l) == 0L)
-                     break;
-                  if (kind > 20)
-                     kind = 20;
-                  jjCheckNAdd(1);
-                  break;
-               case 2:
-                  if ((0x3ff6cfafffffdffL & l) == 0L)
-                     break;
-                  if (kind > 21)
-                     kind = 21;
-                  jjCheckNAdd(2);
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else if (curChar < 128)
-      {
-         long l = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 3:
-               case 2:
-                  if ((0xffffffffc7fffffeL & l) == 0L)
-                     break;
-                  kind = 21;
-                  jjCheckNAdd(2);
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else
-      {
-         int i2 = (curChar & 0xff) >> 6;
-         long l2 = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 3:
-               case 2:
-                  if ((jjbitVec0[i2] & l2) == 0L)
-                     break;
-                  if (kind > 21)
-                     kind = 21;
-                  jjCheckNAdd(2);
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      if (kind != 0x7fffffff)
-      {
-         jjmatchedKind = kind;
-         jjmatchedPos = curPos;
-         kind = 0x7fffffff;
-      }
-      ++curPos;
-      if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
-         return curPos;
-      try { curChar = input_stream.readChar(); }
-      catch(java.io.IOException e) { return curPos; }
-   }
-}
-private final int jjStopStringLiteralDfa_1(int pos, long active0)
-{
-   switch (pos)
-   {
-      default :
-         return -1;
-   }
-}
-private final int jjStartNfa_1(int pos, long active0)
-{
-   return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1);
-}
-private final int jjStartNfaWithStates_1(int pos, int kind, int state)
-{
-   jjmatchedKind = kind;
-   jjmatchedPos = pos;
-   try { curChar = input_stream.readChar(); }
-   catch(java.io.IOException e) { return pos + 1; }
-   return jjMoveNfa_1(state, pos + 1);
-}
-private final int jjMoveStringLiteralDfa0_1()
-{
-   switch(curChar)
-   {
-      case 40:
-         return jjStopAtPos(0, 10);
-      case 41:
-         return jjStopAtPos(0, 8);
-      default :
-         return jjMoveNfa_1(0, 0);
-   }
-}
-private final int jjMoveNfa_1(int startState, int curPos)
-{
-   int[] nextStates;
-   int startsAt = 0;
-   jjnewStateCnt = 3;
-   int i = 1;
-   jjstateSet[0] = startState;
-   int j, kind = 0x7fffffff;
-   for (;;)
-   {
-      if (++jjround == 0x7fffffff)
-         ReInitRounds();
-      if (curChar < 64)
-      {
-         long l = 1L << curChar;
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if (kind > 11)
-                     kind = 11;
-                  break;
-               case 1:
-                  if (kind > 9)
-                     kind = 9;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else if (curChar < 128)
-      {
-         long l = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if (kind > 11)
-                     kind = 11;
-                  if (curChar == 92)
-                     jjstateSet[jjnewStateCnt++] = 1;
-                  break;
-               case 1:
-                  if (kind > 9)
-                     kind = 9;
-                  break;
-               case 2:
-                  if (kind > 11)
-                     kind = 11;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else
-      {
-         int i2 = (curChar & 0xff) >> 6;
-         long l2 = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if ((jjbitVec0[i2] & l2) != 0L && kind > 11)
-                     kind = 11;
-                  break;
-               case 1:
-                  if ((jjbitVec0[i2] & l2) != 0L && kind > 9)
-                     kind = 9;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      if (kind != 0x7fffffff)
-      {
-         jjmatchedKind = kind;
-         jjmatchedPos = curPos;
-         kind = 0x7fffffff;
-      }
-      ++curPos;
-      if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
-         return curPos;
-      try { curChar = input_stream.readChar(); }
-      catch(java.io.IOException e) { return curPos; }
-   }
-}
-private final int jjStopStringLiteralDfa_3(int pos, long active0)
-{
-   switch (pos)
-   {
-      default :
-         return -1;
-   }
-}
-private final int jjStartNfa_3(int pos, long active0)
-{
-   return jjMoveNfa_3(jjStopStringLiteralDfa_3(pos, active0), pos + 1);
-}
-private final int jjStartNfaWithStates_3(int pos, int kind, int state)
-{
-   jjmatchedKind = kind;
-   jjmatchedPos = pos;
-   try { curChar = input_stream.readChar(); }
-   catch(java.io.IOException e) { return pos + 1; }
-   return jjMoveNfa_3(state, pos + 1);
-}
-private final int jjMoveStringLiteralDfa0_3()
-{
-   switch(curChar)
-   {
-      case 34:
-         return jjStopAtPos(0, 19);
-      default :
-         return jjMoveNfa_3(0, 0);
-   }
-}
-private final int jjMoveNfa_3(int startState, int curPos)
-{
-   int[] nextStates;
-   int startsAt = 0;
-   jjnewStateCnt = 3;
-   int i = 1;
-   jjstateSet[0] = startState;
-   int j, kind = 0x7fffffff;
-   for (;;)
-   {
-      if (++jjround == 0x7fffffff)
-         ReInitRounds();
-      if (curChar < 64)
-      {
-         long l = 1L << curChar;
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-               case 2:
-                  if ((0xfffffffbffffffffL & l) == 0L)
-                     break;
-                  if (kind > 18)
-                     kind = 18;
-                  jjCheckNAdd(2);
-                  break;
-               case 1:
-                  if (kind > 17)
-                     kind = 17;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else if (curChar < 128)
-      {
-         long l = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if ((0xffffffffefffffffL & l) != 0L)
-                  {
-                     if (kind > 18)
-                        kind = 18;
-                     jjCheckNAdd(2);
-                  }
-                  else if (curChar == 92)
-                     jjstateSet[jjnewStateCnt++] = 1;
-                  break;
-               case 1:
-                  if (kind > 17)
-                     kind = 17;
-                  break;
-               case 2:
-                  if ((0xffffffffefffffffL & l) == 0L)
-                     break;
-                  if (kind > 18)
-                     kind = 18;
-                  jjCheckNAdd(2);
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else
-      {
-         int i2 = (curChar & 0xff) >> 6;
-         long l2 = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-               case 2:
-                  if ((jjbitVec0[i2] & l2) == 0L)
-                     break;
-                  if (kind > 18)
-                     kind = 18;
-                  jjCheckNAdd(2);
-                  break;
-               case 1:
-                  if ((jjbitVec0[i2] & l2) != 0L && kind > 17)
-                     kind = 17;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      if (kind != 0x7fffffff)
-      {
-         jjmatchedKind = kind;
-         jjmatchedPos = curPos;
-         kind = 0x7fffffff;
-      }
-      ++curPos;
-      if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
-         return curPos;
-      try { curChar = input_stream.readChar(); }
-      catch(java.io.IOException e) { return curPos; }
-   }
-}
-private final int jjStopStringLiteralDfa_2(int pos, long active0)
-{
-   switch (pos)
-   {
-      default :
-         return -1;
-   }
-}
-private final int jjStartNfa_2(int pos, long active0)
-{
-   return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1);
-}
-private final int jjStartNfaWithStates_2(int pos, int kind, int state)
-{
-   jjmatchedKind = kind;
-   jjmatchedPos = pos;
-   try { curChar = input_stream.readChar(); }
-   catch(java.io.IOException e) { return pos + 1; }
-   return jjMoveNfa_2(state, pos + 1);
-}
-private final int jjMoveStringLiteralDfa0_2()
-{
-   switch(curChar)
-   {
-      case 40:
-         return jjStopAtPos(0, 13);
-      case 41:
-         return jjStopAtPos(0, 14);
-      default :
-         return jjMoveNfa_2(0, 0);
-   }
-}
-private final int jjMoveNfa_2(int startState, int curPos)
-{
-   int[] nextStates;
-   int startsAt = 0;
-   jjnewStateCnt = 3;
-   int i = 1;
-   jjstateSet[0] = startState;
-   int j, kind = 0x7fffffff;
-   for (;;)
-   {
-      if (++jjround == 0x7fffffff)
-         ReInitRounds();
-      if (curChar < 64)
-      {
-         long l = 1L << curChar;
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if (kind > 15)
-                     kind = 15;
-                  break;
-               case 1:
-                  if (kind > 12)
-                     kind = 12;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else if (curChar < 128)
-      {
-         long l = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if (kind > 15)
-                     kind = 15;
-                  if (curChar == 92)
-                     jjstateSet[jjnewStateCnt++] = 1;
-                  break;
-               case 1:
-                  if (kind > 12)
-                     kind = 12;
-                  break;
-               case 2:
-                  if (kind > 15)
-                     kind = 15;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else
-      {
-         int i2 = (curChar & 0xff) >> 6;
-         long l2 = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if ((jjbitVec0[i2] & l2) != 0L && kind > 15)
-                     kind = 15;
-                  break;
-               case 1:
-                  if ((jjbitVec0[i2] & l2) != 0L && kind > 12)
-                     kind = 12;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      if (kind != 0x7fffffff)
-      {
-         jjmatchedKind = kind;
-         jjmatchedPos = curPos;
-         kind = 0x7fffffff;
-      }
-      ++curPos;
-      if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
-         return curPos;
-      try { curChar = input_stream.readChar(); }
-      catch(java.io.IOException e) { return curPos; }
-   }
-}
-static final int[] jjnextStates = {
-};
-public static final String[] jjstrLiteralImages = {
-"", "\15", "\12", "\57", "\73", "\75", null, null, null, null, null, null, 
-null, null, null, null, null, null, null, null, null, null, null, null, };
-public static final String[] lexStateNames = {
-   "DEFAULT", 
-   "INCOMMENT", 
-   "NESTED_COMMENT", 
-   "INQUOTEDSTRING", 
-};
-public static final int[] jjnewLexState = {
-   -1, -1, -1, -1, -1, -1, -1, 1, 0, -1, 2, -1, -1, -1, -1, -1, 3, -1, -1, 0, -1, -1, -1, -1, 
-};
-static final long[] jjtoToken = {
-   0x38003fL, 
-};
-static final long[] jjtoSkip = {
-   0x140L, 
-};
-static final long[] jjtoSpecial = {
-   0x40L, 
-};
-static final long[] jjtoMore = {
-   0x7fe80L, 
-};
-protected SimpleCharStream input_stream;
-private final int[] jjrounds = new int[3];
-private final int[] jjstateSet = new int[6];
-StringBuffer image;
-int jjimageLen;
-int lengthOfMatch;
-protected char curChar;
-public ContentTypeParserTokenManager(SimpleCharStream stream){
-   if (SimpleCharStream.staticFlag)
-      throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer.");
-   input_stream = stream;
-}
-public ContentTypeParserTokenManager(SimpleCharStream stream, int lexState){
-   this(stream);
-   SwitchTo(lexState);
-}
-public void ReInit(SimpleCharStream stream)
-{
-   jjmatchedPos = jjnewStateCnt = 0;
-   curLexState = defaultLexState;
-   input_stream = stream;
-   ReInitRounds();
-}
-private final void ReInitRounds()
-{
-   int i;
-   jjround = 0x80000001;
-   for (i = 3; i-- > 0;)
-      jjrounds[i] = 0x80000000;
-}
-public void ReInit(SimpleCharStream stream, int lexState)
-{
-   ReInit(stream);
-   SwitchTo(lexState);
-}
-public void SwitchTo(int lexState)
-{
-   if (lexState >= 4 || lexState < 0)
-      throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE);
-   else
-      curLexState = lexState;
-}
-
-protected Token jjFillToken()
-{
-   Token t = Token.newToken(jjmatchedKind);
-   t.kind = jjmatchedKind;
-   String im = jjstrLiteralImages[jjmatchedKind];
-   t.image = (im == null) ? input_stream.GetImage() : im;
-   t.beginLine = input_stream.getBeginLine();
-   t.beginColumn = input_stream.getBeginColumn();
-   t.endLine = input_stream.getEndLine();
-   t.endColumn = input_stream.getEndColumn();
-   return t;
-}
-
-int curLexState = 0;
-int defaultLexState = 0;
-int jjnewStateCnt;
-int jjround;
-int jjmatchedPos;
-int jjmatchedKind;
-
-public Token getNextToken() 
-{
-  int kind;
-  Token specialToken = null;
-  Token matchedToken;
-  int curPos = 0;
-
-  EOFLoop :
-  for (;;)
-  {   
-   try   
-   {     
-      curChar = input_stream.BeginToken();
-   }     
-   catch(java.io.IOException e)
-   {        
-      jjmatchedKind = 0;
-      matchedToken = jjFillToken();
-      matchedToken.specialToken = specialToken;
-      return matchedToken;
-   }
-   image = null;
-   jjimageLen = 0;
-
-   for (;;)
-   {
-     switch(curLexState)
-     {
-       case 0:
-         jjmatchedKind = 0x7fffffff;
-         jjmatchedPos = 0;
-         curPos = jjMoveStringLiteralDfa0_0();
-         break;
-       case 1:
-         jjmatchedKind = 0x7fffffff;
-         jjmatchedPos = 0;
-         curPos = jjMoveStringLiteralDfa0_1();
-         break;
-       case 2:
-         jjmatchedKind = 0x7fffffff;
-         jjmatchedPos = 0;
-         curPos = jjMoveStringLiteralDfa0_2();
-         break;
-       case 3:
-         jjmatchedKind = 0x7fffffff;
-         jjmatchedPos = 0;
-         curPos = jjMoveStringLiteralDfa0_3();
-         break;
-     }
-     if (jjmatchedKind != 0x7fffffff)
-     {
-        if (jjmatchedPos + 1 < curPos)
-           input_stream.backup(curPos - jjmatchedPos - 1);
-        if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
-        {
-           matchedToken = jjFillToken();
-           matchedToken.specialToken = specialToken;
-           TokenLexicalActions(matchedToken);
-       if (jjnewLexState[jjmatchedKind] != -1)
-         curLexState = jjnewLexState[jjmatchedKind];
-           return matchedToken;
-        }
-        else if ((jjtoSkip[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
-        {
-           if ((jjtoSpecial[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
-           {
-              matchedToken = jjFillToken();
-              if (specialToken == null)
-                 specialToken = matchedToken;
-              else
-              {
-                 matchedToken.specialToken = specialToken;
-                 specialToken = (specialToken.next = matchedToken);
-              }
-           }
-         if (jjnewLexState[jjmatchedKind] != -1)
-           curLexState = jjnewLexState[jjmatchedKind];
-           continue EOFLoop;
-        }
-        MoreLexicalActions();
-      if (jjnewLexState[jjmatchedKind] != -1)
-        curLexState = jjnewLexState[jjmatchedKind];
-        curPos = 0;
-        jjmatchedKind = 0x7fffffff;
-        try {
-           curChar = input_stream.readChar();
-           continue;
-        }
-        catch (java.io.IOException e1) { }
-     }
-     int error_line = input_stream.getEndLine();
-     int error_column = input_stream.getEndColumn();
-     String error_after = null;
-     boolean EOFSeen = false;
-     try { input_stream.readChar(); input_stream.backup(1); }
-     catch (java.io.IOException e1) {
-        EOFSeen = true;
-        error_after = curPos <= 1 ? "" : input_stream.GetImage();
-        if (curChar == '\n' || curChar == '\r') {
-           error_line++;
-           error_column = 0;
-        }
-        else
-           error_column++;
-     }
-     if (!EOFSeen) {
-        input_stream.backup(1);
-        error_after = curPos <= 1 ? "" : input_stream.GetImage();
-     }
-     throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR);
-   }
-  }
-}
-
-void MoreLexicalActions()
-{
-   jjimageLen += (lengthOfMatch = jjmatchedPos + 1);
-   switch(jjmatchedKind)
-   {
-      case 9 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-                          image.deleteCharAt(image.length() - 2);
-         break;
-      case 10 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-              commentNest = 1;
-         break;
-      case 12 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-                          image.deleteCharAt(image.length() - 2);
-         break;
-      case 13 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-              ++commentNest;
-         break;
-      case 14 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-              --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT);
-         break;
-      case 16 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-               image.deleteCharAt(image.length() - 1);
-         break;
-      case 17 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-                          image.deleteCharAt(image.length() - 2);
-         break;
-      default : 
-         break;
-   }
-}
-void TokenLexicalActions(Token matchedToken)
-{
-   switch(jjmatchedKind)
-   {
-      case 19 :
-        if (image == null)
-            image = new StringBuffer();
-            image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1)));
-                                 matchedToken.image = image.substring(0, image.length() - 1);
-         break;
-      default : 
-         break;
-   }
-}
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java
deleted file mode 100644
index d9b69b2..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.0 */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.contenttype.parser;
-
-/**
- * This exception is thrown when parse errors are encountered.
- * You can explicitly create objects of this exception type by
- * calling the method generateParseException in the generated
- * parser.
- *
- * You can modify this class to customize your error reporting
- * mechanisms so long as you retain the public fields.
- */
-public class ParseException extends Exception {
-
-  /**
-   * This constructor is used by the method "generateParseException"
-   * in the generated parser.  Calling this constructor generates
-   * a new object of this type with the fields "currentToken",
-   * "expectedTokenSequences", and "tokenImage" set.  The boolean
-   * flag "specialConstructor" is also set to true to indicate that
-   * this constructor was used to create this object.
-   * This constructor calls its super class with the empty string
-   * to force the "toString" method of parent class "Throwable" to
-   * print the error message in the form:
-   *     ParseException: <result of getMessage>
-   */
-  public ParseException(Token currentTokenVal,
-                        int[][] expectedTokenSequencesVal,
-                        String[] tokenImageVal
-                       )
-  {
-    super("");
-    specialConstructor = true;
-    currentToken = currentTokenVal;
-    expectedTokenSequences = expectedTokenSequencesVal;
-    tokenImage = tokenImageVal;
-  }
-
-  /**
-   * The following constructors are for use by you for whatever
-   * purpose you can think of.  Constructing the exception in this
-   * manner makes the exception behave in the normal way - i.e., as
-   * documented in the class "Throwable".  The fields "errorToken",
-   * "expectedTokenSequences", and "tokenImage" do not contain
-   * relevant information.  The JavaCC generated code does not use
-   * these constructors.
-   */
-
-  public ParseException() {
-    super();
-    specialConstructor = false;
-  }
-
-  public ParseException(String message) {
-    super(message);
-    specialConstructor = false;
-  }
-
-  /**
-   * This variable determines which constructor was used to create
-   * this object and thereby affects the semantics of the
-   * "getMessage" method (see below).
-   */
-  protected boolean specialConstructor;
-
-  /**
-   * This is the last token that has been consumed successfully.  If
-   * this object has been created due to a parse error, the token
-   * followng this token will (therefore) be the first error token.
-   */
-  public Token currentToken;
-
-  /**
-   * Each entry in this array is an array of integers.  Each array
-   * of integers represents a sequence of tokens (by their ordinal
-   * values) that is expected at this point of the parse.
-   */
-  public int[][] expectedTokenSequences;
-
-  /**
-   * This is a reference to the "tokenImage" array of the generated
-   * parser within which the parse error occurred.  This array is
-   * defined in the generated ...Constants interface.
-   */
-  public String[] tokenImage;
-
-  /**
-   * This method has the standard behavior when this object has been
-   * created using the standard constructors.  Otherwise, it uses
-   * "currentToken" and "expectedTokenSequences" to generate a parse
-   * error message and returns it.  If this object has been created
-   * due to a parse error, and you do not catch it (it gets thrown
-   * from the parser), then this method is called during the printing
-   * of the final stack trace, and hence the correct error message
-   * gets displayed.
-   */
-  public String getMessage() {
-    if (!specialConstructor) {
-      return super.getMessage();
-    }
-    StringBuffer expected = new StringBuffer();
-    int maxSize = 0;
-    for (int i = 0; i < expectedTokenSequences.length; i++) {
-      if (maxSize < expectedTokenSequences[i].length) {
-        maxSize = expectedTokenSequences[i].length;
-      }
-      for (int j = 0; j < expectedTokenSequences[i].length; j++) {
-        expected.append(tokenImage[expectedTokenSequences[i][j]]).append(" ");
-      }
-      if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) {
-        expected.append("...");
-      }
-      expected.append(eol).append("    ");
-    }
-    String retval = "Encountered \"";
-    Token tok = currentToken.next;
-    for (int i = 0; i < maxSize; i++) {
-      if (i != 0) retval += " ";
-      if (tok.kind == 0) {
-        retval += tokenImage[0];
-        break;
-      }
-      retval += add_escapes(tok.image);
-      tok = tok.next; 
-    }
-    retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn;
-    retval += "." + eol;
-    if (expectedTokenSequences.length == 1) {
-      retval += "Was expecting:" + eol + "    ";
-    } else {
-      retval += "Was expecting one of:" + eol + "    ";
-    }
-    retval += expected.toString();
-    return retval;
-  }
-
-  /**
-   * The end of line string for this machine.
-   */
-  protected String eol = System.getProperty("line.separator", "\n");
- 
-  /**
-   * Used to convert raw characters to their escaped version
-   * when these raw version cannot be used as part of an ASCII
-   * string literal.
-   */
-  protected String add_escapes(String str) {
-      StringBuffer retval = new StringBuffer();
-      char ch;
-      for (int i = 0; i < str.length(); i++) {
-        switch (str.charAt(i))
-        {
-           case 0 :
-              continue;
-           case '\b':
-              retval.append("\\b");
-              continue;
-           case '\t':
-              retval.append("\\t");
-              continue;
-           case '\n':
-              retval.append("\\n");
-              continue;
-           case '\f':
-              retval.append("\\f");
-              continue;
-           case '\r':
-              retval.append("\\r");
-              continue;
-           case '\"':
-              retval.append("\\\"");
-              continue;
-           case '\'':
-              retval.append("\\\'");
-              continue;
-           case '\\':
-              retval.append("\\\\");
-              continue;
-           default:
-              if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
-                 String s = "0000" + Integer.toString(ch, 16);
-                 retval.append("\\u" + s.substring(s.length() - 4, s.length()));
-              } else {
-                 retval.append(ch);
-              }
-              continue;
-        }
-      }
-      return retval.toString();
-   }
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java
deleted file mode 100644
index ae035b7..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java
+++ /dev/null
@@ -1,454 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 4.0 */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.contenttype.parser;
-
-/**
- * An implementation of interface CharStream, where the stream is assumed to
- * contain only ASCII characters (without unicode processing).
- */
-
-public class SimpleCharStream
-{
-  public static final boolean staticFlag = false;
-  int bufsize;
-  int available;
-  int tokenBegin;
-  public int bufpos = -1;
-  protected int bufline[];
-  protected int bufcolumn[];
-
-  protected int column = 0;
-  protected int line = 1;
-
-  protected boolean prevCharIsCR = false;
-  protected boolean prevCharIsLF = false;
-
-  protected java.io.Reader inputStream;
-
-  protected char[] buffer;
-  protected int maxNextCharInd = 0;
-  protected int inBuf = 0;
-  protected int tabSize = 8;
-
-  protected void setTabSize(int i) { tabSize = i; }
-  protected int getTabSize(int i) { return tabSize; }
-
-
-  protected void ExpandBuff(boolean wrapAround)
-  {
-     char[] newbuffer = new char[bufsize + 2048];
-     int newbufline[] = new int[bufsize + 2048];
-     int newbufcolumn[] = new int[bufsize + 2048];
-
-     try
-     {
-        if (wrapAround)
-        {
-           System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin);
-           System.arraycopy(buffer, 0, newbuffer,
-                                             bufsize - tokenBegin, bufpos);
-           buffer = newbuffer;
-
-           System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin);
-           System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos);
-           bufline = newbufline;
-
-           System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin);
-           System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos);
-           bufcolumn = newbufcolumn;
-
-           maxNextCharInd = (bufpos += (bufsize - tokenBegin));
-        }
-        else
-        {
-           System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin);
-           buffer = newbuffer;
-
-           System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin);
-           bufline = newbufline;
-
-           System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin);
-           bufcolumn = newbufcolumn;
-
-           maxNextCharInd = (bufpos -= tokenBegin);
-        }
-     }
-     catch (Throwable t)
-     {
-        throw new Error(t.getMessage());
-     }
-
-
-     bufsize += 2048;
-     available = bufsize;
-     tokenBegin = 0;
-  }
-
-  protected void FillBuff() throws java.io.IOException
-  {
-     if (maxNextCharInd == available)
-     {
-        if (available == bufsize)
-        {
-           if (tokenBegin > 2048)
-           {
-              bufpos = maxNextCharInd = 0;
-              available = tokenBegin;
-           }
-           else if (tokenBegin < 0)
-              bufpos = maxNextCharInd = 0;
-           else
-              ExpandBuff(false);
-        }
-        else if (available > tokenBegin)
-           available = bufsize;
-        else if ((tokenBegin - available) < 2048)
-           ExpandBuff(true);
-        else
-           available = tokenBegin;
-     }
-
-     int i;
-     try {
-        if ((i = inputStream.read(buffer, maxNextCharInd,
-                                    available - maxNextCharInd)) == -1)
-        {
-           inputStream.close();
-           throw new java.io.IOException();
-        }
-        else
-           maxNextCharInd += i;
-        return;
-     }
-     catch(java.io.IOException e) {
-        --bufpos;
-        backup(0);
-        if (tokenBegin == -1)
-           tokenBegin = bufpos;
-        throw e;
-     }
-  }
-
-  public char BeginToken() throws java.io.IOException
-  {
-     tokenBegin = -1;
-     char c = readChar();
-     tokenBegin = bufpos;
-
-     return c;
-  }
-
-  protected void UpdateLineColumn(char c)
-  {
-     column++;
-
-     if (prevCharIsLF)
-     {
-        prevCharIsLF = false;
-        line += (column = 1);
-     }
-     else if (prevCharIsCR)
-     {
-        prevCharIsCR = false;
-        if (c == '\n')
-        {
-           prevCharIsLF = true;
-        }
-        else
-           line += (column = 1);
-     }
-
-     switch (c)
-     {
-        case '\r' :
-           prevCharIsCR = true;
-           break;
-        case '\n' :
-           prevCharIsLF = true;
-           break;
-        case '\t' :
-           column--;
-           column += (tabSize - (column % tabSize));
-           break;
-        default :
-           break;
-     }
-
-     bufline[bufpos] = line;
-     bufcolumn[bufpos] = column;
-  }
-
-  public char readChar() throws java.io.IOException
-  {
-     if (inBuf > 0)
-     {
-        --inBuf;
-
-        if (++bufpos == bufsize)
-           bufpos = 0;
-
-        return buffer[bufpos];
-     }
-
-     if (++bufpos >= maxNextCharInd)
-        FillBuff();
-
-     char c = buffer[bufpos];
-
-     UpdateLineColumn(c);
-     return (c);
-  }
-
-  /**
-   * @deprecated 
-   * @see #getEndColumn
-   */
-  @Deprecated
-  public int getColumn() {
-     return bufcolumn[bufpos];
-  }
-
-  /**
-   * @deprecated 
-   * @see #getEndLine
-   */
-  @Deprecated
-  public int getLine() {
-     return bufline[bufpos];
-  }
-
-  public int getEndColumn() {
-     return bufcolumn[bufpos];
-  }
-
-  public int getEndLine() {
-     return bufline[bufpos];
-  }
-
-  public int getBeginColumn() {
-     return bufcolumn[tokenBegin];
-  }
-
-  public int getBeginLine() {
-     return bufline[tokenBegin];
-  }
-
-  public void backup(int amount) {
-
-    inBuf += amount;
-    if ((bufpos -= amount) < 0)
-       bufpos += bufsize;
-  }
-
-  public SimpleCharStream(java.io.Reader dstream, int startline,
-  int startcolumn, int buffersize)
-  {
-    inputStream = dstream;
-    line = startline;
-    column = startcolumn - 1;
-
-    available = bufsize = buffersize;
-    buffer = new char[buffersize];
-    bufline = new int[buffersize];
-    bufcolumn = new int[buffersize];
-  }
-
-  public SimpleCharStream(java.io.Reader dstream, int startline,
-                          int startcolumn)
-  {
-     this(dstream, startline, startcolumn, 4096);
-  }
-
-  public SimpleCharStream(java.io.Reader dstream)
-  {
-     this(dstream, 1, 1, 4096);
-  }
-  public void ReInit(java.io.Reader dstream, int startline,
-  int startcolumn, int buffersize)
-  {
-    inputStream = dstream;
-    line = startline;
-    column = startcolumn - 1;
-
-    if (buffer == null || buffersize != buffer.length)
-    {
-      available = bufsize = buffersize;
-      buffer = new char[buffersize];
-      bufline = new int[buffersize];
-      bufcolumn = new int[buffersize];
-    }
-    prevCharIsLF = prevCharIsCR = false;
-    tokenBegin = inBuf = maxNextCharInd = 0;
-    bufpos = -1;
-  }
-
-  public void ReInit(java.io.Reader dstream, int startline,
-                     int startcolumn)
-  {
-     ReInit(dstream, startline, startcolumn, 4096);
-  }
-
-  public void ReInit(java.io.Reader dstream)
-  {
-     ReInit(dstream, 1, 1, 4096);
-  }
-  public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline,
-  int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException
-  {
-     this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize);
-  }
-
-  public SimpleCharStream(java.io.InputStream dstream, int startline,
-  int startcolumn, int buffersize)
-  {
-     this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize);
-  }
-
-  public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline,
-                          int startcolumn) throws java.io.UnsupportedEncodingException
-  {
-     this(dstream, encoding, startline, startcolumn, 4096);
-  }
-
-  public SimpleCharStream(java.io.InputStream dstream, int startline,
-                          int startcolumn)
-  {
-     this(dstream, startline, startcolumn, 4096);
-  }
-
-  public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException
-  {
-     this(dstream, encoding, 1, 1, 4096);
-  }
-
-  public SimpleCharStream(java.io.InputStream dstream)
-  {
-     this(dstream, 1, 1, 4096);
-  }
-
-  public void ReInit(java.io.InputStream dstream, String encoding, int startline,
-                          int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException
-  {
-     ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize);
-  }
-
-  public void ReInit(java.io.InputStream dstream, int startline,
-                          int startcolumn, int buffersize)
-  {
-     ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize);
-  }
-
-  public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException
-  {
-     ReInit(dstream, encoding, 1, 1, 4096);
-  }
-
-  public void ReInit(java.io.InputStream dstream)
-  {
-     ReInit(dstream, 1, 1, 4096);
-  }
-  public void ReInit(java.io.InputStream dstream, String encoding, int startline,
-                     int startcolumn) throws java.io.UnsupportedEncodingException
-  {
-     ReInit(dstream, encoding, startline, startcolumn, 4096);
-  }
-  public void ReInit(java.io.InputStream dstream, int startline,
-                     int startcolumn)
-  {
-     ReInit(dstream, startline, startcolumn, 4096);
-  }
-  public String GetImage()
-  {
-     if (bufpos >= tokenBegin)
-        return new String(buffer, tokenBegin, bufpos - tokenBegin + 1);
-     else
-        return new String(buffer, tokenBegin, bufsize - tokenBegin) +
-                              new String(buffer, 0, bufpos + 1);
-  }
-
-  public char[] GetSuffix(int len)
-  {
-     char[] ret = new char[len];
-
-     if ((bufpos + 1) >= len)
-        System.arraycopy(buffer, bufpos - len + 1, ret, 0, len);
-     else
-     {
-        System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0,
-                                                          len - bufpos - 1);
-        System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1);
-     }
-
-     return ret;
-  }
-
-  public void Done()
-  {
-     buffer = null;
-     bufline = null;
-     bufcolumn = null;
-  }
-
-  /**
-   * Method to adjust line and column numbers for the start of a token.
-   */
-  public void adjustBeginLineColumn(int newLine, int newCol)
-  {
-     int start = tokenBegin;
-     int len;
-
-     if (bufpos >= tokenBegin)
-     {
-        len = bufpos - tokenBegin + inBuf + 1;
-     }
-     else
-     {
-        len = bufsize - tokenBegin + bufpos + 1 + inBuf;
-     }
-
-     int i = 0, j = 0, k = 0;
-     int nextColDiff = 0, columnDiff = 0;
-
-     while (i < len &&
-            bufline[j = start % bufsize] == bufline[k = ++start % bufsize])
-     {
-        bufline[j] = newLine;
-        nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j];
-        bufcolumn[j] = newCol + columnDiff;
-        columnDiff = nextColDiff;
-        i++;
-     } 
-
-     if (i < len)
-     {
-        bufline[j] = newLine++;
-        bufcolumn[j] = newCol + columnDiff;
-
-        while (i++ < len)
-        {
-           if (bufline[j = start % bufsize] != bufline[++start % bufsize])
-              bufline[j] = newLine++;
-           else
-              bufline[j] = newLine;
-        }
-     }
-
-     line = bufline[j];
-     column = bufcolumn[j];
-  }
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/Token.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/Token.java
deleted file mode 100644
index 34e65ee..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/Token.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. Token.java Version 3.0 */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.contenttype.parser;
-
-/**
- * Describes the input token stream.
- */
-
-public class Token {
-
-  /**
-   * An integer that describes the kind of this token.  This numbering
-   * system is determined by JavaCCParser, and a table of these numbers is
-   * stored in the file ...Constants.java.
-   */
-  public int kind;
-
-  /**
-   * beginLine and beginColumn describe the position of the first character
-   * of this token; endLine and endColumn describe the position of the
-   * last character of this token.
-   */
-  public int beginLine, beginColumn, endLine, endColumn;
-
-  /**
-   * The string image of the token.
-   */
-  public String image;
-
-  /**
-   * A reference to the next regular (non-special) token from the input
-   * stream.  If this is the last token from the input stream, or if the
-   * token manager has not read tokens beyond this one, this field is
-   * set to null.  This is true only if this token is also a regular
-   * token.  Otherwise, see below for a description of the contents of
-   * this field.
-   */
-  public Token next;
-
-  /**
-   * This field is used to access special tokens that occur prior to this
-   * token, but after the immediately preceding regular (non-special) token.
-   * If there are no such special tokens, this field is set to null.
-   * When there are more than one such special token, this field refers
-   * to the last of these special tokens, which in turn refers to the next
-   * previous special token through its specialToken field, and so on
-   * until the first special token (whose specialToken field is null).
-   * The next fields of special tokens refer to other special tokens that
-   * immediately follow it (without an intervening regular token).  If there
-   * is no such token, this field is null.
-   */
-  public Token specialToken;
-
-  /**
-   * Returns the image.
-   */
-  public String toString()
-  {
-     return image;
-  }
-
-  /**
-   * Returns a new Token object, by default. However, if you want, you
-   * can create and return subclass objects based on the value of ofKind.
-   * Simply add the cases to the switch for all those special cases.
-   * For example, if you have a subclass of Token called IDToken that
-   * you want to create if ofKind is ID, simlpy add something like :
-   *
-   *    case MyParserConstants.ID : return new IDToken();
-   *
-   * to the following switch statement. Then you can cast matchedToken
-   * variable to the appropriate type and use it in your lexical actions.
-   */
-  public static final Token newToken(int ofKind)
-  {
-     switch(ofKind)
-     {
-       default : return new Token();
-     }
-  }
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java
deleted file mode 100644
index ea5a782..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 3.0 */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.contenttype.parser;
-
-public class TokenMgrError extends Error
-{
-   /*
-    * Ordinals for various reasons why an Error of this type can be thrown.
-    */
-
-   /**
-    * Lexical error occured.
-    */
-   static final int LEXICAL_ERROR = 0;
-
-   /**
-    * An attempt wass made to create a second instance of a static token manager.
-    */
-   static final int STATIC_LEXER_ERROR = 1;
-
-   /**
-    * Tried to change to an invalid lexical state.
-    */
-   static final int INVALID_LEXICAL_STATE = 2;
-
-   /**
-    * Detected (and bailed out of) an infinite loop in the token manager.
-    */
-   static final int LOOP_DETECTED = 3;
-
-   /**
-    * Indicates the reason why the exception is thrown. It will have
-    * one of the above 4 values.
-    */
-   int errorCode;
-
-   /**
-    * Replaces unprintable characters by their espaced (or unicode escaped)
-    * equivalents in the given string
-    */
-   protected static final String addEscapes(String str) {
-      StringBuffer retval = new StringBuffer();
-      char ch;
-      for (int i = 0; i < str.length(); i++) {
-        switch (str.charAt(i))
-        {
-           case 0 :
-              continue;
-           case '\b':
-              retval.append("\\b");
-              continue;
-           case '\t':
-              retval.append("\\t");
-              continue;
-           case '\n':
-              retval.append("\\n");
-              continue;
-           case '\f':
-              retval.append("\\f");
-              continue;
-           case '\r':
-              retval.append("\\r");
-              continue;
-           case '\"':
-              retval.append("\\\"");
-              continue;
-           case '\'':
-              retval.append("\\\'");
-              continue;
-           case '\\':
-              retval.append("\\\\");
-              continue;
-           default:
-              if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
-                 String s = "0000" + Integer.toString(ch, 16);
-                 retval.append("\\u" + s.substring(s.length() - 4, s.length()));
-              } else {
-                 retval.append(ch);
-              }
-              continue;
-        }
-      }
-      return retval.toString();
-   }
-
-   /**
-    * Returns a detailed message for the Error when it is thrown by the
-    * token manager to indicate a lexical error.
-    * Parameters : 
-    *    EOFSeen     : indicates if EOF caused the lexicl error
-    *    curLexState : lexical state in which this error occured
-    *    errorLine   : line number when the error occured
-    *    errorColumn : column number when the error occured
-    *    errorAfter  : prefix that was seen before this error occured
-    *    curchar     : the offending character
-    * Note: You can customize the lexical error message by modifying this method.
-    */
-   protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) {
-      return("Lexical error at line " +
-           errorLine + ", column " +
-           errorColumn + ".  Encountered: " +
-           (EOFSeen ? "<EOF> " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") +
-           "after : \"" + addEscapes(errorAfter) + "\"");
-   }
-
-   /**
-    * You can also modify the body of this method to customize your error messages.
-    * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not
-    * of end-users concern, so you can return something like : 
-    *
-    *     "Internal Error : Please file a bug report .... "
-    *
-    * from this method for such cases in the release version of your parser.
-    */
-   public String getMessage() {
-      return super.getMessage();
-   }
-
-   /*
-    * Constructors of various flavors follow.
-    */
-
-   public TokenMgrError() {
-   }
-
-   public TokenMgrError(String message, int reason) {
-      super(message);
-      errorCode = reason;
-   }
-
-   public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) {
-      this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason);
-   }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/DateTime.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/DateTime.java
deleted file mode 100644
index 506ff54..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/DateTime.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.field.datetime;
-
-import org.apache.james.mime4j.field.datetime.parser.DateTimeParser;
-import org.apache.james.mime4j.field.datetime.parser.ParseException;
-import org.apache.james.mime4j.field.datetime.parser.TokenMgrError;
-
-import java.util.Date;
-import java.util.Calendar;
-import java.util.TimeZone;
-import java.util.GregorianCalendar;
-import java.io.StringReader;
-
-public class DateTime {
-    private final Date date;
-    private final int year;
-    private final int month;
-    private final int day;
-    private final int hour;
-    private final int minute;
-    private final int second;
-    private final int timeZone;
-
-    public DateTime(String yearString, int month, int day, int hour, int minute, int second, int timeZone) {
-        this.year = convertToYear(yearString);
-        this.date = convertToDate(year, month, day, hour, minute, second, timeZone);
-        this.month = month;
-        this.day = day;
-        this.hour = hour;
-        this.minute = minute;
-        this.second = second;
-        this.timeZone = timeZone;
-    }
-
-    private int convertToYear(String yearString) {
-        int year = Integer.parseInt(yearString);
-        switch (yearString.length()) {
-            case 1:
-            case 2:
-                if (year >= 0 && year < 50)
-                    return 2000 + year;
-                else
-                    return 1900 + year;
-            case 3:
-                return 1900 + year;
-            default:
-                return year;
-        }
-    }
-
-    public static Date convertToDate(int year, int month, int day, int hour, int minute, int second, int timeZone) {
-        Calendar c = new GregorianCalendar(TimeZone.getTimeZone("GMT+0"));
-        c.set(year, month - 1, day, hour, minute, second);
-        c.set(Calendar.MILLISECOND, 0);
-
-        if (timeZone != Integer.MIN_VALUE) {
-            int minutes = ((timeZone / 100) * 60) + timeZone % 100;
-            c.add(Calendar.MINUTE, -1 * minutes);
-        }
-
-        return c.getTime();
-    }
-
-    public Date getDate() {
-        return date;
-    }
-
-    public int getYear() {
-        return year;
-    }
-
-    public int getMonth() {
-        return month;
-    }
-
-    public int getDay() {
-        return day;
-    }
-
-    public int getHour() {
-        return hour;
-    }
-
-    public int getMinute() {
-        return minute;
-    }
-
-    public int getSecond() {
-        return second;
-    }
-
-    public int getTimeZone() {
-        return timeZone;
-    }
-
-    public void print() {
-        System.out.println(getYear() + " " + getMonth() + " " + getDay() + "; " + getHour() + " " + getMinute() + " " + getSecond() + " " + getTimeZone());
-    }
-
-
-    public static DateTime parse(String dateString) throws ParseException {
-        try {
-            return new DateTimeParser(new StringReader(dateString)).parseAll();
-        }
-        catch (TokenMgrError err) {
-            throw new ParseException(err.getMessage());
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java
deleted file mode 100644
index 43edebb..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java
+++ /dev/null
@@ -1,570 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. DateTimeParser.java */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.datetime.parser;
-
-import org.apache.james.mime4j.field.datetime.DateTime;
-
-import java.util.Vector;
-
-public class DateTimeParser implements DateTimeParserConstants {
-    private static final boolean ignoreMilitaryZoneOffset = true;
-
-    public static void main(String args[]) throws ParseException {
-                while (true) {
-                    try {
-                                DateTimeParser parser = new DateTimeParser(System.in);
-                        parser.parseLine();
-                    } catch (Exception x) {
-                                x.printStackTrace();
-                                return;
-                    }
-                }
-    }
-
-    private static int parseDigits(Token token) {
-        return Integer.parseInt(token.image, 10);
-    }
-
-    private static int getMilitaryZoneOffset(char c) {
-        if (ignoreMilitaryZoneOffset)
-            return 0;
-
-        c = Character.toUpperCase(c);
-
-        switch (c) {
-            case 'A': return 1;
-            case 'B': return 2;
-            case 'C': return 3;
-            case 'D': return 4;
-            case 'E': return 5;
-            case 'F': return 6;
-            case 'G': return 7;
-            case 'H': return 8;
-            case 'I': return 9;
-            case 'K': return 10;
-            case 'L': return 11;
-            case 'M': return 12;
-
-            case 'N': return -1;
-            case 'O': return -2;
-            case 'P': return -3;
-            case 'Q': return -4;
-            case 'R': return -5;
-            case 'S': return -6;
-            case 'T': return -7;
-            case 'U': return -8;
-            case 'V': return -9;
-            case 'W': return -10;
-            case 'X': return -11;
-            case 'Y': return -12;
-
-            case 'Z': return 0;
-            default: return 0;
-        }
-    }
-
-    private static class Time {
-        private int hour;
-        private int minute;
-        private int second;
-        private int zone;
-
-        public Time(int hour, int minute, int second, int zone) {
-            this.hour = hour;
-            this.minute = minute;
-            this.second = second;
-            this.zone = zone;
-        }
-
-        public int getHour() { return hour; }
-        public int getMinute() { return minute; }
-        public int getSecond() { return second; }
-        public int getZone() { return zone; }
-    }
-
-    private static class Date {
-        private String year;
-        private int month;
-        private int day;
-
-        public Date(String year, int month, int day) {
-            this.year = year;
-            this.month = month;
-            this.day = day;
-        }
-
-        public String getYear() { return year; }
-        public int getMonth() { return month; }
-        public int getDay() { return day; }
-    }
-
-  final public DateTime parseLine() throws ParseException {
- DateTime dt;
-    dt = date_time();
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case 1:
-      jj_consume_token(1);
-      break;
-    default:
-      jj_la1[0] = jj_gen;
-      ;
-    }
-    jj_consume_token(2);
-          {if (true) return dt;}
-    throw new Error("Missing return statement in function");
-  }
-
-  final public DateTime parseAll() throws ParseException {
- DateTime dt;
-    dt = date_time();
-    jj_consume_token(0);
-          {if (true) return dt;}
-    throw new Error("Missing return statement in function");
-  }
-
-  final public DateTime date_time() throws ParseException {
- Date d; Time t;
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case 4:
-    case 5:
-    case 6:
-    case 7:
-    case 8:
-    case 9:
-    case 10:
-      day_of_week();
-      jj_consume_token(3);
-      break;
-    default:
-      jj_la1[1] = jj_gen;
-      ;
-    }
-    d = date();
-    t = time();
-            {if (true) return new DateTime(
-                    d.getYear(),
-                    d.getMonth(),
-                    d.getDay(),
-                    t.getHour(),
-                    t.getMinute(),
-                    t.getSecond(),
-                    t.getZone());}    // time zone offset
-
-    throw new Error("Missing return statement in function");
-  }
-
-  final public String day_of_week() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case 4:
-      jj_consume_token(4);
-      break;
-    case 5:
-      jj_consume_token(5);
-      break;
-    case 6:
-      jj_consume_token(6);
-      break;
-    case 7:
-      jj_consume_token(7);
-      break;
-    case 8:
-      jj_consume_token(8);
-      break;
-    case 9:
-      jj_consume_token(9);
-      break;
-    case 10:
-      jj_consume_token(10);
-      break;
-    default:
-      jj_la1[2] = jj_gen;
-      jj_consume_token(-1);
-      throw new ParseException();
-    }
-      {if (true) return token.image;}
-    throw new Error("Missing return statement in function");
-  }
-
-  final public Date date() throws ParseException {
- int d, m; String y;
-    d = day();
-    m = month();
-    y = year();
-      {if (true) return new Date(y, m, d);}
-    throw new Error("Missing return statement in function");
-  }
-
-  final public int day() throws ParseException {
- Token t;
-    t = jj_consume_token(DIGITS);
-                 {if (true) return parseDigits(t);}
-    throw new Error("Missing return statement in function");
-  }
-
-  final public int month() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case 11:
-      jj_consume_token(11);
-            {if (true) return 1;}
-      break;
-    case 12:
-      jj_consume_token(12);
-            {if (true) return 2;}
-      break;
-    case 13:
-      jj_consume_token(13);
-            {if (true) return 3;}
-      break;
-    case 14:
-      jj_consume_token(14);
-            {if (true) return 4;}
-      break;
-    case 15:
-      jj_consume_token(15);
-            {if (true) return 5;}
-      break;
-    case 16:
-      jj_consume_token(16);
-            {if (true) return 6;}
-      break;
-    case 17:
-      jj_consume_token(17);
-            {if (true) return 7;}
-      break;
-    case 18:
-      jj_consume_token(18);
-            {if (true) return 8;}
-      break;
-    case 19:
-      jj_consume_token(19);
-            {if (true) return 9;}
-      break;
-    case 20:
-      jj_consume_token(20);
-            {if (true) return 10;}
-      break;
-    case 21:
-      jj_consume_token(21);
-            {if (true) return 11;}
-      break;
-    case 22:
-      jj_consume_token(22);
-            {if (true) return 12;}
-      break;
-    default:
-      jj_la1[3] = jj_gen;
-      jj_consume_token(-1);
-      throw new ParseException();
-    }
-    throw new Error("Missing return statement in function");
-  }
-
-  final public String year() throws ParseException {
- Token t;
-    t = jj_consume_token(DIGITS);
-                 {if (true) return t.image;}
-    throw new Error("Missing return statement in function");
-  }
-
-  final public Time time() throws ParseException {
- int h, m, s=0, z;
-    h = hour();
-    jj_consume_token(23);
-    m = minute();
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case 23:
-      jj_consume_token(23);
-      s = second();
-      break;
-    default:
-      jj_la1[4] = jj_gen;
-      ;
-    }
-    z = zone();
-      {if (true) return new Time(h, m, s, z);}
-    throw new Error("Missing return statement in function");
-  }
-
-  final public int hour() throws ParseException {
- Token t;
-    t = jj_consume_token(DIGITS);
-                 {if (true) return parseDigits(t);}
-    throw new Error("Missing return statement in function");
-  }
-
-  final public int minute() throws ParseException {
- Token t;
-    t = jj_consume_token(DIGITS);
-                 {if (true) return parseDigits(t);}
-    throw new Error("Missing return statement in function");
-  }
-
-  final public int second() throws ParseException {
- Token t;
-    t = jj_consume_token(DIGITS);
-                 {if (true) return parseDigits(t);}
-    throw new Error("Missing return statement in function");
-  }
-
-  final public int zone() throws ParseException {
-  Token t, u; int z;
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case OFFSETDIR:
-      t = jj_consume_token(OFFSETDIR);
-      u = jj_consume_token(DIGITS);
-                                              z=parseDigits(u)*(t.image.equals("-") ? -1 : 1);
-      break;
-    case 25:
-    case 26:
-    case 27:
-    case 28:
-    case 29:
-    case 30:
-    case 31:
-    case 32:
-    case 33:
-    case 34:
-    case MILITARY_ZONE:
-      z = obs_zone();
-      break;
-    default:
-      jj_la1[5] = jj_gen;
-      jj_consume_token(-1);
-      throw new ParseException();
-    }
-      {if (true) return z;}
-    throw new Error("Missing return statement in function");
-  }
-
-  final public int obs_zone() throws ParseException {
- Token t; int z;
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case 25:
-      jj_consume_token(25);
-            z=0;
-      break;
-    case 26:
-      jj_consume_token(26);
-            z=0;
-      break;
-    case 27:
-      jj_consume_token(27);
-            z=-5;
-      break;
-    case 28:
-      jj_consume_token(28);
-            z=-4;
-      break;
-    case 29:
-      jj_consume_token(29);
-            z=-6;
-      break;
-    case 30:
-      jj_consume_token(30);
-            z=-5;
-      break;
-    case 31:
-      jj_consume_token(31);
-            z=-7;
-      break;
-    case 32:
-      jj_consume_token(32);
-            z=-6;
-      break;
-    case 33:
-      jj_consume_token(33);
-            z=-8;
-      break;
-    case 34:
-      jj_consume_token(34);
-            z=-7;
-      break;
-    case MILITARY_ZONE:
-      t = jj_consume_token(MILITARY_ZONE);
-                                                             z=getMilitaryZoneOffset(t.image.charAt(0));
-      break;
-    default:
-      jj_la1[6] = jj_gen;
-      jj_consume_token(-1);
-      throw new ParseException();
-    }
-      {if (true) return z * 100;}
-    throw new Error("Missing return statement in function");
-  }
-
-  public DateTimeParserTokenManager token_source;
-  SimpleCharStream jj_input_stream;
-  public Token token, jj_nt;
-  private int jj_ntk;
-  private int jj_gen;
-  final private int[] jj_la1 = new int[7];
-  static private int[] jj_la1_0;
-  static private int[] jj_la1_1;
-  static {
-      jj_la1_0();
-      jj_la1_1();
-   }
-   private static void jj_la1_0() {
-      jj_la1_0 = new int[] {0x2,0x7f0,0x7f0,0x7ff800,0x800000,0xff000000,0xfe000000,};
-   }
-   private static void jj_la1_1() {
-      jj_la1_1 = new int[] {0x0,0x0,0x0,0x0,0x0,0xf,0xf,};
-   }
-
-  public DateTimeParser(java.io.InputStream stream) {
-     this(stream, null);
-  }
-  public DateTimeParser(java.io.InputStream stream, String encoding) {
-    try { jj_input_stream = new SimpleCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
-    token_source = new DateTimeParserTokenManager(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 7; i++) jj_la1[i] = -1;
-  }
-
-  public void ReInit(java.io.InputStream stream) {
-     ReInit(stream, null);
-  }
-  public void ReInit(java.io.InputStream stream, String encoding) {
-    try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
-    token_source.ReInit(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 7; i++) jj_la1[i] = -1;
-  }
-
-  public DateTimeParser(java.io.Reader stream) {
-    jj_input_stream = new SimpleCharStream(stream, 1, 1);
-    token_source = new DateTimeParserTokenManager(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 7; i++) jj_la1[i] = -1;
-  }
-
-  public void ReInit(java.io.Reader stream) {
-    jj_input_stream.ReInit(stream, 1, 1);
-    token_source.ReInit(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 7; i++) jj_la1[i] = -1;
-  }
-
-  public DateTimeParser(DateTimeParserTokenManager tm) {
-    token_source = tm;
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 7; i++) jj_la1[i] = -1;
-  }
-
-  public void ReInit(DateTimeParserTokenManager tm) {
-    token_source = tm;
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 7; i++) jj_la1[i] = -1;
-  }
-
-  final private Token jj_consume_token(int kind) throws ParseException {
-    Token oldToken;
-    if ((oldToken = token).next != null) token = token.next;
-    else token = token.next = token_source.getNextToken();
-    jj_ntk = -1;
-    if (token.kind == kind) {
-      jj_gen++;
-      return token;
-    }
-    token = oldToken;
-    jj_kind = kind;
-    throw generateParseException();
-  }
-
-  final public Token getNextToken() {
-    if (token.next != null) token = token.next;
-    else token = token.next = token_source.getNextToken();
-    jj_ntk = -1;
-    jj_gen++;
-    return token;
-  }
-
-  final public Token getToken(int index) {
-    Token t = token;
-    for (int i = 0; i < index; i++) {
-      if (t.next != null) t = t.next;
-      else t = t.next = token_source.getNextToken();
-    }
-    return t;
-  }
-
-  final private int jj_ntk() {
-    if ((jj_nt=token.next) == null)
-      return (jj_ntk = (token.next=token_source.getNextToken()).kind);
-    else
-      return (jj_ntk = jj_nt.kind);
-  }
-
-  private Vector<int[]> jj_expentries = new Vector<int[]>();
-  private int[] jj_expentry;
-  private int jj_kind = -1;
-
-  public ParseException generateParseException() {
-    jj_expentries.removeAllElements();
-    boolean[] la1tokens = new boolean[49];
-    for (int i = 0; i < 49; i++) {
-      la1tokens[i] = false;
-    }
-    if (jj_kind >= 0) {
-      la1tokens[jj_kind] = true;
-      jj_kind = -1;
-    }
-    for (int i = 0; i < 7; i++) {
-      if (jj_la1[i] == jj_gen) {
-        for (int j = 0; j < 32; j++) {
-          if ((jj_la1_0[i] & (1<<j)) != 0) {
-            la1tokens[j] = true;
-          }
-          if ((jj_la1_1[i] & (1<<j)) != 0) {
-            la1tokens[32+j] = true;
-          }
-        }
-      }
-    }
-    for (int i = 0; i < 49; i++) {
-      if (la1tokens[i]) {
-        jj_expentry = new int[1];
-        jj_expentry[0] = i;
-        jj_expentries.addElement(jj_expentry);
-      }
-    }
-    int[][] exptokseq = new int[jj_expentries.size()][];
-    for (int i = 0; i < jj_expentries.size(); i++) {
-      exptokseq[i] = jj_expentries.elementAt(i);
-    }
-    return new ParseException(token, exptokseq, tokenImage);
-  }
-
-  final public void enable_tracing() {
-  }
-
-  final public void disable_tracing() {
-  }
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java
deleted file mode 100644
index 2c203db..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. DateTimeParserConstants.java */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.datetime.parser;
-
-public interface DateTimeParserConstants {
-
-  int EOF = 0;
-  int OFFSETDIR = 24;
-  int MILITARY_ZONE = 35;
-  int WS = 36;
-  int COMMENT = 38;
-  int DIGITS = 46;
-  int QUOTEDPAIR = 47;
-  int ANY = 48;
-
-  int DEFAULT = 0;
-  int INCOMMENT = 1;
-  int NESTED_COMMENT = 2;
-
-  String[] tokenImage = {
-    "<EOF>",
-    "\"\\r\"",
-    "\"\\n\"",
-    "\",\"",
-    "\"Mon\"",
-    "\"Tue\"",
-    "\"Wed\"",
-    "\"Thu\"",
-    "\"Fri\"",
-    "\"Sat\"",
-    "\"Sun\"",
-    "\"Jan\"",
-    "\"Feb\"",
-    "\"Mar\"",
-    "\"Apr\"",
-    "\"May\"",
-    "\"Jun\"",
-    "\"Jul\"",
-    "\"Aug\"",
-    "\"Sep\"",
-    "\"Oct\"",
-    "\"Nov\"",
-    "\"Dec\"",
-    "\":\"",
-    "<OFFSETDIR>",
-    "\"UT\"",
-    "\"GMT\"",
-    "\"EST\"",
-    "\"EDT\"",
-    "\"CST\"",
-    "\"CDT\"",
-    "\"MST\"",
-    "\"MDT\"",
-    "\"PST\"",
-    "\"PDT\"",
-    "<MILITARY_ZONE>",
-    "<WS>",
-    "\"(\"",
-    "\")\"",
-    "<token of kind 39>",
-    "\"(\"",
-    "<token of kind 41>",
-    "<token of kind 42>",
-    "\"(\"",
-    "\")\"",
-    "<token of kind 45>",
-    "<DIGITS>",
-    "<QUOTEDPAIR>",
-    "<ANY>",
-  };
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java
deleted file mode 100644
index 4b2d2fd..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java
+++ /dev/null
@@ -1,882 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. DateTimeParserTokenManager.java */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.datetime.parser;
-import org.apache.james.mime4j.field.datetime.DateTime;
-import java.util.Calendar;
-
-public class DateTimeParserTokenManager implements DateTimeParserConstants
-{
-        // Keeps track of how many levels of comment nesting
-        // we've encountered.  This is only used when the 2nd
-        // level is reached, for example ((this)), not (this).
-        // This is because the outermost level must be treated
-        // specially anyway, because the outermost ")" has a
-        // different token type than inner ")" instances.
-        static int commentNest;
-  public  java.io.PrintStream debugStream = System.out;
-  public  void setDebugStream(java.io.PrintStream ds) { debugStream = ds; }
-private final int jjStopStringLiteralDfa_0(int pos, long active0)
-{
-   switch (pos)
-   {
-      case 0:
-         if ((active0 & 0x7fe7cf7f0L) != 0L)
-         {
-            jjmatchedKind = 35;
-            return -1;
-         }
-         return -1;
-      case 1:
-         if ((active0 & 0x7fe7cf7f0L) != 0L)
-         {
-            if (jjmatchedPos == 0)
-            {
-               jjmatchedKind = 35;
-               jjmatchedPos = 0;
-            }
-            return -1;
-         }
-         return -1;
-      default :
-         return -1;
-   }
-}
-private final int jjStartNfa_0(int pos, long active0)
-{
-   return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0), pos + 1);
-}
-private final int jjStopAtPos(int pos, int kind)
-{
-   jjmatchedKind = kind;
-   jjmatchedPos = pos;
-   return pos + 1;
-}
-private final int jjStartNfaWithStates_0(int pos, int kind, int state)
-{
-   jjmatchedKind = kind;
-   jjmatchedPos = pos;
-   try { curChar = input_stream.readChar(); }
-   catch(java.io.IOException e) { return pos + 1; }
-   return jjMoveNfa_0(state, pos + 1);
-}
-private final int jjMoveStringLiteralDfa0_0()
-{
-   switch(curChar)
-   {
-      case 10:
-         return jjStopAtPos(0, 2);
-      case 13:
-         return jjStopAtPos(0, 1);
-      case 40:
-         return jjStopAtPos(0, 37);
-      case 44:
-         return jjStopAtPos(0, 3);
-      case 58:
-         return jjStopAtPos(0, 23);
-      case 65:
-         return jjMoveStringLiteralDfa1_0(0x44000L);
-      case 67:
-         return jjMoveStringLiteralDfa1_0(0x60000000L);
-      case 68:
-         return jjMoveStringLiteralDfa1_0(0x400000L);
-      case 69:
-         return jjMoveStringLiteralDfa1_0(0x18000000L);
-      case 70:
-         return jjMoveStringLiteralDfa1_0(0x1100L);
-      case 71:
-         return jjMoveStringLiteralDfa1_0(0x4000000L);
-      case 74:
-         return jjMoveStringLiteralDfa1_0(0x30800L);
-      case 77:
-         return jjMoveStringLiteralDfa1_0(0x18000a010L);
-      case 78:
-         return jjMoveStringLiteralDfa1_0(0x200000L);
-      case 79:
-         return jjMoveStringLiteralDfa1_0(0x100000L);
-      case 80:
-         return jjMoveStringLiteralDfa1_0(0x600000000L);
-      case 83:
-         return jjMoveStringLiteralDfa1_0(0x80600L);
-      case 84:
-         return jjMoveStringLiteralDfa1_0(0xa0L);
-      case 85:
-         return jjMoveStringLiteralDfa1_0(0x2000000L);
-      case 87:
-         return jjMoveStringLiteralDfa1_0(0x40L);
-      default :
-         return jjMoveNfa_0(0, 0);
-   }
-}
-private final int jjMoveStringLiteralDfa1_0(long active0)
-{
-   try { curChar = input_stream.readChar(); }
-   catch(java.io.IOException e) {
-      jjStopStringLiteralDfa_0(0, active0);
-      return 1;
-   }
-   switch(curChar)
-   {
-      case 68:
-         return jjMoveStringLiteralDfa2_0(active0, 0x550000000L);
-      case 77:
-         return jjMoveStringLiteralDfa2_0(active0, 0x4000000L);
-      case 83:
-         return jjMoveStringLiteralDfa2_0(active0, 0x2a8000000L);
-      case 84:
-         if ((active0 & 0x2000000L) != 0L)
-            return jjStopAtPos(1, 25);
-         break;
-      case 97:
-         return jjMoveStringLiteralDfa2_0(active0, 0xaa00L);
-      case 99:
-         return jjMoveStringLiteralDfa2_0(active0, 0x100000L);
-      case 101:
-         return jjMoveStringLiteralDfa2_0(active0, 0x481040L);
-      case 104:
-         return jjMoveStringLiteralDfa2_0(active0, 0x80L);
-      case 111:
-         return jjMoveStringLiteralDfa2_0(active0, 0x200010L);
-      case 112:
-         return jjMoveStringLiteralDfa2_0(active0, 0x4000L);
-      case 114:
-         return jjMoveStringLiteralDfa2_0(active0, 0x100L);
-      case 117:
-         return jjMoveStringLiteralDfa2_0(active0, 0x70420L);
-      default :
-         break;
-   }
-   return jjStartNfa_0(0, active0);
-}
-private final int jjMoveStringLiteralDfa2_0(long old0, long active0)
-{
-   if (((active0 &= old0)) == 0L)
-      return jjStartNfa_0(0, old0); 
-   try { curChar = input_stream.readChar(); }
-   catch(java.io.IOException e) {
-      jjStopStringLiteralDfa_0(1, active0);
-      return 2;
-   }
-   switch(curChar)
-   {
-      case 84:
-         if ((active0 & 0x4000000L) != 0L)
-            return jjStopAtPos(2, 26);
-         else if ((active0 & 0x8000000L) != 0L)
-            return jjStopAtPos(2, 27);
-         else if ((active0 & 0x10000000L) != 0L)
-            return jjStopAtPos(2, 28);
-         else if ((active0 & 0x20000000L) != 0L)
-            return jjStopAtPos(2, 29);
-         else if ((active0 & 0x40000000L) != 0L)
-            return jjStopAtPos(2, 30);
-         else if ((active0 & 0x80000000L) != 0L)
-            return jjStopAtPos(2, 31);
-         else if ((active0 & 0x100000000L) != 0L)
-            return jjStopAtPos(2, 32);
-         else if ((active0 & 0x200000000L) != 0L)
-            return jjStopAtPos(2, 33);
-         else if ((active0 & 0x400000000L) != 0L)
-            return jjStopAtPos(2, 34);
-         break;
-      case 98:
-         if ((active0 & 0x1000L) != 0L)
-            return jjStopAtPos(2, 12);
-         break;
-      case 99:
-         if ((active0 & 0x400000L) != 0L)
-            return jjStopAtPos(2, 22);
-         break;
-      case 100:
-         if ((active0 & 0x40L) != 0L)
-            return jjStopAtPos(2, 6);
-         break;
-      case 101:
-         if ((active0 & 0x20L) != 0L)
-            return jjStopAtPos(2, 5);
-         break;
-      case 103:
-         if ((active0 & 0x40000L) != 0L)
-            return jjStopAtPos(2, 18);
-         break;
-      case 105:
-         if ((active0 & 0x100L) != 0L)
-            return jjStopAtPos(2, 8);
-         break;
-      case 108:
-         if ((active0 & 0x20000L) != 0L)
-            return jjStopAtPos(2, 17);
-         break;
-      case 110:
-         if ((active0 & 0x10L) != 0L)
-            return jjStopAtPos(2, 4);
-         else if ((active0 & 0x400L) != 0L)
-            return jjStopAtPos(2, 10);
-         else if ((active0 & 0x800L) != 0L)
-            return jjStopAtPos(2, 11);
-         else if ((active0 & 0x10000L) != 0L)
-            return jjStopAtPos(2, 16);
-         break;
-      case 112:
-         if ((active0 & 0x80000L) != 0L)
-            return jjStopAtPos(2, 19);
-         break;
-      case 114:
-         if ((active0 & 0x2000L) != 0L)
-            return jjStopAtPos(2, 13);
-         else if ((active0 & 0x4000L) != 0L)
-            return jjStopAtPos(2, 14);
-         break;
-      case 116:
-         if ((active0 & 0x200L) != 0L)
-            return jjStopAtPos(2, 9);
-         else if ((active0 & 0x100000L) != 0L)
-            return jjStopAtPos(2, 20);
-         break;
-      case 117:
-         if ((active0 & 0x80L) != 0L)
-            return jjStopAtPos(2, 7);
-         break;
-      case 118:
-         if ((active0 & 0x200000L) != 0L)
-            return jjStopAtPos(2, 21);
-         break;
-      case 121:
-         if ((active0 & 0x8000L) != 0L)
-            return jjStopAtPos(2, 15);
-         break;
-      default :
-         break;
-   }
-   return jjStartNfa_0(1, active0);
-}
-private final void jjCheckNAdd(int state)
-{
-   if (jjrounds[state] != jjround)
-   {
-      jjstateSet[jjnewStateCnt++] = state;
-      jjrounds[state] = jjround;
-   }
-}
-private final void jjAddStates(int start, int end)
-{
-   do {
-      jjstateSet[jjnewStateCnt++] = jjnextStates[start];
-   } while (start++ != end);
-}
-private final void jjCheckNAddTwoStates(int state1, int state2)
-{
-   jjCheckNAdd(state1);
-   jjCheckNAdd(state2);
-}
-private final void jjCheckNAddStates(int start, int end)
-{
-   do {
-      jjCheckNAdd(jjnextStates[start]);
-   } while (start++ != end);
-}
-private final void jjCheckNAddStates(int start)
-{
-   jjCheckNAdd(jjnextStates[start]);
-   jjCheckNAdd(jjnextStates[start + 1]);
-}
-private final int jjMoveNfa_0(int startState, int curPos)
-{
-   int[] nextStates;
-   int startsAt = 0;
-   jjnewStateCnt = 4;
-   int i = 1;
-   jjstateSet[0] = startState;
-   int j, kind = 0x7fffffff;
-   for (;;)
-   {
-      if (++jjround == 0x7fffffff)
-         ReInitRounds();
-      if (curChar < 64)
-      {
-         long l = 1L << curChar;
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if ((0x3ff000000000000L & l) != 0L)
-                  {
-                     if (kind > 46)
-                        kind = 46;
-                     jjCheckNAdd(3);
-                  }
-                  else if ((0x100000200L & l) != 0L)
-                  {
-                     if (kind > 36)
-                        kind = 36;
-                     jjCheckNAdd(2);
-                  }
-                  else if ((0x280000000000L & l) != 0L)
-                  {
-                     if (kind > 24)
-                        kind = 24;
-                  }
-                  break;
-               case 2:
-                  if ((0x100000200L & l) == 0L)
-                     break;
-                  kind = 36;
-                  jjCheckNAdd(2);
-                  break;
-               case 3:
-                  if ((0x3ff000000000000L & l) == 0L)
-                     break;
-                  kind = 46;
-                  jjCheckNAdd(3);
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else if (curChar < 128)
-      {
-         long l = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if ((0x7fffbfe07fffbfeL & l) != 0L)
-                     kind = 35;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else
-      {
-         int i2 = (curChar & 0xff) >> 6;
-         long l2 = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      if (kind != 0x7fffffff)
-      {
-         jjmatchedKind = kind;
-         jjmatchedPos = curPos;
-         kind = 0x7fffffff;
-      }
-      ++curPos;
-      if ((i = jjnewStateCnt) == (startsAt = 4 - (jjnewStateCnt = startsAt)))
-         return curPos;
-      try { curChar = input_stream.readChar(); }
-      catch(java.io.IOException e) { return curPos; }
-   }
-}
-private final int jjStopStringLiteralDfa_1(int pos, long active0)
-{
-   switch (pos)
-   {
-      default :
-         return -1;
-   }
-}
-private final int jjStartNfa_1(int pos, long active0)
-{
-   return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1);
-}
-private final int jjStartNfaWithStates_1(int pos, int kind, int state)
-{
-   jjmatchedKind = kind;
-   jjmatchedPos = pos;
-   try { curChar = input_stream.readChar(); }
-   catch(java.io.IOException e) { return pos + 1; }
-   return jjMoveNfa_1(state, pos + 1);
-}
-private final int jjMoveStringLiteralDfa0_1()
-{
-   switch(curChar)
-   {
-      case 40:
-         return jjStopAtPos(0, 40);
-      case 41:
-         return jjStopAtPos(0, 38);
-      default :
-         return jjMoveNfa_1(0, 0);
-   }
-}
-static final long[] jjbitVec0 = {
-   0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL
-};
-private final int jjMoveNfa_1(int startState, int curPos)
-{
-   int[] nextStates;
-   int startsAt = 0;
-   jjnewStateCnt = 3;
-   int i = 1;
-   jjstateSet[0] = startState;
-   int j, kind = 0x7fffffff;
-   for (;;)
-   {
-      if (++jjround == 0x7fffffff)
-         ReInitRounds();
-      if (curChar < 64)
-      {
-         long l = 1L << curChar;
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if (kind > 41)
-                     kind = 41;
-                  break;
-               case 1:
-                  if (kind > 39)
-                     kind = 39;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else if (curChar < 128)
-      {
-         long l = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if (kind > 41)
-                     kind = 41;
-                  if (curChar == 92)
-                     jjstateSet[jjnewStateCnt++] = 1;
-                  break;
-               case 1:
-                  if (kind > 39)
-                     kind = 39;
-                  break;
-               case 2:
-                  if (kind > 41)
-                     kind = 41;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else
-      {
-         int i2 = (curChar & 0xff) >> 6;
-         long l2 = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if ((jjbitVec0[i2] & l2) != 0L && kind > 41)
-                     kind = 41;
-                  break;
-               case 1:
-                  if ((jjbitVec0[i2] & l2) != 0L && kind > 39)
-                     kind = 39;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      if (kind != 0x7fffffff)
-      {
-         jjmatchedKind = kind;
-         jjmatchedPos = curPos;
-         kind = 0x7fffffff;
-      }
-      ++curPos;
-      if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
-         return curPos;
-      try { curChar = input_stream.readChar(); }
-      catch(java.io.IOException e) { return curPos; }
-   }
-}
-private final int jjStopStringLiteralDfa_2(int pos, long active0)
-{
-   switch (pos)
-   {
-      default :
-         return -1;
-   }
-}
-private final int jjStartNfa_2(int pos, long active0)
-{
-   return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1);
-}
-private final int jjStartNfaWithStates_2(int pos, int kind, int state)
-{
-   jjmatchedKind = kind;
-   jjmatchedPos = pos;
-   try { curChar = input_stream.readChar(); }
-   catch(java.io.IOException e) { return pos + 1; }
-   return jjMoveNfa_2(state, pos + 1);
-}
-private final int jjMoveStringLiteralDfa0_2()
-{
-   switch(curChar)
-   {
-      case 40:
-         return jjStopAtPos(0, 43);
-      case 41:
-         return jjStopAtPos(0, 44);
-      default :
-         return jjMoveNfa_2(0, 0);
-   }
-}
-private final int jjMoveNfa_2(int startState, int curPos)
-{
-   int[] nextStates;
-   int startsAt = 0;
-   jjnewStateCnt = 3;
-   int i = 1;
-   jjstateSet[0] = startState;
-   int j, kind = 0x7fffffff;
-   for (;;)
-   {
-      if (++jjround == 0x7fffffff)
-         ReInitRounds();
-      if (curChar < 64)
-      {
-         long l = 1L << curChar;
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if (kind > 45)
-                     kind = 45;
-                  break;
-               case 1:
-                  if (kind > 42)
-                     kind = 42;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else if (curChar < 128)
-      {
-         long l = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if (kind > 45)
-                     kind = 45;
-                  if (curChar == 92)
-                     jjstateSet[jjnewStateCnt++] = 1;
-                  break;
-               case 1:
-                  if (kind > 42)
-                     kind = 42;
-                  break;
-               case 2:
-                  if (kind > 45)
-                     kind = 45;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      else
-      {
-         int i2 = (curChar & 0xff) >> 6;
-         long l2 = 1L << (curChar & 077);
-         MatchLoop: do
-         {
-            switch(jjstateSet[--i])
-            {
-               case 0:
-                  if ((jjbitVec0[i2] & l2) != 0L && kind > 45)
-                     kind = 45;
-                  break;
-               case 1:
-                  if ((jjbitVec0[i2] & l2) != 0L && kind > 42)
-                     kind = 42;
-                  break;
-               default : break;
-            }
-         } while(i != startsAt);
-      }
-      if (kind != 0x7fffffff)
-      {
-         jjmatchedKind = kind;
-         jjmatchedPos = curPos;
-         kind = 0x7fffffff;
-      }
-      ++curPos;
-      if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
-         return curPos;
-      try { curChar = input_stream.readChar(); }
-      catch(java.io.IOException e) { return curPos; }
-   }
-}
-static final int[] jjnextStates = {
-};
-public static final String[] jjstrLiteralImages = {
-"", "\15", "\12", "\54", "\115\157\156", "\124\165\145", "\127\145\144", 
-"\124\150\165", "\106\162\151", "\123\141\164", "\123\165\156", "\112\141\156", 
-"\106\145\142", "\115\141\162", "\101\160\162", "\115\141\171", "\112\165\156", 
-"\112\165\154", "\101\165\147", "\123\145\160", "\117\143\164", "\116\157\166", 
-"\104\145\143", "\72", null, "\125\124", "\107\115\124", "\105\123\124", "\105\104\124", 
-"\103\123\124", "\103\104\124", "\115\123\124", "\115\104\124", "\120\123\124", 
-"\120\104\124", null, null, null, null, null, null, null, null, null, null, null, null, null, 
-null, };
-public static final String[] lexStateNames = {
-   "DEFAULT", 
-   "INCOMMENT", 
-   "NESTED_COMMENT", 
-};
-public static final int[] jjnewLexState = {
-   -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 
-   -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 0, -1, 2, -1, -1, -1, -1, -1, -1, -1, -1, 
-};
-static final long[] jjtoToken = {
-   0x400fffffffffL, 
-};
-static final long[] jjtoSkip = {
-   0x5000000000L, 
-};
-static final long[] jjtoSpecial = {
-   0x1000000000L, 
-};
-static final long[] jjtoMore = {
-   0x3fa000000000L, 
-};
-protected SimpleCharStream input_stream;
-private final int[] jjrounds = new int[4];
-private final int[] jjstateSet = new int[8];
-StringBuffer image;
-int jjimageLen;
-int lengthOfMatch;
-protected char curChar;
-public DateTimeParserTokenManager(SimpleCharStream stream){
-   if (SimpleCharStream.staticFlag)
-      throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer.");
-   input_stream = stream;
-}
-public DateTimeParserTokenManager(SimpleCharStream stream, int lexState){
-   this(stream);
-   SwitchTo(lexState);
-}
-public void ReInit(SimpleCharStream stream)
-{
-   jjmatchedPos = jjnewStateCnt = 0;
-   curLexState = defaultLexState;
-   input_stream = stream;
-   ReInitRounds();
-}
-private final void ReInitRounds()
-{
-   int i;
-   jjround = 0x80000001;
-   for (i = 4; i-- > 0;)
-      jjrounds[i] = 0x80000000;
-}
-public void ReInit(SimpleCharStream stream, int lexState)
-{
-   ReInit(stream);
-   SwitchTo(lexState);
-}
-public void SwitchTo(int lexState)
-{
-   if (lexState >= 3 || lexState < 0)
-      throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE);
-   else
-      curLexState = lexState;
-}
-
-protected Token jjFillToken()
-{
-   Token t = Token.newToken(jjmatchedKind);
-   t.kind = jjmatchedKind;
-   String im = jjstrLiteralImages[jjmatchedKind];
-   t.image = (im == null) ? input_stream.GetImage() : im;
-   t.beginLine = input_stream.getBeginLine();
-   t.beginColumn = input_stream.getBeginColumn();
-   t.endLine = input_stream.getEndLine();
-   t.endColumn = input_stream.getEndColumn();
-   return t;
-}
-
-int curLexState = 0;
-int defaultLexState = 0;
-int jjnewStateCnt;
-int jjround;
-int jjmatchedPos;
-int jjmatchedKind;
-
-public Token getNextToken() 
-{
-  int kind;
-  Token specialToken = null;
-  Token matchedToken;
-  int curPos = 0;
-
-  EOFLoop :
-  for (;;)
-  {   
-   try   
-   {     
-      curChar = input_stream.BeginToken();
-   }     
-   catch(java.io.IOException e)
-   {        
-      jjmatchedKind = 0;
-      matchedToken = jjFillToken();
-      matchedToken.specialToken = specialToken;
-      return matchedToken;
-   }
-   image = null;
-   jjimageLen = 0;
-
-   for (;;)
-   {
-     switch(curLexState)
-     {
-       case 0:
-         jjmatchedKind = 0x7fffffff;
-         jjmatchedPos = 0;
-         curPos = jjMoveStringLiteralDfa0_0();
-         break;
-       case 1:
-         jjmatchedKind = 0x7fffffff;
-         jjmatchedPos = 0;
-         curPos = jjMoveStringLiteralDfa0_1();
-         break;
-       case 2:
-         jjmatchedKind = 0x7fffffff;
-         jjmatchedPos = 0;
-         curPos = jjMoveStringLiteralDfa0_2();
-         break;
-     }
-     if (jjmatchedKind != 0x7fffffff)
-     {
-        if (jjmatchedPos + 1 < curPos)
-           input_stream.backup(curPos - jjmatchedPos - 1);
-        if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
-        {
-           matchedToken = jjFillToken();
-           matchedToken.specialToken = specialToken;
-       if (jjnewLexState[jjmatchedKind] != -1)
-         curLexState = jjnewLexState[jjmatchedKind];
-           return matchedToken;
-        }
-        else if ((jjtoSkip[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
-        {
-           if ((jjtoSpecial[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
-           {
-              matchedToken = jjFillToken();
-              if (specialToken == null)
-                 specialToken = matchedToken;
-              else
-              {
-                 matchedToken.specialToken = specialToken;
-                 specialToken = (specialToken.next = matchedToken);
-              }
-           }
-         if (jjnewLexState[jjmatchedKind] != -1)
-           curLexState = jjnewLexState[jjmatchedKind];
-           continue EOFLoop;
-        }
-        MoreLexicalActions();
-      if (jjnewLexState[jjmatchedKind] != -1)
-        curLexState = jjnewLexState[jjmatchedKind];
-        curPos = 0;
-        jjmatchedKind = 0x7fffffff;
-        try {
-           curChar = input_stream.readChar();
-           continue;
-        }
-        catch (java.io.IOException e1) { }
-     }
-     int error_line = input_stream.getEndLine();
-     int error_column = input_stream.getEndColumn();
-     String error_after = null;
-     boolean EOFSeen = false;
-     try { input_stream.readChar(); input_stream.backup(1); }
-     catch (java.io.IOException e1) {
-        EOFSeen = true;
-        error_after = curPos <= 1 ? "" : input_stream.GetImage();
-        if (curChar == '\n' || curChar == '\r') {
-           error_line++;
-           error_column = 0;
-        }
-        else
-           error_column++;
-     }
-     if (!EOFSeen) {
-        input_stream.backup(1);
-        error_after = curPos <= 1 ? "" : input_stream.GetImage();
-     }
-     throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR);
-   }
-  }
-}
-
-void MoreLexicalActions()
-{
-   jjimageLen += (lengthOfMatch = jjmatchedPos + 1);
-   switch(jjmatchedKind)
-   {
-      case 39 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-                          image.deleteCharAt(image.length() - 2);
-         break;
-      case 40 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-              commentNest = 1;
-         break;
-      case 42 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-                          image.deleteCharAt(image.length() - 2);
-         break;
-      case 43 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-              ++commentNest;
-         break;
-      case 44 :
-         if (image == null)
-            image = new StringBuffer();
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-              --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT);
-         break;
-      default : 
-         break;
-   }
-}
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java
deleted file mode 100644
index 13b3ff0..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.0 */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.datetime.parser;
-
-/**
- * This exception is thrown when parse errors are encountered.
- * You can explicitly create objects of this exception type by
- * calling the method generateParseException in the generated
- * parser.
- *
- * You can modify this class to customize your error reporting
- * mechanisms so long as you retain the public fields.
- */
-public class ParseException extends Exception {
-
-  /**
-   * This constructor is used by the method "generateParseException"
-   * in the generated parser.  Calling this constructor generates
-   * a new object of this type with the fields "currentToken",
-   * "expectedTokenSequences", and "tokenImage" set.  The boolean
-   * flag "specialConstructor" is also set to true to indicate that
-   * this constructor was used to create this object.
-   * This constructor calls its super class with the empty string
-   * to force the "toString" method of parent class "Throwable" to
-   * print the error message in the form:
-   *     ParseException: <result of getMessage>
-   */
-  public ParseException(Token currentTokenVal,
-                        int[][] expectedTokenSequencesVal,
-                        String[] tokenImageVal
-                       )
-  {
-    super("");
-    specialConstructor = true;
-    currentToken = currentTokenVal;
-    expectedTokenSequences = expectedTokenSequencesVal;
-    tokenImage = tokenImageVal;
-  }
-
-  /**
-   * The following constructors are for use by you for whatever
-   * purpose you can think of.  Constructing the exception in this
-   * manner makes the exception behave in the normal way - i.e., as
-   * documented in the class "Throwable".  The fields "errorToken",
-   * "expectedTokenSequences", and "tokenImage" do not contain
-   * relevant information.  The JavaCC generated code does not use
-   * these constructors.
-   */
-
-  public ParseException() {
-    super();
-    specialConstructor = false;
-  }
-
-  public ParseException(String message) {
-    super(message);
-    specialConstructor = false;
-  }
-
-  /**
-   * This variable determines which constructor was used to create
-   * this object and thereby affects the semantics of the
-   * "getMessage" method (see below).
-   */
-  protected boolean specialConstructor;
-
-  /**
-   * This is the last token that has been consumed successfully.  If
-   * this object has been created due to a parse error, the token
-   * followng this token will (therefore) be the first error token.
-   */
-  public Token currentToken;
-
-  /**
-   * Each entry in this array is an array of integers.  Each array
-   * of integers represents a sequence of tokens (by their ordinal
-   * values) that is expected at this point of the parse.
-   */
-  public int[][] expectedTokenSequences;
-
-  /**
-   * This is a reference to the "tokenImage" array of the generated
-   * parser within which the parse error occurred.  This array is
-   * defined in the generated ...Constants interface.
-   */
-  public String[] tokenImage;
-
-  /**
-   * This method has the standard behavior when this object has been
-   * created using the standard constructors.  Otherwise, it uses
-   * "currentToken" and "expectedTokenSequences" to generate a parse
-   * error message and returns it.  If this object has been created
-   * due to a parse error, and you do not catch it (it gets thrown
-   * from the parser), then this method is called during the printing
-   * of the final stack trace, and hence the correct error message
-   * gets displayed.
-   */
-  public String getMessage() {
-    if (!specialConstructor) {
-      return super.getMessage();
-    }
-    StringBuffer expected = new StringBuffer();
-    int maxSize = 0;
-    for (int i = 0; i < expectedTokenSequences.length; i++) {
-      if (maxSize < expectedTokenSequences[i].length) {
-        maxSize = expectedTokenSequences[i].length;
-      }
-      for (int j = 0; j < expectedTokenSequences[i].length; j++) {
-        expected.append(tokenImage[expectedTokenSequences[i][j]]).append(" ");
-      }
-      if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) {
-        expected.append("...");
-      }
-      expected.append(eol).append("    ");
-    }
-    String retval = "Encountered \"";
-    Token tok = currentToken.next;
-    for (int i = 0; i < maxSize; i++) {
-      if (i != 0) retval += " ";
-      if (tok.kind == 0) {
-        retval += tokenImage[0];
-        break;
-      }
-      retval += add_escapes(tok.image);
-      tok = tok.next; 
-    }
-    retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn;
-    retval += "." + eol;
-    if (expectedTokenSequences.length == 1) {
-      retval += "Was expecting:" + eol + "    ";
-    } else {
-      retval += "Was expecting one of:" + eol + "    ";
-    }
-    retval += expected.toString();
-    return retval;
-  }
-
-  /**
-   * The end of line string for this machine.
-   */
-  protected String eol = System.getProperty("line.separator", "\n");
- 
-  /**
-   * Used to convert raw characters to their escaped version
-   * when these raw version cannot be used as part of an ASCII
-   * string literal.
-   */
-  protected String add_escapes(String str) {
-      StringBuffer retval = new StringBuffer();
-      char ch;
-      for (int i = 0; i < str.length(); i++) {
-        switch (str.charAt(i))
-        {
-           case 0 :
-              continue;
-           case '\b':
-              retval.append("\\b");
-              continue;
-           case '\t':
-              retval.append("\\t");
-              continue;
-           case '\n':
-              retval.append("\\n");
-              continue;
-           case '\f':
-              retval.append("\\f");
-              continue;
-           case '\r':
-              retval.append("\\r");
-              continue;
-           case '\"':
-              retval.append("\\\"");
-              continue;
-           case '\'':
-              retval.append("\\\'");
-              continue;
-           case '\\':
-              retval.append("\\\\");
-              continue;
-           default:
-              if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
-                 String s = "0000" + Integer.toString(ch, 16);
-                 retval.append("\\u" + s.substring(s.length() - 4, s.length()));
-              } else {
-                 retval.append(ch);
-              }
-              continue;
-        }
-      }
-      return retval.toString();
-   }
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java
deleted file mode 100644
index 2724529..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java
+++ /dev/null
@@ -1,454 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 4.0 */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.datetime.parser;
-
-/**
- * An implementation of interface CharStream, where the stream is assumed to
- * contain only ASCII characters (without unicode processing).
- */
-
-public class SimpleCharStream
-{
-  public static final boolean staticFlag = false;
-  int bufsize;
-  int available;
-  int tokenBegin;
-  public int bufpos = -1;
-  protected int bufline[];
-  protected int bufcolumn[];
-
-  protected int column = 0;
-  protected int line = 1;
-
-  protected boolean prevCharIsCR = false;
-  protected boolean prevCharIsLF = false;
-
-  protected java.io.Reader inputStream;
-
-  protected char[] buffer;
-  protected int maxNextCharInd = 0;
-  protected int inBuf = 0;
-  protected int tabSize = 8;
-
-  protected void setTabSize(int i) { tabSize = i; }
-  protected int getTabSize(int i) { return tabSize; }
-
-
-  protected void ExpandBuff(boolean wrapAround)
-  {
-     char[] newbuffer = new char[bufsize + 2048];
-     int newbufline[] = new int[bufsize + 2048];
-     int newbufcolumn[] = new int[bufsize + 2048];
-
-     try
-     {
-        if (wrapAround)
-        {
-           System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin);
-           System.arraycopy(buffer, 0, newbuffer,
-                                             bufsize - tokenBegin, bufpos);
-           buffer = newbuffer;
-
-           System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin);
-           System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos);
-           bufline = newbufline;
-
-           System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin);
-           System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos);
-           bufcolumn = newbufcolumn;
-
-           maxNextCharInd = (bufpos += (bufsize - tokenBegin));
-        }
-        else
-        {
-           System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin);
-           buffer = newbuffer;
-
-           System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin);
-           bufline = newbufline;
-
-           System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin);
-           bufcolumn = newbufcolumn;
-
-           maxNextCharInd = (bufpos -= tokenBegin);
-        }
-     }
-     catch (Throwable t)
-     {
-        throw new Error(t.getMessage());
-     }
-
-
-     bufsize += 2048;
-     available = bufsize;
-     tokenBegin = 0;
-  }
-
-  protected void FillBuff() throws java.io.IOException
-  {
-     if (maxNextCharInd == available)
-     {
-        if (available == bufsize)
-        {
-           if (tokenBegin > 2048)
-           {
-              bufpos = maxNextCharInd = 0;
-              available = tokenBegin;
-           }
-           else if (tokenBegin < 0)
-              bufpos = maxNextCharInd = 0;
-           else
-              ExpandBuff(false);
-        }
-        else if (available > tokenBegin)
-           available = bufsize;
-        else if ((tokenBegin - available) < 2048)
-           ExpandBuff(true);
-        else
-           available = tokenBegin;
-     }
-
-     int i;
-     try {
-        if ((i = inputStream.read(buffer, maxNextCharInd,
-                                    available - maxNextCharInd)) == -1)
-        {
-           inputStream.close();
-           throw new java.io.IOException();
-        }
-        else
-           maxNextCharInd += i;
-        return;
-     }
-     catch(java.io.IOException e) {
-        --bufpos;
-        backup(0);
-        if (tokenBegin == -1)
-           tokenBegin = bufpos;
-        throw e;
-     }
-  }
-
-  public char BeginToken() throws java.io.IOException
-  {
-     tokenBegin = -1;
-     char c = readChar();
-     tokenBegin = bufpos;
-
-     return c;
-  }
-
-  protected void UpdateLineColumn(char c)
-  {
-     column++;
-
-     if (prevCharIsLF)
-     {
-        prevCharIsLF = false;
-        line += (column = 1);
-     }
-     else if (prevCharIsCR)
-     {
-        prevCharIsCR = false;
-        if (c == '\n')
-        {
-           prevCharIsLF = true;
-        }
-        else
-           line += (column = 1);
-     }
-
-     switch (c)
-     {
-        case '\r' :
-           prevCharIsCR = true;
-           break;
-        case '\n' :
-           prevCharIsLF = true;
-           break;
-        case '\t' :
-           column--;
-           column += (tabSize - (column % tabSize));
-           break;
-        default :
-           break;
-     }
-
-     bufline[bufpos] = line;
-     bufcolumn[bufpos] = column;
-  }
-
-  public char readChar() throws java.io.IOException
-  {
-     if (inBuf > 0)
-     {
-        --inBuf;
-
-        if (++bufpos == bufsize)
-           bufpos = 0;
-
-        return buffer[bufpos];
-     }
-
-     if (++bufpos >= maxNextCharInd)
-        FillBuff();
-
-     char c = buffer[bufpos];
-
-     UpdateLineColumn(c);
-     return (c);
-  }
-
-  /**
-   * @deprecated 
-   * @see #getEndColumn
-   */
-  @Deprecated
-  public int getColumn() {
-     return bufcolumn[bufpos];
-  }
-
-  /**
-   * @deprecated 
-   * @see #getEndLine
-   */
-  @Deprecated
-  public int getLine() {
-     return bufline[bufpos];
-  }
-
-  public int getEndColumn() {
-     return bufcolumn[bufpos];
-  }
-
-  public int getEndLine() {
-     return bufline[bufpos];
-  }
-
-  public int getBeginColumn() {
-     return bufcolumn[tokenBegin];
-  }
-
-  public int getBeginLine() {
-     return bufline[tokenBegin];
-  }
-
-  public void backup(int amount) {
-
-    inBuf += amount;
-    if ((bufpos -= amount) < 0)
-       bufpos += bufsize;
-  }
-
-  public SimpleCharStream(java.io.Reader dstream, int startline,
-  int startcolumn, int buffersize)
-  {
-    inputStream = dstream;
-    line = startline;
-    column = startcolumn - 1;
-
-    available = bufsize = buffersize;
-    buffer = new char[buffersize];
-    bufline = new int[buffersize];
-    bufcolumn = new int[buffersize];
-  }
-
-  public SimpleCharStream(java.io.Reader dstream, int startline,
-                          int startcolumn)
-  {
-     this(dstream, startline, startcolumn, 4096);
-  }
-
-  public SimpleCharStream(java.io.Reader dstream)
-  {
-     this(dstream, 1, 1, 4096);
-  }
-  public void ReInit(java.io.Reader dstream, int startline,
-  int startcolumn, int buffersize)
-  {
-    inputStream = dstream;
-    line = startline;
-    column = startcolumn - 1;
-
-    if (buffer == null || buffersize != buffer.length)
-    {
-      available = bufsize = buffersize;
-      buffer = new char[buffersize];
-      bufline = new int[buffersize];
-      bufcolumn = new int[buffersize];
-    }
-    prevCharIsLF = prevCharIsCR = false;
-    tokenBegin = inBuf = maxNextCharInd = 0;
-    bufpos = -1;
-  }
-
-  public void ReInit(java.io.Reader dstream, int startline,
-                     int startcolumn)
-  {
-     ReInit(dstream, startline, startcolumn, 4096);
-  }
-
-  public void ReInit(java.io.Reader dstream)
-  {
-     ReInit(dstream, 1, 1, 4096);
-  }
-  public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline,
-  int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException
-  {
-     this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize);
-  }
-
-  public SimpleCharStream(java.io.InputStream dstream, int startline,
-  int startcolumn, int buffersize)
-  {
-     this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize);
-  }
-
-  public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline,
-                          int startcolumn) throws java.io.UnsupportedEncodingException
-  {
-     this(dstream, encoding, startline, startcolumn, 4096);
-  }
-
-  public SimpleCharStream(java.io.InputStream dstream, int startline,
-                          int startcolumn)
-  {
-     this(dstream, startline, startcolumn, 4096);
-  }
-
-  public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException
-  {
-     this(dstream, encoding, 1, 1, 4096);
-  }
-
-  public SimpleCharStream(java.io.InputStream dstream)
-  {
-     this(dstream, 1, 1, 4096);
-  }
-
-  public void ReInit(java.io.InputStream dstream, String encoding, int startline,
-                          int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException
-  {
-     ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize);
-  }
-
-  public void ReInit(java.io.InputStream dstream, int startline,
-                          int startcolumn, int buffersize)
-  {
-     ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize);
-  }
-
-  public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException
-  {
-     ReInit(dstream, encoding, 1, 1, 4096);
-  }
-
-  public void ReInit(java.io.InputStream dstream)
-  {
-     ReInit(dstream, 1, 1, 4096);
-  }
-  public void ReInit(java.io.InputStream dstream, String encoding, int startline,
-                     int startcolumn) throws java.io.UnsupportedEncodingException
-  {
-     ReInit(dstream, encoding, startline, startcolumn, 4096);
-  }
-  public void ReInit(java.io.InputStream dstream, int startline,
-                     int startcolumn)
-  {
-     ReInit(dstream, startline, startcolumn, 4096);
-  }
-  public String GetImage()
-  {
-     if (bufpos >= tokenBegin)
-        return new String(buffer, tokenBegin, bufpos - tokenBegin + 1);
-     else
-        return new String(buffer, tokenBegin, bufsize - tokenBegin) +
-                              new String(buffer, 0, bufpos + 1);
-  }
-
-  public char[] GetSuffix(int len)
-  {
-     char[] ret = new char[len];
-
-     if ((bufpos + 1) >= len)
-        System.arraycopy(buffer, bufpos - len + 1, ret, 0, len);
-     else
-     {
-        System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0,
-                                                          len - bufpos - 1);
-        System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1);
-     }
-
-     return ret;
-  }
-
-  public void Done()
-  {
-     buffer = null;
-     bufline = null;
-     bufcolumn = null;
-  }
-
-  /**
-   * Method to adjust line and column numbers for the start of a token.
-   */
-  public void adjustBeginLineColumn(int newLine, int newCol)
-  {
-     int start = tokenBegin;
-     int len;
-
-     if (bufpos >= tokenBegin)
-     {
-        len = bufpos - tokenBegin + inBuf + 1;
-     }
-     else
-     {
-        len = bufsize - tokenBegin + bufpos + 1 + inBuf;
-     }
-
-     int i = 0, j = 0, k = 0;
-     int nextColDiff = 0, columnDiff = 0;
-
-     while (i < len &&
-            bufline[j = start % bufsize] == bufline[k = ++start % bufsize])
-     {
-        bufline[j] = newLine;
-        nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j];
-        bufcolumn[j] = newCol + columnDiff;
-        columnDiff = nextColDiff;
-        i++;
-     } 
-
-     if (i < len)
-     {
-        bufline[j] = newLine++;
-        bufcolumn[j] = newCol + columnDiff;
-
-        while (i++ < len)
-        {
-           if (bufline[j = start % bufsize] != bufline[++start % bufsize])
-              bufline[j] = newLine++;
-           else
-              bufline[j] = newLine;
-        }
-     }
-
-     line = bufline[j];
-     column = bufcolumn[j];
-  }
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/Token.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/Token.java
deleted file mode 100644
index 0927a09..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/Token.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. Token.java Version 3.0 */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.datetime.parser;
-
-/**
- * Describes the input token stream.
- */
-
-public class Token {
-
-  /**
-   * An integer that describes the kind of this token.  This numbering
-   * system is determined by JavaCCParser, and a table of these numbers is
-   * stored in the file ...Constants.java.
-   */
-  public int kind;
-
-  /**
-   * beginLine and beginColumn describe the position of the first character
-   * of this token; endLine and endColumn describe the position of the
-   * last character of this token.
-   */
-  public int beginLine, beginColumn, endLine, endColumn;
-
-  /**
-   * The string image of the token.
-   */
-  public String image;
-
-  /**
-   * A reference to the next regular (non-special) token from the input
-   * stream.  If this is the last token from the input stream, or if the
-   * token manager has not read tokens beyond this one, this field is
-   * set to null.  This is true only if this token is also a regular
-   * token.  Otherwise, see below for a description of the contents of
-   * this field.
-   */
-  public Token next;
-
-  /**
-   * This field is used to access special tokens that occur prior to this
-   * token, but after the immediately preceding regular (non-special) token.
-   * If there are no such special tokens, this field is set to null.
-   * When there are more than one such special token, this field refers
-   * to the last of these special tokens, which in turn refers to the next
-   * previous special token through its specialToken field, and so on
-   * until the first special token (whose specialToken field is null).
-   * The next fields of special tokens refer to other special tokens that
-   * immediately follow it (without an intervening regular token).  If there
-   * is no such token, this field is null.
-   */
-  public Token specialToken;
-
-  /**
-   * Returns the image.
-   */
-  public String toString()
-  {
-     return image;
-  }
-
-  /**
-   * Returns a new Token object, by default. However, if you want, you
-   * can create and return subclass objects based on the value of ofKind.
-   * Simply add the cases to the switch for all those special cases.
-   * For example, if you have a subclass of Token called IDToken that
-   * you want to create if ofKind is ID, simlpy add something like :
-   *
-   *    case MyParserConstants.ID : return new IDToken();
-   *
-   * to the following switch statement. Then you can cast matchedToken
-   * variable to the appropriate type and use it in your lexical actions.
-   */
-  public static final Token newToken(int ofKind)
-  {
-     switch(ofKind)
-     {
-       default : return new Token();
-     }
-  }
-
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java
deleted file mode 100644
index e7043c1..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 3.0 */
-/*
- *  Copyright 2004 the mime4j project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.james.mime4j.field.datetime.parser;
-
-public class TokenMgrError extends Error
-{
-   /*
-    * Ordinals for various reasons why an Error of this type can be thrown.
-    */
-
-   /**
-    * Lexical error occured.
-    */
-   static final int LEXICAL_ERROR = 0;
-
-   /**
-    * An attempt wass made to create a second instance of a static token manager.
-    */
-   static final int STATIC_LEXER_ERROR = 1;
-
-   /**
-    * Tried to change to an invalid lexical state.
-    */
-   static final int INVALID_LEXICAL_STATE = 2;
-
-   /**
-    * Detected (and bailed out of) an infinite loop in the token manager.
-    */
-   static final int LOOP_DETECTED = 3;
-
-   /**
-    * Indicates the reason why the exception is thrown. It will have
-    * one of the above 4 values.
-    */
-   int errorCode;
-
-   /**
-    * Replaces unprintable characters by their espaced (or unicode escaped)
-    * equivalents in the given string
-    */
-   protected static final String addEscapes(String str) {
-      StringBuffer retval = new StringBuffer();
-      char ch;
-      for (int i = 0; i < str.length(); i++) {
-        switch (str.charAt(i))
-        {
-           case 0 :
-              continue;
-           case '\b':
-              retval.append("\\b");
-              continue;
-           case '\t':
-              retval.append("\\t");
-              continue;
-           case '\n':
-              retval.append("\\n");
-              continue;
-           case '\f':
-              retval.append("\\f");
-              continue;
-           case '\r':
-              retval.append("\\r");
-              continue;
-           case '\"':
-              retval.append("\\\"");
-              continue;
-           case '\'':
-              retval.append("\\\'");
-              continue;
-           case '\\':
-              retval.append("\\\\");
-              continue;
-           default:
-              if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
-                 String s = "0000" + Integer.toString(ch, 16);
-                 retval.append("\\u" + s.substring(s.length() - 4, s.length()));
-              } else {
-                 retval.append(ch);
-              }
-              continue;
-        }
-      }
-      return retval.toString();
-   }
-
-   /**
-    * Returns a detailed message for the Error when it is thrown by the
-    * token manager to indicate a lexical error.
-    * Parameters : 
-    *    EOFSeen     : indicates if EOF caused the lexicl error
-    *    curLexState : lexical state in which this error occured
-    *    errorLine   : line number when the error occured
-    *    errorColumn : column number when the error occured
-    *    errorAfter  : prefix that was seen before this error occured
-    *    curchar     : the offending character
-    * Note: You can customize the lexical error message by modifying this method.
-    */
-   protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) {
-      return("Lexical error at line " +
-           errorLine + ", column " +
-           errorColumn + ".  Encountered: " +
-           (EOFSeen ? "<EOF> " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") +
-           "after : \"" + addEscapes(errorAfter) + "\"");
-   }
-
-   /**
-    * You can also modify the body of this method to customize your error messages.
-    * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not
-    * of end-users concern, so you can return something like : 
-    *
-    *     "Internal Error : Please file a bug report .... "
-    *
-    * from this method for such cases in the release version of your parser.
-    */
-   public String getMessage() {
-      return super.getMessage();
-   }
-
-   /*
-    * Constructors of various flavors follow.
-    */
-
-   public TokenMgrError() {
-   }
-
-   public TokenMgrError(String message, int reason) {
-      super(message);
-      errorCode = reason;
-   }
-
-   public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) {
-      this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason);
-   }
-}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/util/CharsetUtil.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/util/CharsetUtil.java
deleted file mode 100644
index 4e712fc..0000000
--- a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/util/CharsetUtil.java
+++ /dev/null
@@ -1,1249 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one   *
- * or more contributor license agreements.  See the NOTICE file *
- * distributed with this work for additional information        *
- * regarding copyright ownership.  The ASF licenses this file   *
- * to you under the Apache License, Version 2.0 (the            *
- * "License"); you may not use this file except in compliance   *
- * with the License.  You may obtain a copy of the License at   *
- *                                                              *
- *   http://www.apache.org/licenses/LICENSE-2.0                 *
- *                                                              *
- * Unless required by applicable law or agreed to in writing,   *
- * software distributed under the License is distributed on an  *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
- * KIND, either express or implied.  See the License for the    *
- * specific language governing permissions and limitations      *
- * under the License.                                           *
- ****************************************************************/
-
-package org.apache.james.mime4j.util;
-
-import java.io.UnsupportedEncodingException;
-import java.nio.charset.IllegalCharsetNameException;
-import java.nio.charset.UnsupportedCharsetException;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.TreeSet;
-
-//BEGIN android-changed: Stubbing out logging
-import org.apache.james.mime4j.Log;
-import org.apache.james.mime4j.LogFactory;
-//END android-changed
-
-/**
- * Utility class for working with character sets. It is somewhat similar to
- * the Java 1.4 <code>java.nio.charset.Charset</code> class but knows many
- * more aliases and is compatible with Java 1.3. It will use a simple detection
- * mechanism to detect what character sets the current VM supports. This will
- * be a sub-set of the character sets listed in the
- * <a href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">
- * Java 1.5 (J2SE5.0) Supported Encodings</a> document.
- * <p>
- * The <a href="http://www.iana.org/assignments/character-sets">
- * IANA Character Sets</a> document has been used to determine the preferred
- * MIME character set names and to get a list of known aliases.
- * <p>
- * This is a complete list of the character sets known to this class:
- * <table>
- *     <tr>
- *         <td>Canonical (Java) name</td>
- *         <td>MIME preferred</td>
- *         <td>Aliases</td>
- *     </tr>
- *     <tr>
- *         <td>ASCII</td>
- *         <td>US-ASCII</td>
- *         <td>ANSI_X3.4-1968 iso-ir-6 ANSI_X3.4-1986 ISO_646.irv:1991 ISO646-US us IBM367 cp367 csASCII ascii7 646 iso_646.irv:1983 </td>
- *     </tr>
- *     <tr>
- *         <td>Big5</td>
- *         <td>Big5</td>
- *         <td>csBig5 CN-Big5 BIG-FIVE BIGFIVE </td>
- *     </tr>
- *     <tr>
- *         <td>Big5_HKSCS</td>
- *         <td>Big5-HKSCS</td>
- *         <td>big5hkscs </td>
- *     </tr>
- *     <tr>
- *         <td>Big5_Solaris</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp037</td>
- *         <td>IBM037</td>
- *         <td>ebcdic-cp-us ebcdic-cp-ca ebcdic-cp-wt ebcdic-cp-nl csIBM037 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp1006</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1025</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1026</td>
- *         <td>IBM1026</td>
- *         <td>csIBM1026 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp1046</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1047</td>
- *         <td>IBM1047</td>
- *         <td>IBM-1047 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp1097</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1098</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1112</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1122</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1123</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1124</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1140</td>
- *         <td>IBM01140</td>
- *         <td>CCSID01140 CP01140 ebcdic-us-37+euro </td>
- *     </tr>
- *     <tr>
- *         <td>Cp1141</td>
- *         <td>IBM01141</td>
- *         <td>CCSID01141 CP01141 ebcdic-de-273+euro </td>
- *     </tr>
- *     <tr>
- *         <td>Cp1142</td>
- *         <td>IBM01142</td>
- *         <td>CCSID01142 CP01142 ebcdic-dk-277+euro ebcdic-no-277+euro </td>
- *     </tr>
- *     <tr>
- *         <td>Cp1143</td>
- *         <td>IBM01143</td>
- *         <td>CCSID01143 CP01143 ebcdic-fi-278+euro ebcdic-se-278+euro </td>
- *     </tr>
- *     <tr>
- *         <td>Cp1144</td>
- *         <td>IBM01144</td>
- *         <td>CCSID01144 CP01144 ebcdic-it-280+euro </td>
- *     </tr>
- *     <tr>
- *         <td>Cp1145</td>
- *         <td>IBM01145</td>
- *         <td>CCSID01145 CP01145 ebcdic-es-284+euro </td>
- *     </tr>
- *     <tr>
- *         <td>Cp1146</td>
- *         <td>IBM01146</td>
- *         <td>CCSID01146 CP01146 ebcdic-gb-285+euro </td>
- *     </tr>
- *     <tr>
- *         <td>Cp1147</td>
- *         <td>IBM01147</td>
- *         <td>CCSID01147 CP01147 ebcdic-fr-297+euro </td>
- *     </tr>
- *     <tr>
- *         <td>Cp1148</td>
- *         <td>IBM01148</td>
- *         <td>CCSID01148 CP01148 ebcdic-international-500+euro </td>
- *     </tr>
- *     <tr>
- *         <td>Cp1149</td>
- *         <td>IBM01149</td>
- *         <td>CCSID01149 CP01149 ebcdic-is-871+euro </td>
- *     </tr>
- *     <tr>
- *         <td>Cp1250</td>
- *         <td>windows-1250</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1251</td>
- *         <td>windows-1251</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1252</td>
- *         <td>windows-1252</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1253</td>
- *         <td>windows-1253</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1254</td>
- *         <td>windows-1254</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1255</td>
- *         <td>windows-1255</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1256</td>
- *         <td>windows-1256</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1257</td>
- *         <td>windows-1257</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1258</td>
- *         <td>windows-1258</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1381</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp1383</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp273</td>
- *         <td>IBM273</td>
- *         <td>csIBM273 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp277</td>
- *         <td>IBM277</td>
- *         <td>EBCDIC-CP-DK EBCDIC-CP-NO csIBM277 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp278</td>
- *         <td>IBM278</td>
- *         <td>CP278 ebcdic-cp-fi ebcdic-cp-se csIBM278 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp280</td>
- *         <td>IBM280</td>
- *         <td>ebcdic-cp-it csIBM280 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp284</td>
- *         <td>IBM284</td>
- *         <td>ebcdic-cp-es csIBM284 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp285</td>
- *         <td>IBM285</td>
- *         <td>ebcdic-cp-gb csIBM285 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp297</td>
- *         <td>IBM297</td>
- *         <td>ebcdic-cp-fr csIBM297 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp33722</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp420</td>
- *         <td>IBM420</td>
- *         <td>ebcdic-cp-ar1 csIBM420 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp424</td>
- *         <td>IBM424</td>
- *         <td>ebcdic-cp-he csIBM424 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp437</td>
- *         <td>IBM437</td>
- *         <td>437 csPC8CodePage437 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp500</td>
- *         <td>IBM500</td>
- *         <td>ebcdic-cp-be ebcdic-cp-ch csIBM500 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp737</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp775</td>
- *         <td>IBM775</td>
- *         <td>csPC775Baltic </td>
- *     </tr>
- *     <tr>
- *         <td>Cp838</td>
- *         <td>IBM-Thai</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp850</td>
- *         <td>IBM850</td>
- *         <td>850 csPC850Multilingual </td>
- *     </tr>
- *     <tr>
- *         <td>Cp852</td>
- *         <td>IBM852</td>
- *         <td>852 csPCp852 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp855</td>
- *         <td>IBM855</td>
- *         <td>855 csIBM855 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp856</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp857</td>
- *         <td>IBM857</td>
- *         <td>857 csIBM857 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp858</td>
- *         <td>IBM00858</td>
- *         <td>CCSID00858 CP00858 PC-Multilingual-850+euro </td>
- *     </tr>
- *     <tr>
- *         <td>Cp860</td>
- *         <td>IBM860</td>
- *         <td>860 csIBM860 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp861</td>
- *         <td>IBM861</td>
- *         <td>861 cp-is csIBM861 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp862</td>
- *         <td>IBM862</td>
- *         <td>862 csPC862LatinHebrew </td>
- *     </tr>
- *     <tr>
- *         <td>Cp863</td>
- *         <td>IBM863</td>
- *         <td>863 csIBM863 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp864</td>
- *         <td>IBM864</td>
- *         <td>cp864 csIBM864 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp865</td>
- *         <td>IBM865</td>
- *         <td>865 csIBM865 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp866</td>
- *         <td>IBM866</td>
- *         <td>866 csIBM866 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp868</td>
- *         <td>IBM868</td>
- *         <td>cp-ar csIBM868 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp869</td>
- *         <td>IBM869</td>
- *         <td>cp-gr csIBM869 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp870</td>
- *         <td>IBM870</td>
- *         <td>ebcdic-cp-roece ebcdic-cp-yu csIBM870 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp871</td>
- *         <td>IBM871</td>
- *         <td>ebcdic-cp-is csIBM871 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp875</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp918</td>
- *         <td>IBM918</td>
- *         <td>ebcdic-cp-ar2 csIBM918 </td>
- *     </tr>
- *     <tr>
- *         <td>Cp921</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp922</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp930</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp933</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp935</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp937</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp939</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp942</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp942C</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp943</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp943C</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp948</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp949</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp949C</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp950</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp964</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>Cp970</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>EUC_CN</td>
- *         <td>GB2312</td>
- *         <td>x-EUC-CN csGB2312 euccn euc-cn gb2312-80 gb2312-1980 CN-GB CN-GB-ISOIR165 </td>
- *     </tr>
- *     <tr>
- *         <td>EUC_JP</td>
- *         <td>EUC-JP</td>
- *         <td>csEUCPkdFmtJapanese Extended_UNIX_Code_Packed_Format_for_Japanese eucjis x-eucjp eucjp x-euc-jp </td>
- *     </tr>
- *     <tr>
- *         <td>EUC_JP_LINUX</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>EUC_JP_Solaris</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>EUC_KR</td>
- *         <td>EUC-KR</td>
- *         <td>csEUCKR ksc5601 5601 ksc5601_1987 ksc_5601 ksc5601-1987 ks_c_5601-1987 euckr </td>
- *     </tr>
- *     <tr>
- *         <td>EUC_TW</td>
- *         <td>EUC-TW</td>
- *         <td>x-EUC-TW cns11643 euctw </td>
- *     </tr>
- *     <tr>
- *         <td>GB18030</td>
- *         <td>GB18030</td>
- *         <td>gb18030-2000 </td>
- *     </tr>
- *     <tr>
- *         <td>GBK</td>
- *         <td>windows-936</td>
- *         <td>CP936 MS936 ms_936 x-mswin-936 </td>
- *     </tr>
- *     <tr>
- *         <td>ISCII91</td>
- *         <td>?</td>
- *         <td>x-ISCII91 iscii </td>
- *     </tr>
- *     <tr>
- *         <td>ISO2022CN</td>
- *         <td>ISO-2022-CN</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>ISO2022JP</td>
- *         <td>ISO-2022-JP</td>
- *         <td>csISO2022JP JIS jis_encoding csjisencoding </td>
- *     </tr>
- *     <tr>
- *         <td>ISO2022KR</td>
- *         <td>ISO-2022-KR</td>
- *         <td>csISO2022KR </td>
- *     </tr>
- *     <tr>
- *         <td>ISO2022_CN_CNS</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>ISO2022_CN_GB</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>ISO8859_1</td>
- *         <td>ISO-8859-1</td>
- *         <td>ISO_8859-1:1987 iso-ir-100 ISO_8859-1 latin1 l1 IBM819 CP819 csISOLatin1 8859_1 819 IBM-819 ISO8859-1 ISO_8859_1 </td>
- *     </tr>
- *     <tr>
- *         <td>ISO8859_13</td>
- *         <td>ISO-8859-13</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>ISO8859_15</td>
- *         <td>ISO-8859-15</td>
- *         <td>ISO_8859-15 Latin-9 8859_15 csISOlatin9 IBM923 cp923 923 L9 IBM-923 ISO8859-15 LATIN9 LATIN0 csISOlatin0 ISO8859_15_FDIS </td>
- *     </tr>
- *     <tr>
- *         <td>ISO8859_2</td>
- *         <td>ISO-8859-2</td>
- *         <td>ISO_8859-2:1987 iso-ir-101 ISO_8859-2 latin2 l2 csISOLatin2 8859_2 iso8859_2 </td>
- *     </tr>
- *     <tr>
- *         <td>ISO8859_3</td>
- *         <td>ISO-8859-3</td>
- *         <td>ISO_8859-3:1988 iso-ir-109 ISO_8859-3 latin3 l3 csISOLatin3 8859_3 </td>
- *     </tr>
- *     <tr>
- *         <td>ISO8859_4</td>
- *         <td>ISO-8859-4</td>
- *         <td>ISO_8859-4:1988 iso-ir-110 ISO_8859-4 latin4 l4 csISOLatin4 8859_4 </td>
- *     </tr>
- *     <tr>
- *         <td>ISO8859_5</td>
- *         <td>ISO-8859-5</td>
- *         <td>ISO_8859-5:1988 iso-ir-144 ISO_8859-5 cyrillic csISOLatinCyrillic 8859_5 </td>
- *     </tr>
- *     <tr>
- *         <td>ISO8859_6</td>
- *         <td>ISO-8859-6</td>
- *         <td>ISO_8859-6:1987 iso-ir-127 ISO_8859-6 ECMA-114 ASMO-708 arabic csISOLatinArabic 8859_6 </td>
- *     </tr>
- *     <tr>
- *         <td>ISO8859_7</td>
- *         <td>ISO-8859-7</td>
- *         <td>ISO_8859-7:1987 iso-ir-126 ISO_8859-7 ELOT_928 ECMA-118 greek greek8 csISOLatinGreek 8859_7 sun_eu_greek </td>
- *     </tr>
- *     <tr>
- *         <td>ISO8859_8</td>
- *         <td>ISO-8859-8</td>
- *         <td>ISO_8859-8:1988 iso-ir-138 ISO_8859-8 hebrew csISOLatinHebrew 8859_8 </td>
- *     </tr>
- *     <tr>
- *         <td>ISO8859_9</td>
- *         <td>ISO-8859-9</td>
- *         <td>ISO_8859-9:1989 iso-ir-148 ISO_8859-9 latin5 l5 csISOLatin5 8859_9 </td>
- *     </tr>
- *     <tr>
- *         <td>JISAutoDetect</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>JIS_C6626-1983</td>
- *         <td>JIS_C6626-1983</td>
- *         <td>x-JIS0208 JIS0208 csISO87JISX0208 x0208 JIS_X0208-1983 iso-ir-87 </td>
- *     </tr>
- *     <tr>
- *         <td>JIS_X0201</td>
- *         <td>JIS_X0201</td>
- *         <td>X0201 JIS0201 csHalfWidthKatakana </td>
- *     </tr>
- *     <tr>
- *         <td>JIS_X0212-1990</td>
- *         <td>JIS_X0212-1990</td>
- *         <td>iso-ir-159 x0212 JIS0212 csISO159JISX02121990 </td>
- *     </tr>
- *     <tr>
- *         <td>KOI8_R</td>
- *         <td>KOI8-R</td>
- *         <td>csKOI8R koi8 </td>
- *     </tr>
- *     <tr>
- *         <td>MS874</td>
- *         <td>windows-874</td>
- *         <td>cp874 </td>
- *     </tr>
- *     <tr>
- *         <td>MS932</td>
- *         <td>Windows-31J</td>
- *         <td>windows-932 csWindows31J x-ms-cp932 </td>
- *     </tr>
- *     <tr>
- *         <td>MS949</td>
- *         <td>windows-949</td>
- *         <td>windows949 ms_949 x-windows-949 </td>
- *     </tr>
- *     <tr>
- *         <td>MS950</td>
- *         <td>windows-950</td>
- *         <td>x-windows-950 </td>
- *     </tr>
- *     <tr>
- *         <td>MS950_HKSCS</td>
- *         <td></td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>MacArabic</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>MacCentralEurope</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>MacCroatian</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>MacCyrillic</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>MacDingbat</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>MacGreek</td>
- *         <td>MacGreek</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>MacHebrew</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>MacIceland</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>MacRoman</td>
- *         <td>MacRoman</td>
- *         <td>Macintosh MAC csMacintosh </td>
- *     </tr>
- *     <tr>
- *         <td>MacRomania</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>MacSymbol</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>MacThai</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>MacTurkish</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>MacUkraine</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>SJIS</td>
- *         <td>Shift_JIS</td>
- *         <td>MS_Kanji csShiftJIS shift-jis x-sjis pck </td>
- *     </tr>
- *     <tr>
- *         <td>TIS620</td>
- *         <td>TIS-620</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>UTF-16</td>
- *         <td>UTF-16</td>
- *         <td>UTF_16 </td>
- *     </tr>
- *     <tr>
- *         <td>UTF8</td>
- *         <td>UTF-8</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>UnicodeBig</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>UnicodeBigUnmarked</td>
- *         <td>UTF-16BE</td>
- *         <td>X-UTF-16BE UTF_16BE ISO-10646-UCS-2 </td>
- *     </tr>
- *     <tr>
- *         <td>UnicodeLittle</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- *     <tr>
- *         <td>UnicodeLittleUnmarked</td>
- *         <td>UTF-16LE</td>
- *         <td>UTF_16LE X-UTF-16LE </td>
- *     </tr>
- *     <tr>
- *         <td>x-Johab</td>
- *         <td>johab</td>
- *         <td>johab cp1361 ms1361 ksc5601-1992 ksc5601_1992 </td>
- *     </tr>
- *     <tr>
- *         <td>x-iso-8859-11</td>
- *         <td>?</td>
- *         <td></td>
- *     </tr>
- * </table>
- *
- *
- * @version $Id: CharsetUtil.java,v 1.1 2004/10/25 07:26:46 ntherning Exp $
- */
-public class CharsetUtil {
-    private static Log log = LogFactory.getLog(CharsetUtil.class);
-
-    private static class Charset implements Comparable<Charset> {
-        private String canonical = null;
-        private String mime = null;
-        private String[] aliases = null;
-
-        private Charset(String canonical, String mime, String[] aliases) {
-            this.canonical = canonical;
-            this.mime = mime;
-            this.aliases = aliases;
-        }
-
-        public int compareTo(Charset c) {
-            return this.canonical.compareTo(c.canonical);
-        }
-    }
-
-    private static Charset[] JAVA_CHARSETS = {
-        new Charset("ISO8859_1", "ISO-8859-1",
-                    new String[] {"ISO_8859-1:1987", "iso-ir-100", "ISO_8859-1",
-                                  "latin1", "l1", "IBM819", "CP819",
-                                  "csISOLatin1", "8859_1", "819", "IBM-819",
-                                  "ISO8859-1", "ISO_8859_1"}),
-        new Charset("ISO8859_2", "ISO-8859-2",
-                    new String[] {"ISO_8859-2:1987", "iso-ir-101", "ISO_8859-2",
-                                  "latin2", "l2", "csISOLatin2", "8859_2",
-                                  "iso8859_2"}),
-        new Charset("ISO8859_3", "ISO-8859-3", new String[] {"ISO_8859-3:1988", "iso-ir-109", "ISO_8859-3", "latin3", "l3", "csISOLatin3", "8859_3"}),
-        new Charset("ISO8859_4", "ISO-8859-4",
-                    new String[] {"ISO_8859-4:1988", "iso-ir-110", "ISO_8859-4",
-                                  "latin4", "l4", "csISOLatin4", "8859_4"}),
-        new Charset("ISO8859_5", "ISO-8859-5",
-                    new String[] {"ISO_8859-5:1988", "iso-ir-144", "ISO_8859-5",
-                                  "cyrillic", "csISOLatinCyrillic", "8859_5"}),
-        new Charset("ISO8859_6", "ISO-8859-6", new String[] {"ISO_8859-6:1987", "iso-ir-127", "ISO_8859-6", "ECMA-114", "ASMO-708", "arabic", "csISOLatinArabic", "8859_6"}),
-        new Charset("ISO8859_7", "ISO-8859-7",
-                    new String[] {"ISO_8859-7:1987", "iso-ir-126", "ISO_8859-7",
-                                  "ELOT_928", "ECMA-118", "greek", "greek8",
-                                  "csISOLatinGreek", "8859_7", "sun_eu_greek"}),
-        new Charset("ISO8859_8", "ISO-8859-8", new String[] {"ISO_8859-8:1988", "iso-ir-138", "ISO_8859-8", "hebrew", "csISOLatinHebrew", "8859_8"}),
-        new Charset("ISO8859_9", "ISO-8859-9",
-                    new String[] {"ISO_8859-9:1989", "iso-ir-148", "ISO_8859-9",
-                                  "latin5", "l5", "csISOLatin5", "8859_9"}),
-
-        new Charset("ISO8859_13", "ISO-8859-13", new String[] {}),
-        new Charset("ISO8859_15", "ISO-8859-15",
-                    new String[] {"ISO_8859-15", "Latin-9", "8859_15",
-                                  "csISOlatin9", "IBM923", "cp923", "923", "L9",
-                                  "IBM-923", "ISO8859-15", "LATIN9", "LATIN0",
-                                  "csISOlatin0", "ISO8859_15_FDIS"}),
-        new Charset("KOI8_R", "KOI8-R", new String[] {"csKOI8R", "koi8"}),
-        new Charset("ASCII", "US-ASCII",
-                    new String[] {"ANSI_X3.4-1968", "iso-ir-6",
-                                  "ANSI_X3.4-1986", "ISO_646.irv:1991",
-                                  "ISO646-US", "us", "IBM367", "cp367",
-                                  "csASCII", "ascii7", "646", "iso_646.irv:1983"}),
-        new Charset("UTF8", "UTF-8", new String[] {}),
-        new Charset("UTF-16", "UTF-16", new String[] {"UTF_16"}),
-        new Charset("UnicodeBigUnmarked", "UTF-16BE", new String[] {"X-UTF-16BE", "UTF_16BE", "ISO-10646-UCS-2"}),
-        new Charset("UnicodeLittleUnmarked", "UTF-16LE", new String[] {"UTF_16LE", "X-UTF-16LE"}),
-        new Charset("Big5", "Big5", new String[] {"csBig5", "CN-Big5", "BIG-FIVE", "BIGFIVE"}),
-        new Charset("Big5_HKSCS", "Big5-HKSCS", new String[] {"big5hkscs"}),
-        new Charset("EUC_JP", "EUC-JP",
-                    new String[] {"csEUCPkdFmtJapanese",
-                              "Extended_UNIX_Code_Packed_Format_for_Japanese",
-                              "eucjis", "x-eucjp", "eucjp", "x-euc-jp"}),
-        new Charset("EUC_KR", "EUC-KR",
-                    new String[] {"csEUCKR", "ksc5601", "5601", "ksc5601_1987",
-                                  "ksc_5601", "ksc5601-1987", "ks_c_5601-1987",
-                                  "euckr"}),
-        new Charset("GB18030", "GB18030", new String[] {"gb18030-2000"}),
-        new Charset("EUC_CN", "GB2312", new String[] {"x-EUC-CN", "csGB2312", "euccn", "euc-cn", "gb2312-80", "gb2312-1980", "CN-GB", "CN-GB-ISOIR165"}),
-        new Charset("GBK", "windows-936", new String[] {"CP936", "MS936", "ms_936", "x-mswin-936"}),
-
-        new Charset("Cp037", "IBM037", new String[] {"ebcdic-cp-us", "ebcdic-cp-ca", "ebcdic-cp-wt", "ebcdic-cp-nl", "csIBM037"}),
-        new Charset("Cp273", "IBM273", new String[] {"csIBM273"}),
-        new Charset("Cp277", "IBM277", new String[] {"EBCDIC-CP-DK", "EBCDIC-CP-NO", "csIBM277"}),
-        new Charset("Cp278", "IBM278", new String[] {"CP278", "ebcdic-cp-fi", "ebcdic-cp-se", "csIBM278"}),
-        new Charset("Cp280", "IBM280", new String[] {"ebcdic-cp-it", "csIBM280"}),
-        new Charset("Cp284", "IBM284", new String[] {"ebcdic-cp-es", "csIBM284"}),
-        new Charset("Cp285", "IBM285", new String[] {"ebcdic-cp-gb", "csIBM285"}),
-        new Charset("Cp297", "IBM297", new String[] {"ebcdic-cp-fr", "csIBM297"}),
-        new Charset("Cp420", "IBM420", new String[] {"ebcdic-cp-ar1", "csIBM420"}),
-        new Charset("Cp424", "IBM424", new String[] {"ebcdic-cp-he", "csIBM424"}),
-        new Charset("Cp437", "IBM437", new String[] {"437", "csPC8CodePage437"}),
-        new Charset("Cp500", "IBM500", new String[] {"ebcdic-cp-be", "ebcdic-cp-ch", "csIBM500"}),
-        new Charset("Cp775", "IBM775", new String[] {"csPC775Baltic"}),
-        new Charset("Cp838", "IBM-Thai", new String[] {}),
-        new Charset("Cp850", "IBM850", new String[] {"850", "csPC850Multilingual"}),
-        new Charset("Cp852", "IBM852", new String[] {"852", "csPCp852"}),
-        new Charset("Cp855", "IBM855", new String[] {"855", "csIBM855"}),
-        new Charset("Cp857", "IBM857", new String[] {"857", "csIBM857"}),
-        new Charset("Cp858", "IBM00858",
-                new String[] {"CCSID00858", "CP00858",
-                              "PC-Multilingual-850+euro"}),
-        new Charset("Cp860", "IBM860", new String[] {"860", "csIBM860"}),
-        new Charset("Cp861", "IBM861", new String[] {"861", "cp-is", "csIBM861"}),
-        new Charset("Cp862", "IBM862", new String[] {"862", "csPC862LatinHebrew"}),
-        new Charset("Cp863", "IBM863", new String[] {"863", "csIBM863"}),
-        new Charset("Cp864", "IBM864", new String[] {"cp864", "csIBM864"}),
-        new Charset("Cp865", "IBM865", new String[] {"865", "csIBM865"}),
-        new Charset("Cp866", "IBM866", new String[] {"866", "csIBM866"}),
-        new Charset("Cp868", "IBM868", new String[] {"cp-ar", "csIBM868"}),
-        new Charset("Cp869", "IBM869", new String[] {"cp-gr", "csIBM869"}),
-        new Charset("Cp870", "IBM870", new String[] {"ebcdic-cp-roece", "ebcdic-cp-yu", "csIBM870"}),
-        new Charset("Cp871", "IBM871", new String[] {"ebcdic-cp-is", "csIBM871"}),
-        new Charset("Cp918", "IBM918", new String[] {"ebcdic-cp-ar2", "csIBM918"}),
-        new Charset("Cp1026", "IBM1026", new String[] {"csIBM1026"}),
-        new Charset("Cp1047", "IBM1047", new String[] {"IBM-1047"}),
-        new Charset("Cp1140", "IBM01140",
-                    new String[] {"CCSID01140", "CP01140",
-                                  "ebcdic-us-37+euro"}),
-        new Charset("Cp1141", "IBM01141",
-                    new String[] {"CCSID01141", "CP01141",
-                                  "ebcdic-de-273+euro"}),
-        new Charset("Cp1142", "IBM01142", new String[] {"CCSID01142", "CP01142", "ebcdic-dk-277+euro", "ebcdic-no-277+euro"}),
-        new Charset("Cp1143", "IBM01143", new String[] {"CCSID01143", "CP01143", "ebcdic-fi-278+euro", "ebcdic-se-278+euro"}),
-        new Charset("Cp1144", "IBM01144", new String[] {"CCSID01144", "CP01144", "ebcdic-it-280+euro"}),
-        new Charset("Cp1145", "IBM01145", new String[] {"CCSID01145", "CP01145", "ebcdic-es-284+euro"}),
-        new Charset("Cp1146", "IBM01146", new String[] {"CCSID01146", "CP01146", "ebcdic-gb-285+euro"}),
-        new Charset("Cp1147", "IBM01147", new String[] {"CCSID01147", "CP01147", "ebcdic-fr-297+euro"}),
-        new Charset("Cp1148", "IBM01148", new String[] {"CCSID01148", "CP01148", "ebcdic-international-500+euro"}),
-        new Charset("Cp1149", "IBM01149", new String[] {"CCSID01149", "CP01149", "ebcdic-is-871+euro"}),
-        new Charset("Cp1250", "windows-1250", new String[] {}),
-        new Charset("Cp1251", "windows-1251", new String[] {}),
-        new Charset("Cp1252", "windows-1252", new String[] {}),
-        new Charset("Cp1253", "windows-1253", new String[] {}),
-        new Charset("Cp1254", "windows-1254", new String[] {}),
-        new Charset("Cp1255", "windows-1255", new String[] {}),
-        new Charset("Cp1256", "windows-1256", new String[] {}),
-        new Charset("Cp1257", "windows-1257", new String[] {}),
-        new Charset("Cp1258", "windows-1258", new String[] {}),
-        new Charset("ISO2022CN", "ISO-2022-CN", new String[] {}),
-        new Charset("ISO2022JP", "ISO-2022-JP", new String[] {"csISO2022JP", "JIS", "jis_encoding", "csjisencoding"}),
-        new Charset("ISO2022KR", "ISO-2022-KR", new String[] {"csISO2022KR"}),
-        new Charset("JIS_X0201", "JIS_X0201", new String[] {"X0201", "JIS0201", "csHalfWidthKatakana"}),
-        new Charset("JIS_X0212-1990", "JIS_X0212-1990", new String[] {"iso-ir-159", "x0212", "JIS0212", "csISO159JISX02121990"}),
-        new Charset("JIS_C6626-1983", "JIS_C6626-1983", new String[] {"x-JIS0208", "JIS0208", "csISO87JISX0208", "x0208", "JIS_X0208-1983", "iso-ir-87"}),
-        new Charset("SJIS", "Shift_JIS", new String[] {"MS_Kanji", "csShiftJIS", "shift-jis", "x-sjis", "pck"}),
-        new Charset("TIS620", "TIS-620", new String[] {}),
-        new Charset("MS932", "Windows-31J", new String[] {"windows-932", "csWindows31J", "x-ms-cp932"}),
-        new Charset("EUC_TW", "EUC-TW", new String[] {"x-EUC-TW", "cns11643", "euctw"}),
-        new Charset("x-Johab", "johab", new String[] {"johab", "cp1361", "ms1361", "ksc5601-1992", "ksc5601_1992"}),
-        new Charset("MS950_HKSCS", "", new String[] {}),
-        new Charset("MS874", "windows-874", new String[] {"cp874"}),
-        new Charset("MS949", "windows-949", new String[] {"windows949", "ms_949", "x-windows-949"}),
-        new Charset("MS950", "windows-950", new String[] {"x-windows-950"}),
-
-        new Charset("Cp737", null, new String[] {}),
-        new Charset("Cp856", null, new String[] {}),
-        new Charset("Cp875", null, new String[] {}),
-        new Charset("Cp921", null, new String[] {}),
-        new Charset("Cp922", null, new String[] {}),
-        new Charset("Cp930", null, new String[] {}),
-        new Charset("Cp933", null, new String[] {}),
-        new Charset("Cp935", null, new String[] {}),
-        new Charset("Cp937", null, new String[] {}),
-        new Charset("Cp939", null, new String[] {}),
-        new Charset("Cp942", null, new String[] {}),
-        new Charset("Cp942C", null, new String[] {}),
-        new Charset("Cp943", null, new String[] {}),
-        new Charset("Cp943C", null, new String[] {}),
-        new Charset("Cp948", null, new String[] {}),
-        new Charset("Cp949", null, new String[] {}),
-        new Charset("Cp949C", null, new String[] {}),
-        new Charset("Cp950", null, new String[] {}),
-        new Charset("Cp964", null, new String[] {}),
-        new Charset("Cp970", null, new String[] {}),
-        new Charset("Cp1006", null, new String[] {}),
-        new Charset("Cp1025", null, new String[] {}),
-        new Charset("Cp1046", null, new String[] {}),
-        new Charset("Cp1097", null, new String[] {}),
-        new Charset("Cp1098", null, new String[] {}),
-        new Charset("Cp1112", null, new String[] {}),
-        new Charset("Cp1122", null, new String[] {}),
-        new Charset("Cp1123", null, new String[] {}),
-        new Charset("Cp1124", null, new String[] {}),
-        new Charset("Cp1381", null, new String[] {}),
-        new Charset("Cp1383", null, new String[] {}),
-        new Charset("Cp33722", null, new String[] {}),
-        new Charset("Big5_Solaris", null, new String[] {}),
-        new Charset("EUC_JP_LINUX", null, new String[] {}),
-        new Charset("EUC_JP_Solaris", null, new String[] {}),
-        new Charset("ISCII91", null, new String[] {"x-ISCII91", "iscii"}),
-        new Charset("ISO2022_CN_CNS", null, new String[] {}),
-        new Charset("ISO2022_CN_GB", null, new String[] {}),
-        new Charset("x-iso-8859-11", null, new String[] {}),
-        new Charset("JISAutoDetect", null, new String[] {}),
-        new Charset("MacArabic", null, new String[] {}),
-        new Charset("MacCentralEurope", null, new String[] {}),
-        new Charset("MacCroatian", null, new String[] {}),
-        new Charset("MacCyrillic", null, new String[] {}),
-        new Charset("MacDingbat", null, new String[] {}),
-        new Charset("MacGreek", "MacGreek", new String[] {}),
-        new Charset("MacHebrew", null, new String[] {}),
-        new Charset("MacIceland", null, new String[] {}),
-        new Charset("MacRoman", "MacRoman", new String[] {"Macintosh", "MAC", "csMacintosh"}),
-        new Charset("MacRomania", null, new String[] {}),
-        new Charset("MacSymbol", null, new String[] {}),
-        new Charset("MacThai", null, new String[] {}),
-        new Charset("MacTurkish", null, new String[] {}),
-        new Charset("MacUkraine", null, new String[] {}),
-        new Charset("UnicodeBig", null, new String[] {}),
-        new Charset("UnicodeLittle", null, new String[] {})
-    };
-
-    /**
-     * Contains the canonical names of character sets which can be used to
-     * decode bytes into Java chars.
-     */
-    private static TreeSet<String> decodingSupported = null;
-
-    /**
-     * Contains the canonical names of character sets which can be used to
-     * encode Java chars into bytes.
-     */
-    private static TreeSet<String> encodingSupported = null;
-
-    /**
-     * Maps character set names to Charset objects. All possible names of
-     * a charset will be mapped to the Charset.
-     */
-    private static HashMap<String, Charset> charsetMap = null;
-
-    static {
-        decodingSupported = new TreeSet<String>();
-        encodingSupported = new TreeSet<String>();
-        byte[] dummy = new byte[] {'d', 'u', 'm', 'm', 'y'};
-        for (int i = 0; i < JAVA_CHARSETS.length; i++) {
-            try {
-                String s = new String(dummy, JAVA_CHARSETS[i].canonical);
-                decodingSupported.add(JAVA_CHARSETS[i].canonical.toLowerCase(Locale.US));
-            } catch (UnsupportedOperationException e) {
-            } catch (UnsupportedEncodingException e) {
-            }
-            try {
-                "dummy".getBytes(JAVA_CHARSETS[i].canonical);
-                encodingSupported.add(JAVA_CHARSETS[i].canonical.toLowerCase(Locale.US));
-            } catch (UnsupportedOperationException e) {
-            } catch (UnsupportedEncodingException e) {
-            }
-        }
-
-        charsetMap = new HashMap<String, Charset>();
-        for (int i = 0; i < JAVA_CHARSETS.length; i++) {
-            Charset c = JAVA_CHARSETS[i];
-            charsetMap.put(c.canonical.toLowerCase(Locale.US), c);
-            if (c.mime != null) {
-                charsetMap.put(c.mime.toLowerCase(Locale.US), c);
-            }
-            if (c.aliases != null) {
-                for (int j = 0; j < c.aliases.length; j++) {
-                    charsetMap.put(c.aliases[j].toLowerCase(Locale.US), c);
-                }
-            }
-        }
-
-        if (log.isDebugEnabled()) {
-            log.debug("Character sets which support decoding: "
-                        + decodingSupported);
-            log.debug("Character sets which support encoding: "
-                        + encodingSupported);
-        }
-    }
-
-    /**
-     * ANDROID:  THE FOLLOWING SET OF STATIC STRINGS ARE COPIED FROM A NEWER VERSION OF MIME4J
-     */
-
-    /** carriage return - line feed sequence */
-    public static final String CRLF = "\r\n";
-
-    /** US-ASCII CR, carriage return (13) */
-    public static final int CR = '\r';
-
-    /** US-ASCII LF, line feed (10) */
-    public static final int LF = '\n';
-
-    /** US-ASCII SP, space (32) */
-    public static final int SP = ' ';
-
-    /** US-ASCII HT, horizontal-tab (9)*/
-    public static final int HT = '\t';
-
-    public static final java.nio.charset.Charset US_ASCII = java.nio.charset.Charset
-            .forName("US-ASCII");
-
-    public static final java.nio.charset.Charset ISO_8859_1 = java.nio.charset.Charset
-            .forName("ISO-8859-1");
-
-    public static final java.nio.charset.Charset UTF_8 = java.nio.charset.Charset
-            .forName("UTF-8");
-
-    /**
-     * Returns <code>true</code> if the specified character is a whitespace
-     * character (CR, LF, SP or HT).
-     *
-     * ANDROID:  COPIED FROM A NEWER VERSION OF MIME4J
-     *
-     * @param ch
-     *            character to test.
-     * @return <code>true</code> if the specified character is a whitespace
-     *         character, <code>false</code> otherwise.
-     */
-    public static boolean isWhitespace(char ch) {
-        return ch == SP || ch == HT || ch == CR || ch == LF;
-    }
-
-    /**
-     * Returns <code>true</code> if the specified string consists entirely of
-     * whitespace characters.
-     *
-     * ANDROID:  COPIED FROM A NEWER VERSION OF MIME4J
-     *
-     * @param s
-     *            string to test.
-     * @return <code>true</code> if the specified string consists entirely of
-     *         whitespace characters, <code>false</code> otherwise.
-     */
-    public static boolean isWhitespace(final String s) {
-        if (s == null) {
-            throw new IllegalArgumentException("String may not be null");
-        }
-        final int len = s.length();
-        for (int i = 0; i < len; i++) {
-            if (!isWhitespace(s.charAt(i))) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Determines if the VM supports encoding (chars to bytes) the
-     * specified character set. NOTE: the given character set name may
-     * not be known to the VM even if this method returns <code>true</code>.
-     * Use {@link #toJavaCharset(String)} to get the canonical Java character
-     * set name.
-     *
-     * @param charsetName the characters set name.
-     * @return <code>true</code> if encoding is supported, <code>false</code>
-     *         otherwise.
-     */
-    public static boolean isEncodingSupported(String charsetName) {
-        return encodingSupported.contains(charsetName.toLowerCase(Locale.US));
-    }
-
-    /**
-     * Determines if the VM supports decoding (bytes to chars) the
-     * specified character set. NOTE: the given character set name may
-     * not be known to the VM even if this method returns <code>true</code>.
-     * Use {@link #toJavaCharset(String)} to get the canonical Java character
-     * set name.
-     *
-     * @param charsetName the characters set name.
-     * @return <code>true</code> if decoding is supported, <code>false</code>
-     *         otherwise.
-     */
-    public static boolean isDecodingSupported(String charsetName) {
-        return decodingSupported.contains(charsetName.toLowerCase(Locale.US));
-    }
-
-    /**
-     * Gets the preferred MIME character set name for the specified
-     * character set or <code>null</code> if not known.
-     *
-     * @param charsetName the character set name to look for.
-     * @return the MIME preferred name or <code>null</code> if not known.
-     */
-    public static String toMimeCharset(String charsetName) {
-        Charset c = charsetMap.get(charsetName.toLowerCase(Locale.US));
-        if (c != null) {
-            return c.mime;
-        }
-        return null;
-    }
-
-    /**
-     * Gets the canonical Java character set name for the specified
-     * character set or <code>null</code> if not known. This should be
-     * called before doing any conversions using the Java API. NOTE:
-     * you must use {@link #isEncodingSupported(String)} or
-     * {@link #isDecodingSupported(String)} to make sure the returned
-     * Java character set is supported by the current VM.
-     *
-     * @param charsetName the character set name to look for.
-     * @return the canonical Java name or <code>null</code> if not known.
-     */
-    public static String toJavaCharset(String charsetName) {
-        Charset c = charsetMap.get(charsetName.toLowerCase(Locale.US));
-        if (c != null) {
-            return c.canonical;
-        }
-        return null;
-    }
-
-    public static java.nio.charset.Charset getCharset(String charsetName) {
-        String defaultCharset = "ISO-8859-1";
-
-        // Use the default chareset if given charset is null
-        if(charsetName == null) charsetName = defaultCharset;
-
-        try {
-            return java.nio.charset.Charset.forName(charsetName);
-        } catch (IllegalCharsetNameException e) {
-            log.info("Illegal charset " + charsetName + ", fallback to " +
-                    defaultCharset + ": " + e);
-            // Use default charset on exception
-            return java.nio.charset.Charset.forName(defaultCharset);
-        } catch (UnsupportedCharsetException ex) {
-            log.info("Unsupported charset " + charsetName + ", fallback to " +
-                    defaultCharset + ": " + ex);
-            // Use default charset on exception
-            return java.nio.charset.Charset.forName(defaultCharset);
-        }
-
-    }
-    /*
-     * Uncomment the code below and run the main method to regenerate the
-     * Javadoc table above when the known charsets change.
-     */
-
-    /*
-    private static String dumpHtmlTable() {
-        LinkedList l = new LinkedList(Arrays.asList(JAVA_CHARSETS));
-        Collections.sort(l);
-        StringBuffer sb = new StringBuffer();
-        sb.append(" * <table>\n");
-        sb.append(" *     <tr>\n");
-        sb.append(" *         <td>Canonical (Java) name</td>\n");
-        sb.append(" *         <td>MIME preferred</td>\n");
-        sb.append(" *         <td>Aliases</td>\n");
-        sb.append(" *     </tr>\n");
-
-        for (Iterator it = l.iterator(); it.hasNext();) {
-            Charset c = (Charset) it.next();
-            sb.append(" *     <tr>\n");
-            sb.append(" *         <td>" + c.canonical + "</td>\n");
-            sb.append(" *         <td>" + (c.mime == null ? "?" : c.mime)+ "</td>\n");
-            sb.append(" *         <td>");
-            for (int i = 0; c.aliases != null && i < c.aliases.length; i++) {
-                sb.append(c.aliases[i] + " ");
-            }
-            sb.append("</td>\n");
-            sb.append(" *     </tr>\n");
-        }
-        sb.append(" * </table>\n");
-        return sb.toString();
-    }
-
-    public static void main(String[] args) {
-        System.out.println(dumpHtmlTable());
-    }*/
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java b/java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java
deleted file mode 100644
index ad3c025..0000000
--- a/java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sync;
-
-import android.content.Context;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.PhoneStateListener;
-import android.telephony.TelephonyManager;
-
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmPhoneStateListener;
-
-import java.util.Collections;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * A singleton class designed to remember the active OMTP visual voicemail sources. Because a
- * voicemail source is tied 1:1 to a phone account, the phone account handle is used as the key
- * for each voicemail source and the associated data.
- */
-public class OmtpVvmSourceManager {
-    public static final String TAG = "OmtpVvmSourceManager";
-
-    private static OmtpVvmSourceManager sInstance = new OmtpVvmSourceManager();
-
-    private Context mContext;
-    private TelephonyManager mTelephonyManager;
-    // Each phone account is associated with a phone state listener for updates to whether the
-    // device is able to sync.
-    private Set<PhoneAccountHandle> mActiveVvmSources;
-    private Map<PhoneAccountHandle, PhoneStateListener> mPhoneStateListenerMap;
-
-    /**
-     * Private constructor. Instance should only be acquired through getInstance().
-     */
-    private OmtpVvmSourceManager() {}
-
-    public static OmtpVvmSourceManager getInstance(Context context) {
-        sInstance.setup(context);
-        return sInstance;
-    }
-
-    /**
-     * Set the context and system services so they do not need to be retrieved every time.
-     * @param context The context to get the subscription and telephony manager for.
-     */
-    private void setup(Context context) {
-        if (mContext == null) {
-            mContext = context;
-            mTelephonyManager = (TelephonyManager)
-                    mContext.getSystemService(Context.TELEPHONY_SERVICE);
-            mActiveVvmSources = Collections.newSetFromMap(
-                    new ConcurrentHashMap<PhoneAccountHandle, Boolean>(8, 0.9f, 1));
-            mPhoneStateListenerMap =
-                    new ConcurrentHashMap<PhoneAccountHandle, PhoneStateListener>(8, 0.9f, 1);
-        }
-    }
-
-    public void addSource(PhoneAccountHandle phoneAccount) {
-        mActiveVvmSources.add(phoneAccount);
-    }
-
-    public void removeSource(PhoneAccountHandle phoneAccount) {
-        // TODO: should use OmtpVvmCarrierConfigHelper to handle the event. But currently it
-        // couldn't handle events on removed SIMs
-        VoicemailStatus.disable(mContext, phoneAccount);
-        removePhoneStateListener(phoneAccount);
-        mActiveVvmSources.remove(phoneAccount);
-    }
-
-    public void addPhoneStateListener(PhoneAccountHandle phoneAccount) {
-        if (!mPhoneStateListenerMap.containsKey(phoneAccount)) {
-            VvmPhoneStateListener phoneStateListener = new VvmPhoneStateListener(mContext,
-                    phoneAccount);
-            mPhoneStateListenerMap.put(phoneAccount, phoneStateListener);
-            mTelephonyManager.createForPhoneAccountHandle(phoneAccount)
-                    .listen(phoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE);
-        }
-    }
-
-    public void removePhoneStateListener(PhoneAccountHandle phoneAccount) {
-        PhoneStateListener phoneStateListener =
-                mPhoneStateListenerMap.remove(phoneAccount);
-        mTelephonyManager.createForPhoneAccountHandle(phoneAccount).listen(phoneStateListener, 0);
-    }
-
-    public Set<PhoneAccountHandle> getOmtpVvmSources() {
-        return mActiveVvmSources;
-    }
-
-    /**
-     * Check if a certain account is registered.
-     *
-     * @param phoneAccount The account to look for.
-     * @return {@code true} if the account is in the list of registered OMTP voicemail sources.
-     * {@code false} otherwise.
-     */
-    public boolean isVvmSourceRegistered(PhoneAccountHandle phoneAccount) {
-        if (phoneAccount == null) {
-            return false;
-        }
-
-        return mActiveVvmSources.contains(phoneAccount);
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java b/java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java
deleted file mode 100644
index 971a1c5..0000000
--- a/java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.sync;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.provider.VoicemailContract;
-import android.telecom.PhoneAccountHandle;
-import android.telecom.TelecomManager;
-
-import com.android.voicemailomtp.ActivationTask;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil;
-
-import java.util.List;
-
-public class OmtpVvmSyncReceiver extends BroadcastReceiver {
-
-    private static final String TAG = "OmtpVvmSyncReceiver";
-
-    @Override
-    public void onReceive(final Context context, Intent intent) {
-        if (VoicemailContract.ACTION_SYNC_VOICEMAIL.equals(intent.getAction())) {
-            VvmLog.v(TAG, "Sync intent received");
-            for (PhoneAccountHandle source : OmtpVvmSourceManager.getInstance(context)
-                    .getOmtpVvmSources()) {
-                SyncTask.start(context, source, OmtpVvmSyncService.SYNC_FULL_SYNC);
-            }
-            activateUnactivatedAccounts(context);
-        }
-    }
-
-    private static void activateUnactivatedAccounts(Context context) {
-        List<PhoneAccountHandle> accounts =
-                context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts();
-        for (PhoneAccountHandle phoneAccount : accounts) {
-            if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccount)) {
-                continue;
-            }
-            if (!OmtpVvmSourceManager.getInstance(context).isVvmSourceRegistered(phoneAccount)) {
-                VvmLog.i(TAG, "Unactivated account " + phoneAccount + " found, activating");
-                ActivationTask.start(context, phoneAccount, null);
-            }
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java b/java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java
deleted file mode 100644
index a3418cc..0000000
--- a/java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sync;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.net.Network;
-import android.net.Uri;
-import android.os.Build.VERSION_CODES;
-import android.telecom.PhoneAccountHandle;
-import android.text.TextUtils;
-import com.android.voicemailomtp.ActivationTask;
-import com.android.voicemailomtp.Assert;
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.Voicemail;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.fetch.VoicemailFetchedCallback;
-import com.android.voicemailomtp.imap.ImapHelper;
-import com.android.voicemailomtp.imap.ImapHelper.InitializingException;
-import com.android.voicemailomtp.scheduling.BaseTask;
-import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil;
-import com.android.voicemailomtp.sync.VvmNetworkRequest.NetworkWrapper;
-import com.android.voicemailomtp.sync.VvmNetworkRequest.RequestFailedException;
-import com.android.voicemailomtp.utils.VoicemailDatabaseUtil;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/** Sync OMTP visual voicemail. */
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class OmtpVvmSyncService {
-
-    private static final String TAG = OmtpVvmSyncService.class.getSimpleName();
-
-    /**
-     * Signifies a sync with both uploading to the server and downloading from the server.
-     */
-    public static final String SYNC_FULL_SYNC = "full_sync";
-    /**
-     * Only upload to the server.
-     */
-    public static final String SYNC_UPLOAD_ONLY = "upload_only";
-    /**
-     * Only download from the server.
-     */
-    public static final String SYNC_DOWNLOAD_ONLY = "download_only";
-    /**
-     * Only download single voicemail transcription.
-     */
-    public static final String SYNC_DOWNLOAD_ONE_TRANSCRIPTION =
-            "download_one_transcription";
-
-    private final Context mContext;
-
-    // Record the timestamp of the last full sync so that duplicate syncs can be reduced.
-    private static final String LAST_FULL_SYNC_TIMESTAMP = "last_full_sync_timestamp";
-    // Constant indicating that there has never been a full sync.
-    public static final long NO_PRIOR_FULL_SYNC = -1;
-
-    private VoicemailsQueryHelper mQueryHelper;
-
-    public OmtpVvmSyncService(Context context) {
-        mContext = context;
-        mQueryHelper = new VoicemailsQueryHelper(mContext);
-    }
-
-    public void sync(BaseTask task, String action, PhoneAccountHandle phoneAccount,
-            Voicemail voicemail, VoicemailStatus.Editor status) {
-        Assert.isTrue(phoneAccount != null);
-        VvmLog.v(TAG, "Sync requested: " + action + " - for account: " + phoneAccount);
-        setupAndSendRequest(task, phoneAccount, voicemail, action, status);
-    }
-
-    private void setupAndSendRequest(BaseTask task, PhoneAccountHandle phoneAccount,
-            Voicemail voicemail, String action, VoicemailStatus.Editor status) {
-        if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phoneAccount)) {
-            VvmLog.v(TAG, "Sync requested for disabled account");
-            return;
-        }
-        if (!OmtpVvmSourceManager.getInstance(mContext).isVvmSourceRegistered(phoneAccount)) {
-            ActivationTask.start(mContext, phoneAccount, null);
-            return;
-        }
-
-        OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(mContext, phoneAccount);
-        // DATA_IMAP_OPERATION_STARTED posting should not be deferred. This event clears all data
-        // channel errors, which should happen when the task starts, not when it ends. It is the
-        // "Sync in progress..." status.
-        config.handleEvent(VoicemailStatus.edit(mContext, phoneAccount),
-                OmtpEvents.DATA_IMAP_OPERATION_STARTED);
-        try (NetworkWrapper network = VvmNetworkRequest.getNetwork(config, phoneAccount, status)) {
-            if (network == null) {
-                VvmLog.e(TAG, "unable to acquire network");
-                task.fail();
-                return;
-            }
-            doSync(task, network.get(), phoneAccount, voicemail, action, status);
-        } catch (RequestFailedException e) {
-            config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
-            task.fail();
-        }
-    }
-
-    private void doSync(BaseTask task, Network network, PhoneAccountHandle phoneAccount,
-            Voicemail voicemail, String action, VoicemailStatus.Editor status) {
-        try (ImapHelper imapHelper = new ImapHelper(mContext, phoneAccount, network, status)) {
-            boolean success;
-            if (voicemail == null) {
-                success = syncAll(action, imapHelper, phoneAccount);
-            } else {
-                success = syncOne(imapHelper, voicemail, phoneAccount);
-            }
-            if (success) {
-                // TODO: b/30569269 failure should interrupt all subsequent task via exceptions
-                imapHelper.updateQuota();
-                imapHelper.handleEvent(OmtpEvents.DATA_IMAP_OPERATION_COMPLETED);
-            } else {
-                task.fail();
-            }
-        } catch (InitializingException e) {
-            VvmLog.w(TAG, "Can't retrieve Imap credentials.", e);
-            return;
-        }
-    }
-
-    private boolean syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account) {
-        boolean uploadSuccess = true;
-        boolean downloadSuccess = true;
-
-        if (SYNC_FULL_SYNC.equals(action) || SYNC_UPLOAD_ONLY.equals(action)) {
-            uploadSuccess = upload(imapHelper);
-        }
-        if (SYNC_FULL_SYNC.equals(action) || SYNC_DOWNLOAD_ONLY.equals(action)) {
-            downloadSuccess = download(imapHelper, account);
-        }
-
-        VvmLog.v(TAG, "upload succeeded: [" + String.valueOf(uploadSuccess)
-                + "] download succeeded: [" + String.valueOf(downloadSuccess) + "]");
-
-        return uploadSuccess && downloadSuccess;
-    }
-
-    private boolean syncOne(ImapHelper imapHelper, Voicemail voicemail,
-            PhoneAccountHandle account) {
-        if (shouldPerformPrefetch(account, imapHelper)) {
-            VoicemailFetchedCallback callback = new VoicemailFetchedCallback(mContext,
-                    voicemail.getUri(), account);
-            imapHelper.fetchVoicemailPayload(callback, voicemail.getSourceData());
-        }
-
-        return imapHelper.fetchTranscription(
-                new TranscriptionFetchedCallback(mContext, voicemail),
-                voicemail.getSourceData());
-    }
-
-    private boolean upload(ImapHelper imapHelper) {
-        List<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails();
-        List<Voicemail> deletedVoicemails = mQueryHelper.getDeletedVoicemails();
-
-        boolean success = true;
-
-        if (deletedVoicemails.size() > 0) {
-            if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) {
-                // We want to delete selectively instead of all the voicemails for this provider
-                // in case the state changed since the IMAP query was completed.
-                mQueryHelper.deleteFromDatabase(deletedVoicemails);
-            } else {
-                success = false;
-            }
-        }
-
-        if (readVoicemails.size() > 0) {
-            if (imapHelper.markMessagesAsRead(readVoicemails)) {
-                mQueryHelper.markCleanInDatabase(readVoicemails);
-            } else {
-                success = false;
-            }
-        }
-
-        return success;
-    }
-
-    private boolean download(ImapHelper imapHelper, PhoneAccountHandle account) {
-        List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails();
-        List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails();
-
-        if (localVoicemails == null || serverVoicemails == null) {
-            // Null value means the query failed.
-            return false;
-        }
-
-        Map<String, Voicemail> remoteMap = buildMap(serverVoicemails);
-
-        // Go through all the local voicemails and check if they are on the server.
-        // They may be read or deleted on the server but not locally. Perform the
-        // appropriate local operation if the status differs from the server. Remove
-        // the messages that exist both locally and on the server to know which server
-        // messages to insert locally.
-        for (int i = 0; i < localVoicemails.size(); i++) {
-            Voicemail localVoicemail = localVoicemails.get(i);
-            Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData());
-            if (remoteVoicemail == null) {
-                mQueryHelper.deleteFromDatabase(localVoicemail);
-            } else {
-                if (remoteVoicemail.isRead() != localVoicemail.isRead()) {
-                    mQueryHelper.markReadInDatabase(localVoicemail);
-                }
-
-                if (!TextUtils.isEmpty(remoteVoicemail.getTranscription()) &&
-                        TextUtils.isEmpty(localVoicemail.getTranscription())) {
-                    mQueryHelper.updateWithTranscription(localVoicemail,
-                            remoteVoicemail.getTranscription());
-                }
-            }
-        }
-
-        // The leftover messages are messages that exist on the server but not locally.
-        boolean prefetchEnabled = shouldPerformPrefetch(account, imapHelper);
-        for (Voicemail remoteVoicemail : remoteMap.values()) {
-            Uri uri = VoicemailDatabaseUtil.insert(mContext, remoteVoicemail);
-            if (prefetchEnabled) {
-                VoicemailFetchedCallback fetchedCallback =
-                        new VoicemailFetchedCallback(mContext, uri, account);
-                imapHelper.fetchVoicemailPayload(fetchedCallback, remoteVoicemail.getSourceData());
-            }
-        }
-
-        return true;
-    }
-
-    private boolean shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper) {
-        OmtpVvmCarrierConfigHelper carrierConfigHelper =
-                new OmtpVvmCarrierConfigHelper(mContext, account);
-        return carrierConfigHelper.isPrefetchEnabled() && !imapHelper.isRoaming();
-    }
-
-    /**
-     * Builds a map from provider data to message for the given collection of voicemails.
-     */
-    private Map<String, Voicemail> buildMap(List<Voicemail> messages) {
-        Map<String, Voicemail> map = new HashMap<String, Voicemail>();
-        for (Voicemail message : messages) {
-            map.put(message.getSourceData(), message);
-        }
-        return map;
-    }
-
-    public class TranscriptionFetchedCallback {
-
-        private Context mContext;
-        private Voicemail mVoicemail;
-
-        public TranscriptionFetchedCallback(Context context, Voicemail voicemail) {
-            mContext = context;
-            mVoicemail = voicemail;
-        }
-
-        public void setVoicemailTranscription(String transcription) {
-            VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
-            queryHelper.updateWithTranscription(mVoicemail, transcription);
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/sync/SyncOneTask.java b/java/com/android/voicemailomtp/sync/SyncOneTask.java
deleted file mode 100644
index 9264e6c..0000000
--- a/java/com/android/voicemailomtp/sync/SyncOneTask.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.sync;
-
-import android.content.Context;
-import android.content.Intent;
-import android.telecom.PhoneAccountHandle;
-
-import com.android.voicemailomtp.Voicemail;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.scheduling.BaseTask;
-import com.android.voicemailomtp.scheduling.RetryPolicy;
-
-/**
- * Task to download a single voicemail from the server. This task is initiated by a SMS notifying
- * the new voicemail arrival, and ignores the duplicated tasks constraint.
- */
-public class SyncOneTask extends BaseTask {
-
-    private static final int RETRY_TIMES = 2;
-    private static final int RETRY_INTERVAL_MILLIS = 5_000;
-
-    private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
-    private static final String EXTRA_SYNC_TYPE = "extra_sync_type";
-    private static final String EXTRA_VOICEMAIL = "extra_voicemail";
-
-    private PhoneAccountHandle mPhone;
-    private String mSyncType;
-    private Voicemail mVoicemail;
-
-    public static void start(Context context, PhoneAccountHandle phone, Voicemail voicemail) {
-        Intent intent = BaseTask
-                .createIntent(context, SyncOneTask.class, phone);
-        intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phone);
-        intent.putExtra(EXTRA_SYNC_TYPE, OmtpVvmSyncService.SYNC_DOWNLOAD_ONE_TRANSCRIPTION);
-        intent.putExtra(EXTRA_VOICEMAIL, voicemail);
-        context.startService(intent);
-    }
-
-    public SyncOneTask() {
-        super(TASK_ALLOW_DUPLICATES);
-        addPolicy(new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS));
-    }
-
-    public void onCreate(Context context, Intent intent, int flags, int startId) {
-        super.onCreate(context, intent, flags, startId);
-        mPhone = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
-        mSyncType = intent.getStringExtra(EXTRA_SYNC_TYPE);
-        mVoicemail = intent.getParcelableExtra(EXTRA_VOICEMAIL);
-    }
-
-    @Override
-    public void onExecuteInBackgroundThread() {
-        OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
-        service.sync(this, mSyncType, mPhone, mVoicemail,
-                VoicemailStatus.edit(getContext(), mPhone));
-    }
-
-    @Override
-    public Intent createRestartIntent() {
-        Intent intent = super.createRestartIntent();
-        intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, mPhone);
-        intent.putExtra(EXTRA_SYNC_TYPE, mSyncType);
-        intent.putExtra(EXTRA_VOICEMAIL, mVoicemail);
-        return intent;
-    }
-
-}
diff --git a/java/com/android/voicemailomtp/sync/SyncTask.java b/java/com/android/voicemailomtp/sync/SyncTask.java
deleted file mode 100644
index 41b22f2..0000000
--- a/java/com/android/voicemailomtp/sync/SyncTask.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.sync;
-
-import android.content.Context;
-import android.content.Intent;
-import android.telecom.PhoneAccountHandle;
-
-import com.android.voicemailomtp.scheduling.BaseTask;
-import com.android.voicemailomtp.scheduling.MinimalIntervalPolicy;
-import com.android.voicemailomtp.scheduling.RetryPolicy;
-
-/**
- * System initiated sync request.
- */
-public class SyncTask extends BaseTask {
-
-    // Try sync for a total of 5 times, should take around 5 minutes before finally giving up.
-    private static final int RETRY_TIMES = 4;
-    private static final int RETRY_INTERVAL_MILLIS = 5_000;
-    private static final int MINIMAL_INTERVAL_MILLIS = 60_000;
-
-    private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
-    private static final String EXTRA_SYNC_TYPE = "extra_sync_type";
-
-    private final RetryPolicy mRetryPolicy;
-
-    private PhoneAccountHandle mPhone;
-    private String mSyncType;
-
-    public static void start(Context context, PhoneAccountHandle phone, String syncType) {
-        Intent intent = BaseTask
-                .createIntent(context, SyncTask.class, phone);
-        intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phone);
-        intent.putExtra(EXTRA_SYNC_TYPE, syncType);
-        context.startService(intent);
-    }
-
-    public SyncTask() {
-        super(TASK_SYNC);
-        mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS);
-        addPolicy(mRetryPolicy);
-        addPolicy(new MinimalIntervalPolicy(MINIMAL_INTERVAL_MILLIS));
-    }
-
-    public void onCreate(Context context, Intent intent, int flags, int startId) {
-        super.onCreate(context, intent, flags, startId);
-        mPhone = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
-        mSyncType = intent.getStringExtra(EXTRA_SYNC_TYPE);
-    }
-
-    @Override
-    public void onExecuteInBackgroundThread() {
-        OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
-        service.sync(this, mSyncType, mPhone, null, mRetryPolicy.getVoicemailStatusEditor());
-    }
-
-    @Override
-    public Intent createRestartIntent() {
-        Intent intent = super.createRestartIntent();
-        intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, mPhone);
-        intent.putExtra(EXTRA_SYNC_TYPE, mSyncType);
-        return intent;
-    }
-}
diff --git a/java/com/android/voicemailomtp/sync/UploadTask.java b/java/com/android/voicemailomtp/sync/UploadTask.java
deleted file mode 100644
index 30a1681..0000000
--- a/java/com/android/voicemailomtp/sync/UploadTask.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.sync;
-
-import android.content.Context;
-import android.content.Intent;
-import android.telecom.PhoneAccountHandle;
-
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-import com.android.voicemailomtp.scheduling.BaseTask;
-import com.android.voicemailomtp.scheduling.PostponePolicy;
-
-/**
- * Upload task triggered by database changes. Will wait until the database has been stable for
- * {@link #POSTPONE_MILLIS} to execute.
- */
-public class UploadTask extends BaseTask {
-
-    private static final String TAG = "VvmUploadTask";
-
-    private static final int POSTPONE_MILLIS = 5_000;
-
-    public UploadTask() {
-        super(TASK_UPLOAD);
-        addPolicy(new PostponePolicy(POSTPONE_MILLIS));
-    }
-
-    public static void start(Context context, PhoneAccountHandle phoneAccountHandle) {
-        Intent intent = BaseTask
-                .createIntent(context, UploadTask.class, phoneAccountHandle);
-        context.startService(intent);
-    }
-
-    @Override
-    public void onCreate(Context context, Intent intent, int flags, int startId) {
-        super.onCreate(context, intent, flags, startId);
-    }
-
-    @Override
-    public void onExecuteInBackgroundThread() {
-        OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
-
-        PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
-        if (phoneAccountHandle == null) {
-            // This should never happen
-            VvmLog.e(TAG, "null phone account for phoneAccountHandle " + getPhoneAccountHandle());
-            return;
-        }
-        service.sync(this, OmtpVvmSyncService.SYNC_UPLOAD_ONLY,
-                phoneAccountHandle, null,
-                VoicemailStatus.edit(getContext(), phoneAccountHandle));
-    }
-}
diff --git a/java/com/android/voicemailomtp/sync/VoicemailProviderChangeReceiver.java b/java/com/android/voicemailomtp/sync/VoicemailProviderChangeReceiver.java
deleted file mode 100644
index ade9ef1..0000000
--- a/java/com/android/voicemailomtp/sync/VoicemailProviderChangeReceiver.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sync;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.provider.VoicemailContract;
-import android.telecom.PhoneAccountHandle;
-
-/**
- * Receives changes to the voicemail provider so they can be sent to the voicemail server.
- */
-public class VoicemailProviderChangeReceiver extends BroadcastReceiver {
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        boolean isSelfChanged = intent.getBooleanExtra(VoicemailContract.EXTRA_SELF_CHANGE, false);
-        OmtpVvmSourceManager vvmSourceManager =
-                OmtpVvmSourceManager.getInstance(context);
-        if (vvmSourceManager.getOmtpVvmSources().size() > 0 && !isSelfChanged) {
-            for (PhoneAccountHandle source : OmtpVvmSourceManager.getInstance(context)
-                    .getOmtpVvmSources()) {
-                UploadTask.start(context, source);
-            }
-        }
-    }
-}
diff --git a/java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java b/java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java
deleted file mode 100644
index 89ba0b4..0000000
--- a/java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sync;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.VoicemailContract;
-import android.provider.VoicemailContract.Status;
-import android.telecom.PhoneAccountHandle;
-
-/**
- * Construct queries to interact with the voicemail status table.
- */
-public class VoicemailStatusQueryHelper {
-
-    final static String[] PROJECTION = new String[] {
-            Status._ID,                        // 0
-            Status.CONFIGURATION_STATE,        // 1
-            Status.NOTIFICATION_CHANNEL_STATE, // 2
-            Status.SOURCE_PACKAGE              // 3
-   };
-
-    public static final int _ID = 0;
-    public static final int CONFIGURATION_STATE = 1;
-    public static final int NOTIFICATION_CHANNEL_STATE = 2;
-    public static final int SOURCE_PACKAGE = 3;
-
-    private Context mContext;
-    private ContentResolver mContentResolver;
-    private Uri mSourceUri;
-
-    public VoicemailStatusQueryHelper(Context context) {
-        mContext = context;
-        mContentResolver = context.getContentResolver();
-        mSourceUri = VoicemailContract.Status.buildSourceUri(mContext.getPackageName());
-    }
-
-    /**
-     * Check if the configuration state for the voicemail source is "ok", meaning that the
-     * source is set up.
-     *
-     * @param phoneAccount The phone account for the voicemail source to check.
-     * @return {@code true} if the voicemail source is configured, {@code} false otherwise,
-     * including if the voicemail source is not registered in the table.
-     */
-    public boolean isVoicemailSourceConfigured(PhoneAccountHandle phoneAccount) {
-        return isFieldEqualTo(phoneAccount, CONFIGURATION_STATE, Status.CONFIGURATION_STATE_OK);
-    }
-
-    /**
-     * Check if the notifications channel of a voicemail source is active. That is, when a new
-     * voicemail is available, if the server able to notify the device.
-     *
-     * @return {@code true} if notifications channel is active, {@code false} otherwise.
-     */
-    public boolean isNotificationsChannelActive(PhoneAccountHandle phoneAccount) {
-        return isFieldEqualTo(phoneAccount, NOTIFICATION_CHANNEL_STATE,
-                Status.NOTIFICATION_CHANNEL_STATE_OK);
-    }
-
-    /**
-     * Check if a field for an entry in the status table is equal to a specific value.
-     *
-     * @param phoneAccount The phone account of the voicemail source to query for.
-     * @param columnIndex The column index of the field in the returned query.
-     * @param value The value to compare against.
-     * @return {@code true} if the stored value is equal to the provided value. {@code false}
-     * otherwise.
-     */
-    private boolean isFieldEqualTo(PhoneAccountHandle phoneAccount, int columnIndex, int value) {
-        Cursor cursor = null;
-        if (phoneAccount != null) {
-            String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString();
-            String phoneAccountId = phoneAccount.getId();
-            if (phoneAccountComponentName == null || phoneAccountId == null) {
-                return false;
-            }
-            try {
-                String whereClause =
-                        Status.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " +
-                        Status.PHONE_ACCOUNT_ID + "=? AND " + Status.SOURCE_PACKAGE + "=?";
-                String[] whereArgs = { phoneAccountComponentName, phoneAccountId,
-                        mContext.getPackageName()};
-                cursor = mContentResolver.query(
-                        mSourceUri, PROJECTION, whereClause, whereArgs, null);
-                if (cursor != null && cursor.moveToFirst()) {
-                    return cursor.getInt(columnIndex) == value;
-                }
-            }
-            finally {
-                if (cursor != null) {
-                    cursor.close();
-                }
-            }
-        }
-        return false;
-    }
-}
diff --git a/java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java b/java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java
deleted file mode 100644
index 1450e3d..0000000
--- a/java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java
+++ /dev/null
@@ -1,244 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sync;
-
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.VoicemailContract;
-import android.provider.VoicemailContract.Voicemails;
-import android.telecom.PhoneAccountHandle;
-import com.android.voicemailomtp.Voicemail;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Construct queries to interact with the voicemails table.
- */
-public class VoicemailsQueryHelper {
-    final static String[] PROJECTION = new String[] {
-            Voicemails._ID,              // 0
-            Voicemails.SOURCE_DATA,      // 1
-            Voicemails.IS_READ,          // 2
-            Voicemails.DELETED,          // 3
-            Voicemails.TRANSCRIPTION     // 4
-    };
-
-    public static final int _ID = 0;
-    public static final int SOURCE_DATA = 1;
-    public static final int IS_READ = 2;
-    public static final int DELETED = 3;
-    public static final int TRANSCRIPTION = 4;
-
-    final static String READ_SELECTION = Voicemails.DIRTY + "=1 AND "
-                + Voicemails.DELETED + "!=1 AND " + Voicemails.IS_READ + "=1";
-    final static String DELETED_SELECTION = Voicemails.DELETED + "=1";
-
-    private Context mContext;
-    private ContentResolver mContentResolver;
-    private Uri mSourceUri;
-
-    public VoicemailsQueryHelper(Context context) {
-        mContext = context;
-        mContentResolver = context.getContentResolver();
-        mSourceUri = VoicemailContract.Voicemails.buildSourceUri(mContext.getPackageName());
-    }
-
-    /**
-     * Get all the local read voicemails that have not been synced to the server.
-     *
-     * @return A list of read voicemails.
-     */
-    public List<Voicemail> getReadVoicemails() {
-        return getLocalVoicemails(READ_SELECTION);
-    }
-
-    /**
-     * Get all the locally deleted voicemails that have not been synced to the server.
-     *
-     * @return A list of deleted voicemails.
-     */
-    public List<Voicemail> getDeletedVoicemails() {
-        return getLocalVoicemails(DELETED_SELECTION);
-    }
-
-    /**
-     * Get all voicemails locally stored.
-     *
-     * @return A list of all locally stored voicemails.
-     */
-    public List<Voicemail> getAllVoicemails() {
-        return getLocalVoicemails(null);
-    }
-
-    /**
-     * Utility method to make queries to the voicemail database.
-     *
-     * @param selection A filter declaring which rows to return. {@code null} returns all rows.
-     * @return A list of voicemails according to the selection statement.
-     */
-    private List<Voicemail> getLocalVoicemails(String selection) {
-        Cursor cursor = mContentResolver.query(mSourceUri, PROJECTION, selection, null, null);
-        if (cursor == null) {
-            return null;
-        }
-        try {
-            List<Voicemail> voicemails = new ArrayList<Voicemail>();
-            while (cursor.moveToNext()) {
-                final long id = cursor.getLong(_ID);
-                final String sourceData = cursor.getString(SOURCE_DATA);
-                final boolean isRead = cursor.getInt(IS_READ) == 1;
-                final String transcription = cursor.getString(TRANSCRIPTION);
-                Voicemail voicemail = Voicemail
-                        .createForUpdate(id, sourceData)
-                        .setIsRead(isRead)
-                        .setTranscription(transcription).build();
-                voicemails.add(voicemail);
-            }
-            return voicemails;
-        } finally {
-            cursor.close();
-        }
-    }
-
-    /**
-     * Deletes a list of voicemails from the voicemail content provider.
-     *
-     * @param voicemails The list of voicemails to delete
-     * @return The number of voicemails deleted
-     */
-    public int deleteFromDatabase(List<Voicemail> voicemails) {
-        int count = voicemails.size();
-        if (count == 0) {
-            return 0;
-        }
-
-        StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < count; i++) {
-            if (i > 0) {
-                sb.append(",");
-            }
-            sb.append(voicemails.get(i).getId());
-        }
-
-        String selectionStatement = String.format(Voicemails._ID + " IN (%s)", sb.toString());
-        return mContentResolver.delete(Voicemails.CONTENT_URI, selectionStatement, null);
-    }
-
-    /**
-     * Utility method to delete a single voicemail.
-     */
-    public void deleteFromDatabase(Voicemail voicemail) {
-        mContentResolver.delete(Voicemails.CONTENT_URI, Voicemails._ID + "=?",
-                new String[] { Long.toString(voicemail.getId()) });
-    }
-
-    public int markReadInDatabase(List<Voicemail> voicemails) {
-        int count = voicemails.size();
-        for (int i = 0; i < count; i++) {
-            markReadInDatabase(voicemails.get(i));
-        }
-        return count;
-    }
-
-    /**
-     * Utility method to mark single message as read.
-     */
-    public void markReadInDatabase(Voicemail voicemail) {
-        Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
-        ContentValues contentValues = new ContentValues();
-        contentValues.put(Voicemails.IS_READ, "1");
-        mContentResolver.update(uri, contentValues, null, null);
-    }
-
-    /**
-     * Sends an update command to the voicemail content provider for a list of voicemails. From the
-     * view of the provider, since the updater is the owner of the entry, a blank "update" means
-     * that the voicemail source is indicating that the server has up-to-date information on the
-     * voicemail. This flips the "dirty" bit to "0".
-     *
-     * @param voicemails The list of voicemails to update
-     * @return The number of voicemails updated
-     */
-    public int markCleanInDatabase(List<Voicemail> voicemails) {
-        int count = voicemails.size();
-        for (int i = 0; i < count; i++) {
-            markCleanInDatabase(voicemails.get(i));
-        }
-        return count;
-    }
-
-    /**
-     * Utility method to mark single message as clean.
-     */
-    public void markCleanInDatabase(Voicemail voicemail) {
-        Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
-        ContentValues contentValues = new ContentValues();
-        mContentResolver.update(uri, contentValues, null, null);
-    }
-
-    /**
-     * Utility method to add a transcription to the voicemail.
-     */
-    public void updateWithTranscription(Voicemail voicemail, String transcription) {
-        Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
-        ContentValues contentValues = new ContentValues();
-        contentValues.put(Voicemails.TRANSCRIPTION, transcription);
-        mContentResolver.update(uri, contentValues, null, null);
-    }
-
-    /**
-     * Voicemail is unique if the tuple of (phone account component name, phone account id, source
-     * data) is unique. If the phone account is missing, we also consider this unique since it's
-     * simply an "unknown" account.
-     * @param voicemail The voicemail to check if it is unique.
-     * @return {@code true} if the voicemail is unique, {@code false} otherwise.
-     */
-    public boolean isVoicemailUnique(Voicemail voicemail) {
-        Cursor cursor = null;
-        PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount();
-        if (phoneAccount != null) {
-            String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString();
-            String phoneAccountId = phoneAccount.getId();
-            String sourceData = voicemail.getSourceData();
-            if (phoneAccountComponentName == null || phoneAccountId == null || sourceData == null) {
-                return true;
-            }
-            try {
-                String whereClause =
-                        Voicemails.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " +
-                        Voicemails.PHONE_ACCOUNT_ID + "=? AND " + Voicemails.SOURCE_DATA + "=?";
-                String[] whereArgs = { phoneAccountComponentName, phoneAccountId, sourceData };
-                cursor = mContentResolver.query(
-                        mSourceUri, PROJECTION, whereClause, whereArgs, null);
-                if (cursor.getCount() == 0) {
-                    return true;
-                } else {
-                    return false;
-                }
-            }
-            finally {
-                if (cursor != null) {
-                    cursor.close();
-                }
-            }
-        }
-        return true;
-    }
-}
diff --git a/java/com/android/voicemailomtp/sync/VvmNetworkRequest.java b/java/com/android/voicemailomtp/sync/VvmNetworkRequest.java
deleted file mode 100644
index 966b940..0000000
--- a/java/com/android/voicemailomtp/sync/VvmNetworkRequest.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.sync;
-
-import android.annotation.TargetApi;
-import android.net.Network;
-import android.os.Build.VERSION_CODES;
-import android.support.annotation.NonNull;
-import android.telecom.PhoneAccountHandle;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-import java.io.Closeable;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-
-/**
- * Class to retrieve a {@link Network} synchronously. {@link #getNetwork(OmtpVvmCarrierConfigHelper,
- * PhoneAccountHandle)} will block until a suitable network is retrieved or it has failed.
- */
-@SuppressWarnings("AndroidApiChecker") /* CompletableFuture is java8*/
-@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
-public class VvmNetworkRequest {
-
-    private static final String TAG = "VvmNetworkRequest";
-
-    /**
-     * A wrapper around a Network returned by a {@link VvmNetworkRequestCallback}, which should be
-     * closed once not needed anymore.
-     */
-    public static class NetworkWrapper implements Closeable {
-
-        private final Network mNetwork;
-        private final VvmNetworkRequestCallback mCallback;
-
-        private NetworkWrapper(Network network, VvmNetworkRequestCallback callback) {
-            mNetwork = network;
-            mCallback = callback;
-        }
-
-        public Network get() {
-            return mNetwork;
-        }
-
-        @Override
-        public void close() {
-            mCallback.releaseNetwork();
-        }
-    }
-
-    public static class RequestFailedException extends Exception {
-
-        private RequestFailedException(Throwable cause) {
-            super(cause);
-        }
-    }
-
-    @NonNull
-    public static NetworkWrapper getNetwork(OmtpVvmCarrierConfigHelper config,
-        PhoneAccountHandle handle, VoicemailStatus.Editor status) throws RequestFailedException {
-        FutureNetworkRequestCallback callback = new FutureNetworkRequestCallback(config, handle,
-            status);
-        callback.requestNetwork();
-        try {
-            return callback.getFuture().get();
-        } catch (InterruptedException | ExecutionException e) {
-            callback.releaseNetwork();
-            VvmLog.e(TAG, "can't get future network", e);
-            throw new RequestFailedException(e);
-        }
-    }
-
-    private static class FutureNetworkRequestCallback extends VvmNetworkRequestCallback {
-
-        /**
-         * {@link CompletableFuture#get()} will block until {@link CompletableFuture#
-         * complete(Object) } has been called on the other thread.
-         */
-        private final CompletableFuture<NetworkWrapper> mFuture = new CompletableFuture<>();
-
-        public FutureNetworkRequestCallback(OmtpVvmCarrierConfigHelper config,
-            PhoneAccountHandle phoneAccount, VoicemailStatus.Editor status) {
-            super(config, phoneAccount, status);
-        }
-
-        public Future<NetworkWrapper> getFuture() {
-            return mFuture;
-        }
-
-        @Override
-        public void onAvailable(Network network) {
-            super.onAvailable(network);
-            mFuture.complete(new NetworkWrapper(network, this));
-        }
-
-        @Override
-        public void onFailed(String reason) {
-            super.onFailed(reason);
-            mFuture.complete(null);
-        }
-
-    }
-}
diff --git a/java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java b/java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java
deleted file mode 100644
index 8481a9d..0000000
--- a/java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.voicemailomtp.sync;
-
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
-import android.os.Handler;
-import android.os.Looper;
-import android.support.annotation.CallSuper;
-import android.telecom.PhoneAccountHandle;
-
-import com.android.voicemailomtp.OmtpEvents;
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.TelephonyManagerStub;
-import com.android.voicemailomtp.VoicemailStatus;
-import com.android.voicemailomtp.VvmLog;
-
-/**
- * Base class for network request call backs for visual voicemail syncing with the Imap server. This
- * handles retries and network requests.
- */
-public abstract class VvmNetworkRequestCallback extends ConnectivityManager.NetworkCallback {
-
-    private static final String TAG = "VvmNetworkRequest";
-
-    // Timeout used to call ConnectivityManager.requestNetwork
-    private static final int NETWORK_REQUEST_TIMEOUT_MILLIS = 60 * 1000;
-
-    public static final String NETWORK_REQUEST_FAILED_TIMEOUT = "timeout";
-    public static final String NETWORK_REQUEST_FAILED_LOST = "lost";
-
-    protected Context mContext;
-    protected PhoneAccountHandle mPhoneAccount;
-    protected NetworkRequest mNetworkRequest;
-    private ConnectivityManager mConnectivityManager;
-    private final OmtpVvmCarrierConfigHelper mCarrierConfigHelper;
-    private final VoicemailStatus.Editor mStatus;
-    private boolean mRequestSent = false;
-    private boolean mResultReceived = false;
-
-    public VvmNetworkRequestCallback(Context context, PhoneAccountHandle phoneAccount,
-        VoicemailStatus.Editor status) {
-        mContext = context;
-        mPhoneAccount = phoneAccount;
-        mStatus = status;
-        mCarrierConfigHelper = new OmtpVvmCarrierConfigHelper(context, mPhoneAccount);
-        mNetworkRequest = createNetworkRequest();
-    }
-
-    public VvmNetworkRequestCallback(OmtpVvmCarrierConfigHelper config,
-        PhoneAccountHandle phoneAccount, VoicemailStatus.Editor status) {
-        mContext = config.getContext();
-        mPhoneAccount = phoneAccount;
-        mStatus = status;
-        mCarrierConfigHelper = config;
-        mNetworkRequest = createNetworkRequest();
-    }
-
-    public VoicemailStatus.Editor getVoicemailStatusEditor() {
-        return mStatus;
-    }
-
-    /**
-     * @return NetworkRequest for a proper transport type. Use only cellular network if the carrier
-     * requires it. Otherwise use whatever available.
-     */
-    private NetworkRequest createNetworkRequest() {
-
-        NetworkRequest.Builder builder = new NetworkRequest.Builder()
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
-
-        if (mCarrierConfigHelper.isCellularDataRequired()) {
-            VvmLog.d(TAG, "Transport type: CELLULAR");
-            builder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
-                    .setNetworkSpecifier(TelephonyManagerStub
-                            .getNetworkSpecifierForPhoneAccountHandle(mContext, mPhoneAccount));
-        } else {
-            VvmLog.d(TAG, "Transport type: ANY");
-        }
-        return builder.build();
-    }
-
-    public NetworkRequest getNetworkRequest() {
-        return mNetworkRequest;
-    }
-
-    @Override
-    @CallSuper
-    public void onLost(Network network) {
-        VvmLog.d(TAG, "onLost");
-        mResultReceived = true;
-        onFailed(NETWORK_REQUEST_FAILED_LOST);
-    }
-
-    @Override
-    @CallSuper
-    public void onAvailable(Network network) {
-        super.onAvailable(network);
-        mResultReceived = true;
-    }
-
-    @CallSuper
-    public void onUnavailable() {
-        // TODO: b/32637799 this is hidden, do we really need this?
-        mResultReceived = true;
-        onFailed(NETWORK_REQUEST_FAILED_TIMEOUT);
-    }
-
-    public void requestNetwork() {
-        if (mRequestSent == true) {
-            VvmLog.e(TAG, "requestNetwork() called twice");
-            return;
-        }
-        mRequestSent = true;
-        getConnectivityManager().requestNetwork(getNetworkRequest(), this);
-        /**
-         * Somehow requestNetwork() with timeout doesn't work, and it's a hidden method.
-         * Implement our own timeout mechanism instead.
-         */
-        Handler handler = new Handler(Looper.getMainLooper());
-        handler.postDelayed(new Runnable() {
-            @Override
-            public void run() {
-                if (mResultReceived == false) {
-                    onFailed(NETWORK_REQUEST_FAILED_TIMEOUT);
-                }
-            }
-        }, NETWORK_REQUEST_TIMEOUT_MILLIS);
-    }
-
-    public void releaseNetwork() {
-        VvmLog.d(TAG, "releaseNetwork");
-        getConnectivityManager().unregisterNetworkCallback(this);
-    }
-
-    public ConnectivityManager getConnectivityManager() {
-        if (mConnectivityManager == null) {
-            mConnectivityManager = (ConnectivityManager) mContext.getSystemService(
-                    Context.CONNECTIVITY_SERVICE);
-        }
-        return mConnectivityManager;
-    }
-
-    @CallSuper
-    public void onFailed(String reason) {
-        VvmLog.d(TAG, "onFailed: " + reason);
-        if (mCarrierConfigHelper.isCellularDataRequired()) {
-            mCarrierConfigHelper
-                .handleEvent(mStatus, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
-        } else {
-            mCarrierConfigHelper.handleEvent(mStatus, OmtpEvents.DATA_NO_CONNECTION);
-        }
-        releaseNetwork();
-    }
-}
diff --git a/java/com/android/voicemailomtp/utils/IndentingPrintWriter.java b/java/com/android/voicemailomtp/utils/IndentingPrintWriter.java
deleted file mode 100644
index eda7c4e..0000000
--- a/java/com/android/voicemailomtp/utils/IndentingPrintWriter.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.utils;
-
-import java.io.PrintWriter;
-import java.io.Writer;
-import java.util.Arrays;
-
-/**
- * Lightweight wrapper around {@link PrintWriter} that automatically indents newlines based on
- * internal state. It also automatically wraps long lines based on given line length. <p> Delays
- * writing indent until first actual write on a newline, enabling indent modification after
- * newline.
- */
-public class IndentingPrintWriter extends PrintWriter {
-
-    private final String mSingleIndent;
-    private final int mWrapLength;
-
-    /**
-     * Mutable version of current indent
-     */
-    private StringBuilder mIndentBuilder = new StringBuilder();
-    /**
-     * Cache of current {@link #mIndentBuilder} value
-     */
-    private char[] mCurrentIndent;
-    /**
-     * Length of current line being built, excluding any indent
-     */
-    private int mCurrentLength;
-
-    /**
-     * Flag indicating if we're currently sitting on an empty line, and that next write should be
-     * prefixed with the current indent.
-     */
-    private boolean mEmptyLine = true;
-
-    private char[] mSingleChar = new char[1];
-
-    public IndentingPrintWriter(Writer writer, String singleIndent) {
-        this(writer, singleIndent, -1);
-    }
-
-    public IndentingPrintWriter(Writer writer, String singleIndent, int wrapLength) {
-        super(writer);
-        mSingleIndent = singleIndent;
-        mWrapLength = wrapLength;
-    }
-
-    public void increaseIndent() {
-        mIndentBuilder.append(mSingleIndent);
-        mCurrentIndent = null;
-    }
-
-    public void decreaseIndent() {
-        mIndentBuilder.delete(0, mSingleIndent.length());
-        mCurrentIndent = null;
-    }
-
-    public void printPair(String key, Object value) {
-        print(key + "=" + String.valueOf(value) + " ");
-    }
-
-    public void printPair(String key, Object[] value) {
-        print(key + "=" + Arrays.toString(value) + " ");
-    }
-
-    public void printHexPair(String key, int value) {
-        print(key + "=0x" + Integer.toHexString(value) + " ");
-    }
-
-    @Override
-    public void println() {
-        write('\n');
-    }
-
-    @Override
-    public void write(int c) {
-        mSingleChar[0] = (char) c;
-        write(mSingleChar, 0, 1);
-    }
-
-    @Override
-    public void write(String s, int off, int len) {
-        final char[] buf = new char[len];
-        s.getChars(off, len - off, buf, 0);
-        write(buf, 0, len);
-    }
-
-    @Override
-    public void write(char[] buf, int offset, int count) {
-        final int indentLength = mIndentBuilder.length();
-        final int bufferEnd = offset + count;
-        int lineStart = offset;
-        int lineEnd = offset;
-
-        // March through incoming buffer looking for newlines
-        while (lineEnd < bufferEnd) {
-            char ch = buf[lineEnd++];
-            mCurrentLength++;
-            if (ch == '\n') {
-                maybeWriteIndent();
-                super.write(buf, lineStart, lineEnd - lineStart);
-                lineStart = lineEnd;
-                mEmptyLine = true;
-                mCurrentLength = 0;
-            }
-
-            // Wrap if we've pushed beyond line length
-            if (mWrapLength > 0 && mCurrentLength >= mWrapLength - indentLength) {
-                if (!mEmptyLine) {
-                    // Give ourselves a fresh line to work with
-                    super.write('\n');
-                    mEmptyLine = true;
-                    mCurrentLength = lineEnd - lineStart;
-                } else {
-                    // We need more than a dedicated line, slice it hard
-                    maybeWriteIndent();
-                    super.write(buf, lineStart, lineEnd - lineStart);
-                    super.write('\n');
-                    mEmptyLine = true;
-                    lineStart = lineEnd;
-                    mCurrentLength = 0;
-                }
-            }
-        }
-
-        if (lineStart != lineEnd) {
-            maybeWriteIndent();
-            super.write(buf, lineStart, lineEnd - lineStart);
-        }
-    }
-
-    private void maybeWriteIndent() {
-        if (mEmptyLine) {
-            mEmptyLine = false;
-            if (mIndentBuilder.length() != 0) {
-                if (mCurrentIndent == null) {
-                    mCurrentIndent = mIndentBuilder.toString().toCharArray();
-                }
-                super.write(mCurrentIndent, 0, mCurrentIndent.length);
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.java b/java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.java
deleted file mode 100644
index f94070e..0000000
--- a/java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.utils;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.net.Uri;
-import android.provider.VoicemailContract.Voicemails;
-import android.telecom.PhoneAccountHandle;
-import com.android.voicemailomtp.Voicemail;
-import java.util.List;
-
-public class VoicemailDatabaseUtil {
-
-    /**
-     * Inserts a new voicemail into the voicemail content provider.
-     *
-     * @param context The context of the app doing the inserting
-     * @param voicemail Data to be inserted
-     * @return {@link Uri} of the newly inserted {@link Voicemail}
-     * @hide
-     */
-    public static Uri insert(Context context, Voicemail voicemail) {
-        ContentResolver contentResolver = context.getContentResolver();
-        ContentValues contentValues = getContentValues(voicemail);
-        return contentResolver
-                .insert(Voicemails.buildSourceUri(context.getPackageName()), contentValues);
-    }
-
-    /**
-     * Inserts a list of voicemails into the voicemail content provider.
-     *
-     * @param context The context of the app doing the inserting
-     * @param voicemails Data to be inserted
-     * @return the number of voicemails inserted
-     * @hide
-     */
-    public static int insert(Context context, List<Voicemail> voicemails) {
-        ContentResolver contentResolver = context.getContentResolver();
-        int count = voicemails.size();
-        for (int i = 0; i < count; i++) {
-            ContentValues contentValues = getContentValues(voicemails.get(i));
-            contentResolver
-                    .insert(Voicemails.buildSourceUri(context.getPackageName()), contentValues);
-        }
-        return count;
-    }
-
-
-    /**
-     * Maps structured {@link Voicemail} to {@link ContentValues} in content provider.
-     */
-    private static ContentValues getContentValues(Voicemail voicemail) {
-        ContentValues contentValues = new ContentValues();
-        contentValues.put(Voicemails.DATE, String.valueOf(voicemail.getTimestampMillis()));
-        contentValues.put(Voicemails.NUMBER, voicemail.getNumber());
-        contentValues.put(Voicemails.DURATION, String.valueOf(voicemail.getDuration()));
-        contentValues.put(Voicemails.SOURCE_PACKAGE, voicemail.getSourcePackage());
-        contentValues.put(Voicemails.SOURCE_DATA, voicemail.getSourceData());
-        contentValues.put(Voicemails.IS_READ, voicemail.isRead() ? 1 : 0);
-
-        PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount();
-        if (phoneAccount != null) {
-            contentValues.put(Voicemails.PHONE_ACCOUNT_COMPONENT_NAME,
-                    phoneAccount.getComponentName().flattenToString());
-            contentValues.put(Voicemails.PHONE_ACCOUNT_ID, phoneAccount.getId());
-        }
-
-        if (voicemail.getTranscription() != null) {
-            contentValues.put(Voicemails.TRANSCRIPTION, voicemail.getTranscription());
-        }
-
-        return contentValues;
-    }
-}
diff --git a/java/com/android/voicemailomtp/utils/VvmDumpHandler.java b/java/com/android/voicemailomtp/utils/VvmDumpHandler.java
deleted file mode 100644
index 5768a9c..0000000
--- a/java/com/android/voicemailomtp/utils/VvmDumpHandler.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.voicemailomtp.utils;
-
-import android.content.Context;
-import android.telecom.PhoneAccountHandle;
-import android.telecom.TelecomManager;
-
-import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
-import com.android.voicemailomtp.VvmLog;
-
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-
-public class VvmDumpHandler {
-
-    public static void dump(Context context, FileDescriptor fd, PrintWriter writer,
-            String[] args) {
-        IndentingPrintWriter indentedWriter = new IndentingPrintWriter(writer, "  ");
-        indentedWriter.println("******* OmtpVvm *******");
-        indentedWriter.println("======= Configs =======");
-        indentedWriter.increaseIndent();
-        for (PhoneAccountHandle handle : context.getSystemService(TelecomManager.class)
-                .getCallCapablePhoneAccounts()) {
-            OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(context, handle);
-            indentedWriter.println(config.toString());
-        }
-        indentedWriter.decreaseIndent();
-        indentedWriter.println("======== Logs =========");
-        VvmLog.dump(fd, indentedWriter, args);
-    }
-}
diff --git a/java/com/android/voicemailomtp/utils/XmlUtils.java b/java/com/android/voicemailomtp/utils/XmlUtils.java
deleted file mode 100644
index 768247e..0000000
--- a/java/com/android/voicemailomtp/utils/XmlUtils.java
+++ /dev/null
@@ -1,245 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.voicemailomtp.utils;
-
-import android.util.ArrayMap;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-public class XmlUtils {
-
-    public static final ArrayMap<String, ?> readThisArrayMapXml(XmlPullParser parser, String endTag,
-            String[] name, ReadMapCallback callback)
-            throws XmlPullParserException, java.io.IOException {
-        ArrayMap<String, Object> map = new ArrayMap<>();
-
-        int eventType = parser.getEventType();
-        do {
-            if (eventType == XmlPullParser.START_TAG) {
-                Object val = readThisValueXml(parser, name, callback, true);
-                map.put(name[0], val);
-            } else if (eventType == XmlPullParser.END_TAG) {
-                if (parser.getName().equals(endTag)) {
-                    return map;
-                }
-                throw new XmlPullParserException(
-                        "Expected " + endTag + " end tag at: " + parser.getName());
-            }
-            eventType = parser.next();
-        } while (eventType != XmlPullParser.END_DOCUMENT);
-
-        throw new XmlPullParserException(
-                "Document ended before " + endTag + " end tag");
-    }
-
-    /**
-     * Read an ArrayList object from an XmlPullParser.  The XML data could previously have been
-     * generated by writeListXml().  The XmlPullParser must be positioned <em>after</em> the tag
-     * that begins the list.
-     *
-     * @param parser The XmlPullParser from which to read the list data.
-     * @param endTag Name of the tag that will end the list, usually "list".
-     * @param name An array of one string, used to return the name attribute of the list's tag.
-     * @return HashMap The newly generated list.
-     */
-    public static final ArrayList readThisListXml(XmlPullParser parser, String endTag,
-            String[] name, ReadMapCallback callback, boolean arrayMap)
-            throws XmlPullParserException, java.io.IOException {
-        ArrayList list = new ArrayList();
-
-        int eventType = parser.getEventType();
-        do {
-            if (eventType == XmlPullParser.START_TAG) {
-                Object val = readThisValueXml(parser, name, callback, arrayMap);
-                list.add(val);
-            } else if (eventType == XmlPullParser.END_TAG) {
-                if (parser.getName().equals(endTag)) {
-                    return list;
-                }
-                throw new XmlPullParserException(
-                        "Expected " + endTag + " end tag at: " + parser.getName());
-            }
-            eventType = parser.next();
-        } while (eventType != XmlPullParser.END_DOCUMENT);
-
-        throw new XmlPullParserException(
-                "Document ended before " + endTag + " end tag");
-    }
-
-    /**
-     * Read a String[] object from an XmlPullParser.  The XML data could previously have been
-     * generated by writeStringArrayXml().  The XmlPullParser must be positioned <em>after</em> the
-     * tag that begins the list.
-     *
-     * @param parser The XmlPullParser from which to read the list data.
-     * @param endTag Name of the tag that will end the list, usually "string-array".
-     * @param name An array of one string, used to return the name attribute of the list's tag.
-     * @return Returns a newly generated String[].
-     */
-    public static String[] readThisStringArrayXml(XmlPullParser parser, String endTag,
-            String[] name) throws XmlPullParserException, java.io.IOException {
-
-        parser.next();
-
-        List<String> array = new ArrayList<>();
-
-        int eventType = parser.getEventType();
-        do {
-            if (eventType == XmlPullParser.START_TAG) {
-                if (parser.getName().equals("item")) {
-                    try {
-                        array.add(parser.getAttributeValue(null, "value"));
-                    } catch (NullPointerException e) {
-                        throw new XmlPullParserException("Need value attribute in item");
-                    } catch (NumberFormatException e) {
-                        throw new XmlPullParserException("Not a number in value attribute in item");
-                    }
-                } else {
-                    throw new XmlPullParserException("Expected item tag at: " + parser.getName());
-                }
-            } else if (eventType == XmlPullParser.END_TAG) {
-                if (parser.getName().equals(endTag)) {
-                    return array.toArray(new String[0]);
-                } else if (parser.getName().equals("item")) {
-
-                } else {
-                    throw new XmlPullParserException("Expected " + endTag + " end tag at: " +
-                            parser.getName());
-                }
-            }
-            eventType = parser.next();
-        } while (eventType != XmlPullParser.END_DOCUMENT);
-
-        throw new XmlPullParserException("Document ended before " + endTag + " end tag");
-    }
-
-    private static Object readThisValueXml(XmlPullParser parser, String[] name,
-            ReadMapCallback callback, boolean arrayMap)
-            throws XmlPullParserException, java.io.IOException {
-        final String valueName = parser.getAttributeValue(null, "name");
-        final String tagName = parser.getName();
-
-        Object res;
-
-        if (tagName.equals("null")) {
-            res = null;
-        } else if (tagName.equals("string")) {
-            String value = "";
-            int eventType;
-            while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) {
-                if (eventType == XmlPullParser.END_TAG) {
-                    if (parser.getName().equals("string")) {
-                        name[0] = valueName;
-                        return value;
-                    }
-                    throw new XmlPullParserException(
-                            "Unexpected end tag in <string>: " + parser.getName());
-                } else if (eventType == XmlPullParser.TEXT) {
-                    value += parser.getText();
-                } else if (eventType == XmlPullParser.START_TAG) {
-                    throw new XmlPullParserException(
-                            "Unexpected start tag in <string>: " + parser.getName());
-                }
-            }
-            throw new XmlPullParserException(
-                    "Unexpected end of document in <string>");
-        } else if ((res = readThisPrimitiveValueXml(parser, tagName)) != null) {
-            // all work already done by readThisPrimitiveValueXml
-        } else if (tagName.equals("string-array")) {
-            res = readThisStringArrayXml(parser, "string-array", name);
-            name[0] = valueName;
-            return res;
-        } else if (tagName.equals("list")) {
-            parser.next();
-            res = readThisListXml(parser, "list", name, callback, arrayMap);
-            name[0] = valueName;
-            return res;
-        } else if (callback != null) {
-            res = callback.readThisUnknownObjectXml(parser, tagName);
-            name[0] = valueName;
-            return res;
-        } else {
-            throw new XmlPullParserException("Unknown tag: " + tagName);
-        }
-
-        // Skip through to end tag.
-        int eventType;
-        while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) {
-            if (eventType == XmlPullParser.END_TAG) {
-                if (parser.getName().equals(tagName)) {
-                    name[0] = valueName;
-                    return res;
-                }
-                throw new XmlPullParserException(
-                        "Unexpected end tag in <" + tagName + ">: " + parser.getName());
-            } else if (eventType == XmlPullParser.TEXT) {
-                throw new XmlPullParserException(
-                        "Unexpected text in <" + tagName + ">: " + parser.getName());
-            } else if (eventType == XmlPullParser.START_TAG) {
-                throw new XmlPullParserException(
-                        "Unexpected start tag in <" + tagName + ">: " + parser.getName());
-            }
-        }
-        throw new XmlPullParserException(
-                "Unexpected end of document in <" + tagName + ">");
-    }
-
-    private static final Object readThisPrimitiveValueXml(XmlPullParser parser, String tagName)
-            throws XmlPullParserException, java.io.IOException {
-        try {
-            if (tagName.equals("int")) {
-                return Integer.parseInt(parser.getAttributeValue(null, "value"));
-            } else if (tagName.equals("long")) {
-                return Long.valueOf(parser.getAttributeValue(null, "value"));
-            } else if (tagName.equals("float")) {
-                return Float.valueOf(parser.getAttributeValue(null, "value"));
-            } else if (tagName.equals("double")) {
-                return Double.valueOf(parser.getAttributeValue(null, "value"));
-            } else if (tagName.equals("boolean")) {
-                return Boolean.valueOf(parser.getAttributeValue(null, "value"));
-            } else {
-                return null;
-            }
-        } catch (NullPointerException e) {
-            throw new XmlPullParserException("Need value attribute in <" + tagName + ">");
-        } catch (NumberFormatException e) {
-            throw new XmlPullParserException(
-                    "Not a number in value attribute in <" + tagName + ">");
-        }
-    }
-
-    public interface ReadMapCallback {
-
-        /**
-         * Called from readThisMapXml when a START_TAG is not recognized. The input stream is
-         * positioned within the start tag so that attributes can be read using in.getAttribute.
-         *
-         * @param in the XML input stream
-         * @param tag the START_TAG that was not recognized.
-         * @return the Object parsed from the stream which will be put into the map.
-         * @throws XmlPullParserException if the START_TAG is not recognized.
-         * @throws IOException on XmlPullParser serialization errors.
-         */
-        Object readThisUnknownObjectXml(XmlPullParser in, String tag)
-                throws XmlPullParserException, IOException;
-    }
-}
\ No newline at end of file