reader/android: request external storage access permission on Android M+

The log creation was failing on Android M+ because the external
storage permission was not properly granted by the user.
(cf.http://developer.android.com/training/permissions/requesting.html)

This CL explicitly requests the permission, and reinitialize the
loggers once the permission is granted. The logcat code was moved to a
separate DebugUtils class, which could be pushed to the baku-toolkit
level in the future.

Change-Id: Iffc325330f8815d31649e6a8fb62969a7b7f479f
diff --git a/android/app/src/main/java/io/v/android/apps/reader/BaseReaderActivity.java b/android/app/src/main/java/io/v/android/apps/reader/BaseReaderActivity.java
index 0a82a72..9629741 100644
--- a/android/app/src/main/java/io/v/android/apps/reader/BaseReaderActivity.java
+++ b/android/app/src/main/java/io/v/android/apps/reader/BaseReaderActivity.java
@@ -4,8 +4,11 @@
 
 package io.v.android.apps.reader;
 
+import android.Manifest;
 import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
 import android.os.Bundle;
+import android.support.v4.app.ActivityCompat;
 import android.support.v4.view.GestureDetectorCompat;
 import android.view.MotionEvent;
 
@@ -16,11 +19,15 @@
 import io.v.android.apps.reader.model.DeviceInfoFactory;
 import io.v.baku.toolkit.VAppCompatActivity;
 
+import static io.v.android.apps.reader.debug.DebugUtils.startSavingLogs;
+import static io.v.baku.toolkit.debug.DebugUtils.isApkDebug;
+
 /**
  * Base activity class for all the Reader app activities. Its responsibilities include DB
  * initialization, touch gesture detection, and google analytics tracking
  */
 public abstract class BaseReaderActivity extends VAppCompatActivity {
+
     private String mDeviceId;
     private DB mDB;
     private Tracker mTracker;
@@ -76,6 +83,16 @@
     }
 
     private void initTracker() {
+        if (!Utils.hasExternalStoragePermission(this)) {
+            ActivityCompat.requestPermissions(
+                    this,
+                    new String[] {
+                            Manifest.permission.READ_EXTERNAL_STORAGE,
+                            Manifest.permission.WRITE_EXTERNAL_STORAGE
+                    },
+                    Constants.REQUEST_CODE_PERMISSION_EXTERNAL_STORAGE);
+        }
+
         // TODO(youngseokyoon): consolidate the Tracker into UserActionLogger
         mLogger = UserActionLogger.getInstance(this);
         mGestureListener = new GestureListener(this);
@@ -95,4 +112,26 @@
         return super.dispatchTouchEvent(ev);
     }
 
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String[] permissions,
+                                           int[] grantResults) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+        switch (requestCode) {
+            case Constants.REQUEST_CODE_PERMISSION_EXTERNAL_STORAGE:
+                // If the permission is granted, reinitialize the loggers.
+                if (grantResults.length > 0 &&
+                        grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                    mLogger.initPrinters();
+
+                    if (isApkDebug(this)) {
+                        startSavingLogs(this, Constants.APP_NAME);
+                    }
+                }
+                break;
+
+            default:
+                break;
+        }
+    }
 }
diff --git a/android/app/src/main/java/io/v/android/apps/reader/BaseReaderApplication.java b/android/app/src/main/java/io/v/android/apps/reader/BaseReaderApplication.java
index e9178ab..bc59efe 100644
--- a/android/app/src/main/java/io/v/android/apps/reader/BaseReaderApplication.java
+++ b/android/app/src/main/java/io/v/android/apps/reader/BaseReaderApplication.java
@@ -5,21 +5,9 @@
 package io.v.android.apps.reader;
 
 import android.app.Application;
-import android.os.Environment;
-import android.util.Log;
 
