Fetch voicemail payload, which is usually the audio file.

When the user clicks on a voicemail, dialer sends out an broadcast intent
called "FETCH_VOICEMAIL". Any receiver that receives this broadcast is
in charge of
1) determining whether the voicemail belongs to them
2) downloading the audio file
3) writing the audio file to the voicemail provider and flipping the bit
on "has_content" for the voicemail's row in the calls table

Also fix some indents in the manifest file.

Bug: 19236241
Change-Id: I081cce569c68b52c654f0737fb29fb9c7ef8bf00
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 62bc4dc..ef7a812 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -624,9 +624,9 @@
             </intent-filter>
         </receiver>
         <service
-                android:name="com.android.phone.vvm.omtp.OmtpVvmSyncService"
-                android:exported="true"
-                android:process=":sync">
+            android:name="com.android.phone.vvm.omtp.OmtpVvmSyncService"
+            android:exported="true"
+            android:process=":sync">
             <intent-filter>
                 <action android:name="android.content.SyncAdapter"/>
             </intent-filter>
@@ -635,18 +635,29 @@
         </service>
         <service
             android:name="android.telecom.AuthenticatorService">
-        <intent-filter>
-            <action android:name="android.accounts.AccountAuthenticator"/>
-        </intent-filter>
-        <meta-data
-            android:name="android.accounts.AccountAuthenticator"
-            android:resource="@xml/authenticator" />
+            <intent-filter>
+                <action android:name="android.accounts.AccountAuthenticator"/>
+            </intent-filter>
+            <meta-data
+                android:name="android.accounts.AccountAuthenticator"
+                android:resource="@xml/authenticator" />
        </service>
-        <receiver android:name="com.android.phone.vvm.omtp.SimChangeReceiver"
+       <receiver android:name="com.android.phone.vvm.omtp.SimChangeReceiver"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.SIM_STATE_CHANGED" />
             </intent-filter>
-        </receiver>
+       </receiver>
+       <receiver
+           android:name="com.android.phone.vvm.omtp.sync.FetchVoicemailReceiver"
+           android:exported="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>
     </application>
 </manifest>
diff --git a/src/com/android/phone/common/mail/store/ImapFolder.java b/src/com/android/phone/common/mail/store/ImapFolder.java
index 5e9bc6b..faeb93f 100644
--- a/src/com/android/phone/common/mail/store/ImapFolder.java
+++ b/src/com/android/phone/common/mail/store/ImapFolder.java
@@ -92,7 +92,6 @@
      */
     public interface MessageRetrievalListener {
         public void messageRetrieved(Message message);
-        public void loadAttachmentProgress(int progress);
     }
 
     private void destroyResponses() {
@@ -457,14 +456,6 @@
             while (-1 != (n = in.read(buffer))) {
                 out.write(buffer, 0, n);
                 count += n;
-                if (listener != null) {
-                    if (size == 0) {
-                        // We don't know how big the file is, so just fake it.
-                        listener.loadAttachmentProgress((int)Math.ceil(100 * (1-1.0/count)));
-                    } else {
-                        listener.loadAttachmentProgress(count * 100 / size);
-                    }
-                }
             }
         } catch (Base64DataException bde) {
             String warning = "\n\n" + context.getString(R.string.message_decode_error);
diff --git a/src/com/android/phone/vvm/omtp/OmtpVvmSyncService.java b/src/com/android/phone/vvm/omtp/OmtpVvmSyncService.java
index edc9bec..fc444ba 100644
--- a/src/com/android/phone/vvm/omtp/OmtpVvmSyncService.java
+++ b/src/com/android/phone/vvm/omtp/OmtpVvmSyncService.java
@@ -167,7 +167,8 @@
         public int markReadInDatabase(List<Voicemail> voicemails) {
             int count = voicemails.size();
             for (int i = 0; i < count; i++) {
-                Uri uri = ContentUris.withAppendedId(Voicemails.CONTENT_URI,
+                Uri uri = ContentUris.withAppendedId(
+                        VoicemailContract.Voicemails.buildSourceUri(mContext.getPackageName()),
                         voicemails.get(i).getId());
                 mContentResolver.update(uri, new ContentValues(), null, null);
             }
diff --git a/src/com/android/phone/vvm/omtp/imap/ImapHelper.java b/src/com/android/phone/vvm/omtp/imap/ImapHelper.java
index 22b919b..3a260d7 100644
--- a/src/com/android/phone/vvm/omtp/imap/ImapHelper.java
+++ b/src/com/android/phone/vvm/omtp/imap/ImapHelper.java
@@ -20,7 +20,10 @@
 import android.content.Context;
 import android.telecom.Voicemail;
 
+import android.util.Base64;
+
 import com.android.phone.common.mail.Address;
+import com.android.phone.common.mail.Body;
 import com.android.phone.common.mail.BodyPart;
 import com.android.phone.common.mail.FetchProfile;
 import com.android.phone.common.mail.Flag;
@@ -34,7 +37,13 @@
 import com.android.phone.common.mail.store.imap.ImapConstants;
 import com.android.phone.common.mail.utils.LogUtils;
 import com.android.phone.vvm.omtp.OmtpConstants;
+import com.android.phone.vvm.omtp.sync.VoicemailFetchedCallback;
 
+import libcore.io.IoUtils;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -63,7 +72,7 @@
             // TODO: determine the security protocol (e.g. ssl, tls, none, etc.)
             mImapStore = new ImapStore(
                     context, username, password, port, serverName,
-                    ImapStore.FLAG_TLS);
+                    ImapStore.FLAG_NONE);
         } catch (NumberFormatException e) {
             LogUtils.e(TAG, e, "Could not parse port number");
         }
@@ -160,6 +169,42 @@
         return listener.getVoicemail();
     }
 
