reader/android: AsyncTask version of the progress-bar

Use AsyncTask to load and store pdf files and display progress.
Now it can be properly cancelled in the middle of loading.

Change-Id: I73558a35070e1f1312660ab170d97a906f152071
diff --git a/android/app/build.gradle b/android/app/build.gradle
index f05cdc9..00a88cc 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -24,6 +24,9 @@
 // It's going to use VDL.
 apply plugin: 'io.v.vdl'
 
+// Retrolambda plugin
+apply plugin: 'me.tatarka.retrolambda'
+
 
 // Conditionally apply the google services plugin, depending on existence of the configuration file.
 // Also, add conditional source folders as source directories.
@@ -46,8 +49,8 @@
     buildToolsVersion "23.0.1"
 
     compileOptions {
-        sourceCompatibility JavaVersion.VERSION_1_7
-        targetCompatibility JavaVersion.VERSION_1_7
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
     }
 
     defaultConfig {
diff --git a/android/app/src/main/java/io/v/android/apps/reader/PdfViewerActivity.java b/android/app/src/main/java/io/v/android/apps/reader/PdfViewerActivity.java
index f531fd6..ac842b3 100644
--- a/android/app/src/main/java/io/v/android/apps/reader/PdfViewerActivity.java
+++ b/android/app/src/main/java/io/v/android/apps/reader/PdfViewerActivity.java
@@ -8,9 +8,8 @@
 import android.content.Intent;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
 import android.provider.OpenableColumns;
 import android.support.v4.view.GestureDetectorCompat;
 import android.util.Log;
@@ -22,14 +21,11 @@
 import android.widget.TextView;
 
 import com.google.android.gms.analytics.HitBuilders;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.concurrent.Executors;
 
 import io.v.android.apps.reader.db.DB;
 import io.v.android.apps.reader.db.DB.DBList;
@@ -38,6 +34,7 @@
 import io.v.android.apps.reader.vdl.DeviceMeta;
 import io.v.android.apps.reader.vdl.DeviceSet;
 import io.v.android.apps.reader.vdl.File;
+import io.v.v23.verror.VException;
 
 /**
  * Activity that shows the contents of the selected pdf file.
@@ -59,8 +56,7 @@
     private DBList<DeviceSet> mDeviceSets;
     private DeviceSet mCurrentDS;
 
-    private ListeningExecutorService mThreadPool;
-    private Handler mHandler;
+    private CreateAndJoinDeviceSetTask mCreateAndJoinDeviceSetTask;
 
     /**
      * Helper methods for creating an intent to start a PdfViewerActivity.
@@ -83,9 +79,6 @@
 
         setContentView(R.layout.activity_pdf_viewer);
 
-        mThreadPool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1));
-        mHandler = new Handler(Looper.getMainLooper());
-
         mPdfView = (PdfViewWrapper) findViewById(R.id.pdfview);
         mPdfView.init();
 
@@ -206,6 +199,15 @@
         }
     }
 
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+
+        if (mCreateAndJoinDeviceSetTask != null) {
+            mCreateAndJoinDeviceSetTask.cancel(true);
+        }
+    }
+
     // TODO(youngseokyoon): generalize these clone methods
     private DeviceSet cloneDeviceSet(DeviceSet ds) {
         if (ds == null) {
@@ -233,68 +235,8 @@
     }
 
     private void createAndJoinDeviceSet(final Uri fileUri) {
-        mThreadPool.submit(new Runnable() {
-            @Override
-            public void run() {
-                try {
-                    byte[] bytes = getBytesFromUri(fileUri);
-                    File file = createFile(bytes, getTitleFromUri(fileUri));
-                    final DeviceSet ds = createDeviceSet(file);
-
-                    // Join the device set from the UI thread.
-                    mHandler.post(new Runnable() {
-                        @Override
-                        public void run() {
-                            joinDeviceSet(ds);
-                        }
-                    });
-                } catch (Exception e) {
-                    Log.e(TAG, "Could not create the device set: " + e.getMessage(), e);
-
-                    // In case of an error, finish this activity and go back to the previous one.
-                    mHandler.post(new Runnable() {
-                        @Override
-                        public void run() {
-                            finish();
-                        }
-                    });
-                }
-            }
-        });
-    }
-
-    private File createFile(final byte[] bytes, final String title) throws IOException {
-        initProgress(R.string.progress_writing_pdf, bytes.length);
-
-        // Create a vdl File object representing this pdf file and put it in the db.
-        DB.FileBuilder builder;
-        try {
-            builder = getDB().getFileBuilder(title);
-        } catch (RuntimeException e) {
-            throw new IOException(e);
-        }
-
-        int cur = 0;
-        int available = bytes.length;
-        while (available > 0) {
-            int numBytes = Math.min(BLOCK_SIZE, available);
-            builder.write(bytes, cur, numBytes);
-            cur += numBytes;
-            available -= numBytes;
-
-            updateProgress(cur);
-        }
-
-        initProgress(R.string.progress_finishing_up_writing, -1);
-        File vFile = builder.build();
-
-        Log.i(TAG, "vFile created: " + vFile);
-        if (vFile == null) {
-            throw new IOException("Could not store the file content: " + title);
-        }
-        getDB().addFile(vFile);
-
-        return vFile;
+        mCreateAndJoinDeviceSetTask = new CreateAndJoinDeviceSetTask();
+        mCreateAndJoinDeviceSetTask.execute(fileUri);
     }
 
     @Override
@@ -349,8 +291,6 @@
     }
 
     private DeviceSet createDeviceSet(File vFile) {
-        initProgress(R.string.progress_creating_device_set, 1);
-
         String id = IdFactory.getRandomId();
         String fileId = vFile.getId();
         Map<String, DeviceMeta> devices = new HashMap<>();
@@ -358,14 +298,10 @@
         DeviceSet ds = new DeviceSet(id, fileId, devices);
         getDB().addDeviceSet(ds);
 
-        updateProgress(1);
-
         return ds;
     }
 
     private void joinDeviceSet(DeviceSet ds) {
-        showProgressWidgets(false);
-
         mPdfView.loadPdfFile("/file_id/" + ds.getFileId());
 
         // Create a new device meta, and update the device set with it.
@@ -397,48 +333,6 @@
         mCurrentDS = null;
     }
 
-    private byte[] getBytesFromUri(final Uri uri) throws IOException {
-        Log.i(TAG, "getBytesFromUri: " + uri.toString());
-
-        InputStream in = getContentResolver().openInputStream(uri);
-        int available = in.available();
-
-        initProgress(R.string.progress_reading_source_pdf, available);
-
-        byte[] result = new byte[available];
-        int cur = 0;
-        int bytesRead;
-
-        while ((bytesRead = in.read(result, cur, Math.min(BLOCK_SIZE, available))) != -1 &&
-                available > 0) {
-            cur += bytesRead;
-            available -= bytesRead;
-            updateProgress(cur);
-        }
-
-        in.close();
-
-        return result;
-    }
-
-    private String getTitleFromUri(Uri uri) {
-        try {
-            Cursor cursor = getContentResolver().query(uri, null, null, null, null);
-
-            int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
-            cursor.moveToFirst();
-            return cursor.getString(nameIndex);
-        } catch (Exception e) {
-            handleException(e);
-
-            if (uri != null) {
-                return uri.getLastPathSegment();
-            }
-        }
-
-        return null;
-    }
-
     private DeviceMeta getDeviceMeta() {
         return getDeviceMeta(mCurrentDS);
     }
@@ -621,13 +515,59 @@
                 requestCode, resultCode));
     }
 
-    private void initProgress(final int progressTextRes, final int maxProgress) {
-        showProgressWidgets(true);
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                mProgressText.setText(progressTextRes);
+    private void showProgressWidgets(final boolean showProgress) {
+        if (showProgress) {
+            mProgressText.setVisibility(View.VISIBLE);
+            mProgressBar.setVisibility(View.VISIBLE);
+            mPdfView.setVisibility(View.INVISIBLE);
+        } else {
+            mProgressText.setVisibility(View.INVISIBLE);
+            mProgressBar.setVisibility(View.INVISIBLE);
+            mPdfView.setVisibility(View.VISIBLE);
+        }
+    }
 
+    private class CreateAndJoinDeviceSetTask extends AsyncTask<Uri, Integer, DeviceSet> {
+
+        @Override
+        protected void onPreExecute() {
+            showProgressWidgets(true);
+        }
+
+        @Override
+        protected DeviceSet doInBackground(Uri... uris) {
+            Uri uri = null;
+            try {
+                uri = uris[0];
+                Log.i(TAG, "CreateAndJoinDeviceSetTask$doInBackground: " + uri.toString());
+
+                byte[] bytes = getBytesFromUri(uri);
+                if (isCancelled()) {
+                    return null;
+                }
+
+                File file = createFile(bytes, getTitleFromUri(uri));
+                if (isCancelled()) {
+                    return null;
+                }
+
+                publishProgress(R.string.progress_creating_device_set, -1);
+                DeviceSet ds = createDeviceSet(file);
+
+                return ds;
+            } catch (Exception e) {
+                Log.e(TAG, "Could not create the device set for uri: " + uri.toString() + ": "
+                        + e.getMessage(), e);
+                return null;
+            }
+        }
+
+        @Override
+        protected void onProgressUpdate(Integer... values) {
+            if (values.length == 2) {
+                mProgressText.setText(values[0]);
+
+                int maxProgress = values[1];
                 if (maxProgress >= 0) {
                     mProgressBar.setIndeterminate(false);
                     mProgressBar.setMax(maxProgress);
@@ -635,34 +575,110 @@
                 } else {
                     mProgressBar.setIndeterminate(true);
                 }
+            } else if (values.length == 1) {
+                mProgressBar.setProgress(values[0]);
             }
-        });
-    }
+        }
 
-    private void updateProgress(final int progress) {
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                mProgressBar.setProgress(progress);
+        @Override
+        protected void onPostExecute(DeviceSet ds) {
+            PdfViewerActivity.this.mCreateAndJoinDeviceSetTask = null;
+
+            // In case of an error, finish this activity and go back to the previous one.
+            if (ds == null) {
+                finish();
+                return;
             }
-        });
-    }
 
-    private void showProgressWidgets(final boolean showProgress) {
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                if (showProgress) {
-                    mProgressText.setVisibility(View.VISIBLE);
-                    mProgressBar.setVisibility(View.VISIBLE);
-                    mPdfView.setVisibility(View.INVISIBLE);
-                } else {
-                    mProgressText.setVisibility(View.INVISIBLE);
-                    mProgressBar.setVisibility(View.INVISIBLE);
-                    mPdfView.setVisibility(View.VISIBLE);
+            showProgressWidgets(false);
+            joinDeviceSet(ds);
+        }
+
+        private byte[] getBytesFromUri(final Uri uri) throws IOException {
+            Log.i(TAG, "getBytesFromUri: " + uri.toString());
+
+            InputStream in = getContentResolver().openInputStream(uri);
+            int total = in.available();
+            int available = total;
+
+            publishProgress(R.string.progress_reading_source_pdf, total);
+
+            byte[] result = new byte[available];
+            int cur = 0;
+            int bytesRead;
+
+            while ((bytesRead = in.read(result, cur, Math.min(BLOCK_SIZE, available))) != -1 &&
+                    available > 0) {
+                cur += bytesRead;
+                available -= bytesRead;
+
+                publishProgress(cur);
+
+                if (isCancelled()) {
+                    in.close();
+                    return null;
                 }
             }
-        });
+
+            in.close();
+
+            return result;
+        }
+
+        private File createFile(final byte[] bytes, final String title) throws Exception {
+
+            publishProgress(R.string.progress_writing_pdf, bytes.length);
+
+            // Create a vdl File object representing this pdf file and put it in the db.
+            DB.FileBuilder builder;
+            builder = getDB().getFileBuilder(title);
+
+            int cur = 0;
+            int available = bytes.length;
+
+            while (available > 0) {
+                int numBytes = Math.min(BLOCK_SIZE, available);
+                builder.write(bytes, cur, numBytes);
+                cur += numBytes;
+                available -= numBytes;
+
+                publishProgress(cur);
+
+                if (isCancelled()) {
+                    builder.cancel();
+                    return null;
+                }
+            }
+
+            publishProgress(R.string.progress_finishing_up_writing, -1);
+            File vFile = builder.build();
+
+            Log.i(TAG, "vFile created: " + vFile);
+            if (vFile == null) {
+                throw new VException("Could not store the file content: " + title);
+            }
+            getDB().addFile(vFile);
+
+            return vFile;
+        }
+
+        private String getTitleFromUri(Uri uri) {
+            try {
+                Cursor cursor = getContentResolver().query(uri, null, null, null, null);
+
+                int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+                cursor.moveToFirst();
+                return cursor.getString(nameIndex);
+            } catch (Exception e) {
+                handleException(e);
+
+                if (uri != null) {
+                    return uri.getLastPathSegment();
+                }
+            }
+
+            return null;
+        }
     }
 
 }
diff --git a/android/app/src/main/java/io/v/android/apps/reader/db/DB.java b/android/app/src/main/java/io/v/android/apps/reader/db/DB.java
index 9ddf29b..30a6ec2 100644
--- a/android/app/src/main/java/io/v/android/apps/reader/db/DB.java
+++ b/android/app/src/main/java/io/v/android/apps/reader/db/DB.java
@@ -45,6 +45,7 @@
 
     interface FileBuilder extends Closeable {
         void write(byte[] b, int off, int len) throws IOException;
+        void cancel();
         File build();
     }
 
@@ -159,7 +160,7 @@
      * @param title title of this file.
      * @return      a {@link FileBuilder} object for building the {@link File}.
      */
-    FileBuilder getFileBuilder(String title);
+    FileBuilder getFileBuilder(String title) throws Exception;
 
     /**
      * Opens an {@link InputStream} for the given file.
diff --git a/android/app/src/main/java/io/v/android/apps/reader/db/FakeDB.java b/android/app/src/main/java/io/v/android/apps/reader/db/FakeDB.java
index 687e5d7..8cea9a2 100644
--- a/android/app/src/main/java/io/v/android/apps/reader/db/FakeDB.java
+++ b/android/app/src/main/java/io/v/android/apps/reader/db/FakeDB.java
@@ -14,7 +14,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -152,14 +151,8 @@
         private long mSize;
         private ByteArrayOutputStream mOutputStream;
 
-        public FakeFileBuilder(String title) {
-            try {
-                mDigest = MessageDigest.getInstance("MD5");
-            } catch (NoSuchAlgorithmException e) {
-                Log.e(TAG, "Could not create md5 digest object: " + e.getMessage(), e);
-                throw new RuntimeException(e);
-            }
-
+        public FakeFileBuilder(String title) throws Exception {
+            mDigest = MessageDigest.getInstance("MD5");
             mTitle = title;
             mSize = 0L;
             mOutputStream = new ByteArrayOutputStream();
@@ -173,6 +166,15 @@
         }
 
         @Override
+        public void cancel() {
+            try {
+                mOutputStream.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Could not cancel the writing: " + e.getMessage(), e);
+            }
+        }
+
+        @Override
         public File build() {
             try {
                 mOutputStream.close();
@@ -257,7 +259,7 @@
     }
 
     @Override
-    public FileBuilder getFileBuilder(String title) {
+    public FileBuilder getFileBuilder(String title) throws Exception {
         return new FakeFileBuilder(title);
     }
 
diff --git a/android/app/src/main/java/io/v/android/apps/reader/db/SyncbaseDB.java b/android/app/src/main/java/io/v/android/apps/reader/db/SyncbaseDB.java
index ad3dc21..aaff89a 100644
--- a/android/app/src/main/java/io/v/android/apps/reader/db/SyncbaseDB.java
+++ b/android/app/src/main/java/io/v/android/apps/reader/db/SyncbaseDB.java
@@ -24,7 +24,6 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -491,7 +490,7 @@
     }
 
     @Override
-    public FileBuilder getFileBuilder(String title) {
+    public FileBuilder getFileBuilder(String title) throws Exception {
         return new SyncbaseFileBuilder(title);
     }
 
@@ -817,24 +816,13 @@
         private BlobWriter mBlobWriter;
         private OutputStream mOutputStream;
 
-        public SyncbaseFileBuilder(String title) {
-            try {
-                mDigest = MessageDigest.getInstance("MD5");
-            } catch (NoSuchAlgorithmException e) {
-                Log.e(TAG, "Could not create md5 digest object: " + e.getMessage(), e);
-                throw new RuntimeException(e);
-            }
-
+        public SyncbaseFileBuilder(String title) throws Exception {
+            mDigest = MessageDigest.getInstance("MD5");
             mTitle = title;
             mSize = 0L;
 
-            try {
-                mBlobWriter = sync(mLocalSB.db.writeBlob(mVContext, null));
-                mOutputStream = mBlobWriter.stream(mVContext);
-            } catch (VException e) {
-                Log.e(TAG, "Could not create SyncbaseFileBuilder: " + e.getMessage(), e);
-                throw new RuntimeException(e);
-            }
+            mBlobWriter = sync(mLocalSB.db.writeBlob(mVContext, null));
+            mOutputStream = mBlobWriter.stream(mVContext);
         }
 
         @Override
@@ -845,6 +833,16 @@
         }
 
         @Override
+        public void cancel() {
+            try {
+                mOutputStream.close();
+                mBlobWriter.delete(mVContext);
+            } catch (IOException e) {
+                Log.e(TAG, "Could not cancel the writing: " + e.getMessage(), e);
+            }
+        }
+
+        @Override
         public File build() {
             try {
                 Log.i(TAG, "build() method called.");
diff --git a/android/build.gradle b/android/build.gradle
index cfecc13..0b9fd0a 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -10,6 +10,8 @@
         // This is for sending log data to Google Analytics
         classpath 'com.google.gms:google-services:1.5.0-beta2'
 
+        classpath 'me.tatarka:gradle-retrolambda:3.2.3'
+
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
     }