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);
+ }
+
+}