+
+    public void fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid) {
+        Message message;
+        try {
+            mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+            if (mFolder == null) {
+                // This means we were unable to successfully open the folder.
+                return;
+            }
+            message = mFolder.getMessage(uid);
+            VoicemailPayload voicemailPayload = fetchVoicemailPayload(message);
+            callback.setVoicemailContent(voicemailPayload);
+        } catch (MessagingException e) {
+        } finally {
+            closeImapFolder();
+        }
+    }
+
+    /**
+     * 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();
+    }
+
     /**
      * Listener for the message structure being fetched.
      */
@@ -190,9 +235,6 @@
             }
         }
 
-        @Override
-        public void loadAttachmentProgress(int progress) {}
-
         /**
          * Convert an IMAP message to a voicemail object.
          *
@@ -205,14 +247,11 @@
                 LogUtils.w(TAG, "Ignored non multi-part message");
                 return null;
             }
+
             Multipart multipart = (Multipart) message.getBody();
-
-            LogUtils.d(TAG, "Num body parts: " + multipart.getCount());
-
             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/")) {
@@ -256,6 +295,60 @@
         }
     }
 
+    /**
+     * 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();
+            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/")) {
+                    byte[] bytes = getAudioDataFromBody(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");
+            return null;
+        }
+
+        private byte[] getAudioDataFromBody(Body body) throws IOException, MessagingException {
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            BufferedOutputStream bufferedOut = new BufferedOutputStream(out);
+            try {
+                body.writeTo(bufferedOut);
+            } finally {
+                IoUtils.closeQuietly(bufferedOut);
+            }
+            return Base64.decode(out.toByteArray(), Base64.DEFAULT);
+        }
+    }
+
     private ImapFolder openImapFolder(String modeReadWrite) {
         try {
             ImapFolder folder = new ImapFolder(mImapStore, ImapConstants.INBOX);
diff --git a/src/com/android/phone/vvm/omtp/imap/VoicemailPayload.java b/src/com/android/phone/vvm/omtp/imap/VoicemailPayload.java
new file mode 100644
index 0000000..0ffa018
--- /dev/null
+++ b/src/com/android/phone/vvm/omtp/imap/VoicemailPayload.java
@@ -0,0 +1,38 @@
+/*
+ * 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.phone.vvm.omtp.imap;
+
+/**
+ * The payload for a voicemail, usually audio data.
+ */
+public class VoicemailPayload {
+    private final String mMimeType;
+    private final byte[] mBytes;
+
+    public VoicemailPayload(String mimeType, byte[] bytes) {
+        mMimeType = mimeType;
+        mBytes = bytes;
+    }
+
+    public byte[] getBytes() {
+        return mBytes;
+    }
+
+    public String getMimeType() {
+        return mMimeType;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/vvm/omtp/sync/DirtyVoicemailQuery.java b/src/com/android/phone/vvm/omtp/sync/DirtyVoicemailQuery.java
index 965b36b..8c70372 100644
--- a/src/com/android/phone/vvm/omtp/sync/DirtyVoicemailQuery.java
+++ b/src/com/android/phone/vvm/omtp/sync/DirtyVoicemailQuery.java
@@ -48,7 +48,7 @@
      */
     public static Cursor getDirtyVoicemails(Context context) {
         ContentResolver contentResolver = context.getContentResolver();
-        Uri statusUri = VoicemailContract.Voicemails.buildSourceUri(context.getPackageName());
-        return contentResolver.query(statusUri, PROJECTION, SELECTION, null, null);
+        Uri sourceUri = VoicemailContract.Voicemails.buildSourceUri(context.getPackageName());
+        return contentResolver.query(sourceUri, PROJECTION, SELECTION, null, null);
     }
 }
diff --git a/src/com/android/phone/vvm/omtp/sync/FetchVoicemailReceiver.java b/src/com/android/phone/vvm/omtp/sync/FetchVoicemailReceiver.java
new file mode 100644
index 0000000..66fed16
--- /dev/null
+++ b/src/com/android/phone/vvm/omtp/sync/FetchVoicemailReceiver.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.phone.vvm.omtp.sync;
+
+import android.accounts.Account;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.phone.vvm.omtp.OmtpVvmSyncAccountManager;
+import com.android.phone.vvm.omtp.imap.ImapHelper;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+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
+    };
+
+    public static final int SOURCE_DATA = 0;
+    public static final int PHONE_ACCOUNT_ID = 1;
+
+    private ContentResolver mContentResolver;
+    private Uri mUri;
+
+    @Override
+    public void onReceive(final Context context, Intent intent) {
+        if (VoicemailContract.ACTION_FETCH_VOICEMAIL.equals(intent.getAction())) {
+            mContentResolver = context.getContentResolver();
+            mUri = intent.getData();
+
+            if (mUri == null) {
+                Log.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.
+                return;
+            }
+
+            Cursor cursor = mContentResolver.query(mUri, PROJECTION, null, null, null);
+            if (cursor == null) {
+                return;
+            }
+            try {
+                if (cursor.moveToFirst()) {
+                    final String uid = 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)) {
+                            Log.e(TAG, "Account null and no default sim found.");
+                            return;
+                        }
+                    }
+                    final Account account = new Account(accountId,
+                            OmtpVvmSyncAccountManager.ACCOUNT_TYPE);
+                    Executor executor = Executors.newCachedThreadPool();
+                    executor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            new ImapHelper(context, account).fetchVoicemailPayload(
+                                    new VoicemailFetchedCallback(context, mUri), uid);
+                        }
+                    });
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+    }
+}
diff --git a/src/com/android/phone/vvm/omtp/sync/VoicemailFetchedCallback.java b/src/com/android/phone/vvm/omtp/sync/VoicemailFetchedCallback.java
new file mode 100644
index 0000000..006e83f
--- /dev/null
+++ b/src/com/android/phone/vvm/omtp/sync/VoicemailFetchedCallback.java
@@ -0,0 +1,78 @@
+/*
+ * 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.phone.vvm.omtp.sync;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.VoicemailContract.Voicemails;
+import android.util.Log;
+
+import com.android.phone.vvm.omtp.imap.VoicemailPayload;
+
+import libcore.io.IoUtils;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * 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 ContentResolver mContentResolver;
+    private Uri mUri;
+
+    VoicemailFetchedCallback(Context context, Uri uri) {
+        mContentResolver = context.getContentResolver();
+        mUri = uri;
+    }
+
+    /**
+     * 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(VoicemailPayload voicemailPayload) {
+        Log.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) {
+            Log.e(TAG, "Error writing to file: ", e);
+        } 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);
+        int updatedCount = mContentResolver.update(mUri, values, null, null);
+        if (updatedCount != 1) {
+            Log.e(TAG, "Updating voicemail should have updated 1 row, was: " + updatedCount);
+        }
+    }
+}