diff --git a/Android/AndroidManifest.xml b/Android/AndroidManifest.xml index 20085d5f86..67cf94662f 100644 --- a/Android/AndroidManifest.xml +++ b/Android/AndroidManifest.xml @@ -15,6 +15,7 @@ + diff --git a/Android/build.gradle b/Android/build.gradle index 2e504cbb23..db4508f6d6 100644 --- a/Android/build.gradle +++ b/Android/build.gradle @@ -49,6 +49,14 @@ def versionMinor = 2 def versionPatch = 0 def versionBuild = 4 //bump for dogfood builds, public betas, etc. +//Logging levels +def logLevelVerbose = 2; +def logLevelDebug = 3; +def logLevelInfo = 4; +def logLevelWarn = 5; +def logLevelError = 6; +def logLevelAssert = 7; + android { compileSdkVersion 22 buildToolsVersion "22.0.1" @@ -59,6 +67,9 @@ android { targetSdkVersion 22 versionCode versionMajor * 100000 + versionMinor * 1000 + versionPatch * 100 + versionBuild versionName "Tower-v${versionMajor}.${versionMinor}.${versionPatch}" + + buildConfigField "boolean", "WRITE_LOG_FILE", "true" + buildConfigField "int", "LOG_FILE_LEVEL", "$logLevelDebug" } compileOptions { @@ -124,17 +135,20 @@ android { applicationIdSuffix ".beta" versionNameSuffix ".beta.${versionBuild}" resValue "string", "app_title", "Tower Beta" + debuggable true } beta { signingConfig signingConfigs.release versionNameSuffix ".beta.${versionBuild}" resValue "string", "app_title", "Tower" + debuggable true } release { signingConfig signingConfigs.release resValue "string", "app_title", "Tower" + buildConfigField "boolean", "WRITE_LOG_FILE", "false" } } } diff --git a/Android/src/org/droidplanner/android/AppService.kt b/Android/src/org/droidplanner/android/AppService.kt index d1ddf87389..8c959ca5e9 100644 --- a/Android/src/org/droidplanner/android/AppService.kt +++ b/Android/src/org/droidplanner/android/AppService.kt @@ -1,15 +1,23 @@ package org.droidplanner.android +import android.annotation.TargetApi import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest import android.os.Binder +import android.os.Build import android.support.v4.content.LocalBroadcastManager import com.o3dr.services.android.lib.drone.attribute.AttributeEvent import com.o3dr.services.android.lib.drone.attribute.AttributeEventExtra import org.droidplanner.android.notifications.NotificationHandler +import org.droidplanner.android.utils.NetworkUtils +import timber.log.Timber /** * Created by Fredia Huya-Kouadio on 9/24/15. @@ -29,10 +37,14 @@ public class AppService : Service() { } private val receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { + override fun onReceive(context: Context, intent: Intent?) { when (intent?.action) { AttributeEvent.STATE_CONNECTED -> { notificationHandler?.init() + + if(NetworkUtils.isOnSoloNetwork(context)){ + bringUpCellularNetwork(context) + } } AttributeEvent.STATE_DISCONNECTED -> { @@ -58,8 +70,13 @@ public class AppService : Service() { super.onCreate() val dpApp = application as DroidPlannerApp + dpApp.createFileStartLogging() val drone = dpApp.drone + if(NetworkUtils.isOnSoloNetwork(applicationContext)){ + bringUpCellularNetwork(applicationContext) + } + notificationHandler = NotificationHandler(applicationContext, drone) if (drone.isConnected) { @@ -74,7 +91,41 @@ public class AppService : Service() { LocalBroadcastManager.getInstance(applicationContext).unregisterReceiver(receiver) notificationHandler?.terminate() + + bringDownCellularNetwork() + + val dpApp = application as DroidPlannerApp + dpApp.closeLogFile() } override fun onBind(intent: Intent?) = binder + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private fun bringUpCellularNetwork(context: Context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + return + + Timber.i("Setting up cellular network request.") + val connMgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + val builder = NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + + connMgr.requestNetwork(builder.build(), object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + Timber.d("Setting up process default network: %s", network) + ConnectivityManager.setProcessDefaultNetwork(network) + DroidPlannerApp.setCellularNetworkAvailability(true) + } + }) + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private fun bringDownCellularNetwork() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + return + + Timber.d("Bringing down cellular network access.") + ConnectivityManager.setProcessDefaultNetwork(null) + DroidPlannerApp.setCellularNetworkAvailability(false) + } } \ No newline at end of file diff --git a/Android/src/org/droidplanner/android/DroidPlannerApp.java b/Android/src/org/droidplanner/android/DroidPlannerApp.java index b800b1e74a..4d0ffcf793 100644 --- a/Android/src/org/droidplanner/android/DroidPlannerApp.java +++ b/Android/src/org/droidplanner/android/DroidPlannerApp.java @@ -28,6 +28,7 @@ import org.droidplanner.android.activities.helpers.BluetoothDevicesActivity; import org.droidplanner.android.maps.providers.google_map.tiles.mapbox.offline.MapDownloader; import org.droidplanner.android.proxy.mission.MissionProxy; +import org.droidplanner.android.utils.LogToFileTree; import org.droidplanner.android.utils.Utils; import org.droidplanner.android.utils.analytics.GAUtils; import org.droidplanner.android.utils.file.IO.ExceptionWriter; @@ -35,6 +36,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import io.fabric.sdk.android.Fabric; import timber.log.Timber; @@ -59,6 +61,8 @@ public class DroidPlannerApp extends Application implements DroneListener, Tower public static final String ACTION_DRONE_EVENT = Utils.PACKAGE_NAME + ".ACTION_DRONE_EVENT"; public static final String EXTRA_DRONE_EVENT = "extra_drone_event"; + private static final AtomicBoolean isCellularNetworkOn = new AtomicBoolean(false); + private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -130,14 +134,12 @@ public void run() { private LocalBroadcastManager lbm; private MapDownloader mapDownloader; + private LogToFileTree logToFileTree; + @Override public void onCreate() { super.onCreate(); - if (!BuildConfig.DEBUG) { - Fabric.with(this, new Crashlytics()); - } - final Context context = getApplicationContext(); dpPrefs = new DroidPlannerPrefs(context); @@ -163,7 +165,14 @@ public void uncaughtException(Thread thread, Throwable ex) { GAUtils.startNewSession(context); if (BuildConfig.DEBUG) { - Timber.plant(new Timber.DebugTree()); + if (BuildConfig.WRITE_LOG_FILE) { + logToFileTree = new LogToFileTree(); + Timber.plant(logToFileTree); + } else { + Timber.plant(new Timber.DebugTree()); + } + } else { + Fabric.with(context, new Crashlytics()); } final IntentFilter intentFilter = new IntentFilter(); @@ -383,4 +392,24 @@ public void onDroneServiceInterrupted(String errorMsg) { if (!TextUtils.isEmpty(errorMsg)) Log.e(TAG, errorMsg); } + + public static void setCellularNetworkAvailability(boolean isAvailable){ + isCellularNetworkOn.set(isAvailable); + } + + public static boolean isCellularNetworkAvailable(){ + return isCellularNetworkOn.get(); + } + + public void createFileStartLogging() { + if (logToFileTree != null) { + logToFileTree.createFileStartLogging(getApplicationContext()); + } + } + + public void closeLogFile() { + if(logToFileTree != null) { + logToFileTree.stopLoggingThread(); + } + } } diff --git a/Android/src/org/droidplanner/android/utils/LogToFileTree.java b/Android/src/org/droidplanner/android/utils/LogToFileTree.java new file mode 100644 index 0000000000..446f1ce77e --- /dev/null +++ b/Android/src/org/droidplanner/android/utils/LogToFileTree.java @@ -0,0 +1,140 @@ +package org.droidplanner.android.utils; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.util.Log; + +import org.droidplanner.android.BuildConfig; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +import timber.log.Timber; + +/** + * Timber Tree to log specific log levels to a file + */ +public class LogToFileTree extends Timber.DebugTree { + private static final SimpleDateFormat LOG_DATE_FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US); + private static final SimpleDateFormat FILE_DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd_HH_mm", Locale.US); + + private final LinkedBlockingQueue logQueue = new LinkedBlockingQueue<>(); + private PrintStream logOutputFile; + private Thread dequeueThread; + private final AtomicBoolean isRunning = new AtomicBoolean(false); + private final Date date = new Date(); + + @Override + protected void log(int priority, String tag, String message, Throwable t) { + super.log(priority, tag, message, t); + + if (isLoggableToFile(priority)) { + String logOutput = getLogMessage(priority, tag, message); + logQueue.add(logOutput); + } + } + + private boolean isLoggableToFile(int priority) { + return priority >= BuildConfig.LOG_FILE_LEVEL; + } + + private String getLogMessage(int priority, String tag, String message) { + String priorityShort = getPriorityString(priority); + date.setTime(System.currentTimeMillis()); + return String.format("%s %s/%s : %s", LOG_DATE_FORMAT.format(date), priorityShort, tag, message); + } + + private String getPriorityString(int priority) { + String priorityString = null; + switch (priority) { + case Log.ASSERT: + priorityString = "ASSERT"; + break; + case Log.ERROR: + priorityString = "E"; + break; + case Log.WARN: + priorityString = "W"; + break; + case Log.INFO: + priorityString = "I"; + break; + case Log.DEBUG: + priorityString = "D"; + break; + case Log.VERBOSE: + priorityString = "V"; + break; + default: + priorityString = ""; + break; + } + return priorityString; + } + + public void createFileStartLogging(final Context context) { + if (dequeueThread != null && dequeueThread.isAlive()) { + stopLoggingThread(); + } + + dequeueThread = new Thread(new Runnable() { + public void run() { + PackageInfo pInfo; + String version = ""; + try { + pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + version = pInfo.versionName; + } catch (PackageManager.NameNotFoundException e) { + Timber.w("Failed to get package info"); + } + + File rootDir = context.getExternalFilesDir(null); + File dir = new File(rootDir, "/log_cat/"); + dir.mkdirs(); + + String fileName = String.format("%s_%s.log", version, FILE_DATE_FORMAT.format(new Date())); + File logFile = new File(dir, fileName); + try { + logOutputFile = new PrintStream(new FileOutputStream(logFile, true)); + + while (isRunning.get()) { + try { + String message = logQueue.take(); + logOutputFile.println(message); + } catch (InterruptedException e) { + Timber.w("Failed to receive message from logQueue"); + } + } + + } catch (IOException e) { + Timber.w("Failed to open file"); + } finally { + isRunning.set(false); + if (logOutputFile != null) { + logOutputFile.close(); + } + } + } + }); + + isRunning.set(true); + dequeueThread.start(); + } + + public void stopLoggingThread() { + if (dequeueThread != null) { + isRunning.set(false); + dequeueThread.interrupt(); + dequeueThread = null; + } + } + +} diff --git a/Android/src/org/droidplanner/android/utils/NetworkUtils.java b/Android/src/org/droidplanner/android/utils/NetworkUtils.java index ee711ae384..88d70a8555 100644 --- a/Android/src/org/droidplanner/android/utils/NetworkUtils.java +++ b/Android/src/org/droidplanner/android/utils/NetworkUtils.java @@ -21,6 +21,9 @@ * Created by Fredia Huya-Kouadio on 5/11/15. */ public class NetworkUtils { + + private static final String SOLO_LINK_WIFI_PREFIX = "SoloLink_"; + public static boolean isNetworkAvailable(Context context) { if(context == null) return false; @@ -31,6 +34,15 @@ public static boolean isNetworkAvailable(Context context) { return activeNetworkInfo != null && activeNetworkInfo.isConnected(); } + public static boolean isOnSoloNetwork(Context context) { + if (context == null){ + return false; + } + + final String connectedSSID = getCurrentWifiLink(context); + return connectedSSID != null && connectedSSID.startsWith(SOLO_LINK_WIFI_PREFIX); + } + public static String getCurrentWifiLink(Context context) { if(context == null) return null; diff --git a/Android/src/org/droidplanner/android/utils/file/DirectoryPath.java b/Android/src/org/droidplanner/android/utils/file/DirectoryPath.java index 941f9fb25e..5239c17418 100644 --- a/Android/src/org/droidplanner/android/utils/file/DirectoryPath.java +++ b/Android/src/org/droidplanner/android/utils/file/DirectoryPath.java @@ -54,4 +54,11 @@ static public String getMapsPath() { public static String getLogCatPath(Context context) { return getPrivateDataPath(context) + "/LogCat/"; } + + /** + * Storage folder for stacktraces + */ + public static String getCrashLogPath(Context context) { + return getPrivateDataPath(context) + "/crash_log/"; + } } diff --git a/Android/src/org/droidplanner/android/utils/file/FileStream.java b/Android/src/org/droidplanner/android/utils/file/FileStream.java index b31b0a6751..0d5caca717 100644 --- a/Android/src/org/droidplanner/android/utils/file/FileStream.java +++ b/Android/src/org/droidplanner/android/utils/file/FileStream.java @@ -28,7 +28,7 @@ public static String getParameterFilename(String prefix){ } public static FileOutputStream getExceptionFileStream(Context context) throws FileNotFoundException { - File myDir = new File(DirectoryPath.getLogCatPath(context)); + File myDir = new File(DirectoryPath.getCrashLogPath(context)); myDir.mkdirs(); File file = new File(myDir, getTimeStamp() + ".txt"); if (file.exists())