-import java.io.File;
-import java.io.FilenameFilter;
-import java.io.IOException;
-import java.text.SimpleDateFormat;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-
-import io.v.android.apps.reader.model.DeviceInfoFactory;
-import io.v.baku.toolkit.debug.DebugUtils;
+import static io.v.android.apps.reader.debug.DebugUtils.startSavingLogs;
+import static io.v.baku.toolkit.debug.DebugUtils.isApkDebug;
 
 /**
  * Base application class that contains logic which is shared whether or not the Google Analytics
@@ -30,82 +18,16 @@
  */
 public abstract class BaseReaderApplication extends Application {
 
-    private static int MAX_LOG_COUNT = 10;
-    private static final String APP_NAME = "reader";
-    private static final String TAG = BaseReaderApplication.class.getSimpleName();
-
-    private String mDeviceId;
-
     @Override
     public void onCreate() {
         super.onCreate();
 
-        mDeviceId = DeviceInfoFactory.getDeviceId(this);
-
         // Only save logcat logs in debug mode.
-        if (DebugUtils.isApkDebug(this)) {
-            startSavingLogs();
+        // It is possible that we do not have storage access permission at the moment. In that case,
+        // startSavingLogs() should be called again after the permission is granted.
+        if (isApkDebug(this)) {
+            startSavingLogs(this, Constants.APP_NAME);
         }
     }
 
-    private void startSavingLogs() {
-        // Use an app-independent files directory to avoid accidentally deleting log
-        // files by clearing the app data.
-        File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
-        if (!dir.exists()) {
-            dir.mkdirs();
-        }
-
-        Log.i(TAG, "Logcat logs are saved at: " + dir.getAbsolutePath());
-
-        deleteOldLogs(dir);
-
-        // Avoid having colons in the start timestamp
-        String startTime = getTimeString();
-
-        File logcatFile = new File(dir,
-                String.format("%s-%s.log", getLogPrefix(), startTime));
-
-        try {
-            // Clear the previous logs
-            Runtime.getRuntime().exec("logcat -c");
-            Runtime.getRuntime().exec(String.format("logcat -v time -f %s", logcatFile.getCanonicalPath()));
-        } catch (IOException e) {
-            Log.e(TAG, "Could not start writing the logcat file.", e);
-        }
-    }
-
-    private void deleteOldLogs(File dir) {
-        File[] files = dir.listFiles(new FilenameFilter() {
-            @Override
-            public boolean accept(File file, String s) {
-                return s.startsWith(getLogPrefix());
-            }
-        });
-
-        if (files == null) {
-            return;
-        }
-
-        List<File> logFiles = Arrays.asList(files);
-
-        if (logFiles.size() >= MAX_LOG_COUNT) {
-            Collections.sort(logFiles);
-            Collections.reverse(logFiles);
-            for (File oldLog : logFiles.subList(0, logFiles.size() - MAX_LOG_COUNT + 1)) {
-                oldLog.delete();
-            }
-        }
-    }
-
-    private String getLogPrefix() {
-        return String.format("%s-%s-logcat", APP_NAME, mDeviceId);
-    }
-
-    private String getTimeString() {
-        SimpleDateFormat formatter = new SimpleDateFormat(
-                "yyyyMMdd-HHmmss.SSS", Locale.getDefault());
-        return formatter.format(new Date());
-    }
-
 }
diff --git a/android/app/src/main/java/io/v/android/apps/reader/Constants.java b/android/app/src/main/java/io/v/android/apps/reader/Constants.java
index 26e27a4..975c7a9 100644
--- a/android/app/src/main/java/io/v/android/apps/reader/Constants.java
+++ b/android/app/src/main/java/io/v/android/apps/reader/Constants.java
@@ -6,6 +6,10 @@
 
 public class Constants {
 
+    public static final String APP_NAME = "reader";
     public static final String PDF_MIME_TYPE = "application/pdf";
 
+    public static final int REQUEST_CODE_SEEK_BLESSINGS = 200;
+    public static final int REQUEST_CODE_PERMISSION_EXTERNAL_STORAGE = 201;
+
 }
diff --git a/android/app/src/main/java/io/v/android/apps/reader/UserActionLogger.java b/android/app/src/main/java/io/v/android/apps/reader/UserActionLogger.java
index 15d98df..2095d4a 100644
--- a/android/app/src/main/java/io/v/android/apps/reader/UserActionLogger.java
+++ b/android/app/src/main/java/io/v/android/apps/reader/UserActionLogger.java
@@ -5,7 +5,6 @@
 package io.v.android.apps.reader;
 
 import android.content.Context;
-import android.os.Environment;
 import android.util.Log;
 
 import org.apache.commons.csv.CSVFormat;
@@ -15,9 +14,6 @@
 import java.io.File;
 import java.io.FileWriter;
 import java.io.IOException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
 
 import io.v.android.apps.reader.model.DeviceInfoFactory;
 
@@ -59,19 +55,17 @@
     private UserActionLogger(Context context) {
         mDeviceId = DeviceInfoFactory.getDeviceId(context);
 
-        // Use an app-independent files directory to avoid accidentally deleting log
-        // files by clearing the app data.
-        File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
-        if (!dir.exists()) {
-            dir.mkdirs();
+        if (Utils.hasExternalStoragePermission(context)) {
+            initPrinters();
         }
+    }
+
+    public void initPrinters() {
+        File dir = Utils.getLogDirectory();
 
         Log.i(TAG, "User action logs are saved at: " + dir.getAbsolutePath());
 
-        // Avoid having colons in the start timestamp
-        SimpleDateFormat formatter = new SimpleDateFormat(
-                "yyyyMMdd-HHmmss.SSS", Locale.getDefault());
-        String startTime = formatter.format(new Date());
+        String startTime = Utils.getTimeString();
 
         File touchLogFile = new File(dir,
                 String.format("reader-%s-touch-%s.log", mDeviceId, startTime));
diff --git a/android/app/src/main/java/io/v/android/apps/reader/Utils.java b/android/app/src/main/java/io/v/android/apps/reader/Utils.java
new file mode 100644
index 0000000..ae9d2cd
--- /dev/null
+++ b/android/app/src/main/java/io/v/android/apps/reader/Utils.java
@@ -0,0 +1,57 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.android.apps.reader;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Environment;
+import android.support.v4.content.ContextCompat;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Utility class which contains useful static methods.
+ */
+public class Utils {
+
+    /**
+     * Checks if the app has read and write permission to the external storage.
+     */
+    public static boolean hasExternalStoragePermission(Context context) {
+        boolean hasWritePermission = ContextCompat.checkSelfPermission(context,
+                Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
+        boolean hasReadPermission = ContextCompat.checkSelfPermission(context,
+                Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
+
+        return hasWritePermission && hasReadPermission;
+    }
+
+    /**
+     * Returns an app-independent files directory for saving log files, to avoid accidentally
+     * deleting log files by clearing the app data.
+     */
+    public static File getLogDirectory() {
+        File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
+        if (!dir.exists()) {
+            dir.mkdirs();
+        }
+        return dir;
+    }
+
+    /**
+     * Returns the current time as string formatted as "yyyyMMdd-HHmmss.SSS",
+     * which can be used in log filenames.
+     */
+    public static String getTimeString() {
+        SimpleDateFormat formatter = new SimpleDateFormat(
+                "yyyyMMdd-HHmmss.SSS", Locale.getDefault());
+        return formatter.format(new Date());
+    }
+
+}
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 36c030f..6a8c650 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
@@ -86,12 +86,6 @@
 
     private static final String TAG = SyncbaseDB.class.getSimpleName();
 
-    /**
-     * The intent result code for when we get blessings from the account manager.
-     * The value must not conflict with any other blessing result codes.
-     */
-    private static final int BLESSING_REQUEST = 200;
-
     // TODO(youngseokyoon): change this back to the domain name, once the dns issue is resolved.
     private static final String GLOBAL_MOUNT_TABLE = "/104.197.5.136:8101";
 
@@ -190,12 +184,13 @@
 
     private void refreshBlessings(Activity activity) {
         Intent intent = BlessingService.newBlessingIntent(mContext);
-        activity.startActivityForResult(intent, BLESSING_REQUEST);
+        activity.startActivityForResult(intent,
+                io.v.android.apps.reader.Constants.REQUEST_CODE_SEEK_BLESSINGS);
     }
 
     @Override
     public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
-        if (requestCode == BLESSING_REQUEST) {
+        if (requestCode == io.v.android.apps.reader.Constants.REQUEST_CODE_SEEK_BLESSINGS) {
             try {
                 byte[] blessingsVom = BlessingService.extractBlessingReply(resultCode, data);
                 Blessings blessings = (Blessings) VomUtil.decode(blessingsVom, Blessings.class);
diff --git a/android/app/src/main/java/io/v/android/apps/reader/debug/DebugUtils.java b/android/app/src/main/java/io/v/android/apps/reader/debug/DebugUtils.java
new file mode 100644
index 0000000..78d9a82
--- /dev/null
+++ b/android/app/src/main/java/io/v/android/apps/reader/debug/DebugUtils.java
@@ -0,0 +1,94 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.android.apps.reader.debug;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import io.v.android.apps.reader.Utils;
+import io.v.android.apps.reader.model.DeviceInfoFactory;
+
+/**
+ * Utility class which contains debug related features.
+ */
+public class DebugUtils {
+
+    private static final int MAX_LOG_COUNT = 10;
+    private static final String TAG = DebugUtils.class.getSimpleName();
+
+    private static boolean isSavingLogs = false;
+
+    /**
+     * Start saving the logcat logs to a file in the external storage.
+     * The app needs to obtain the read/write permission to the external storage before calling this
+     * method. If not, this method will do nothing.
+     */
+    public static void startSavingLogs(Context context, String appName) {
+        if (isSavingLogs) {
+            return;
+        }
+
+        // Stop if we don't have enough storage access permission.
+        if (!Utils.hasExternalStoragePermission(context)) {
+            return;
+        }
+
+        File dir = Utils.getLogDirectory();
+
+        Log.i(TAG, "Logcat logs are saved at: " + dir.getAbsolutePath());
+
+        String startTime = Utils.getTimeString();
+        String deviceId = DeviceInfoFactory.getDeviceId(context);
+
+        deleteOldLogs(dir, appName, deviceId);
+
+        File logcatFile = new File(dir,
+                String.format("%s-%s.log", getLogPrefix(appName, deviceId), startTime));
+
+        try {
+            // Clear the previous logs
+            Runtime.getRuntime().exec("logcat -c");
+            Runtime.getRuntime().exec(String.format("logcat -v time -f %s", logcatFile.getCanonicalPath()));
+            isSavingLogs = true;
+        } catch (IOException e) {
+            Log.e(TAG, "Could not start writing the logcat file.", e);
+        }
+    }
+
+    private static void deleteOldLogs(File dir, final String appName, final String deviceId) {
+        File[] files = dir.listFiles(new FilenameFilter() {
+            @Override
+            public boolean accept(File file, String s) {
+                return s.startsWith(getLogPrefix(appName, deviceId));
+            }
+        });
+
+        if (files == null) {
+            return;
+        }
+
+        List<File> logFiles = Arrays.asList(files);
+
+        if (logFiles.size() >= MAX_LOG_COUNT) {
+            Collections.sort(logFiles);
+            Collections.reverse(logFiles);
+            for (File oldLog : logFiles.subList(0, logFiles.size() - MAX_LOG_COUNT + 1)) {
+                oldLog.delete();
+            }
+        }
+    }
+
+    private static String getLogPrefix(String appName, String deviceId) {
+        return String.format("%s-%s-logcat", appName, deviceId);
+    }
+
+}