diff --git a/library/.gitignore b/library/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/library/.gitignore @@ -0,0 +1 @@ +/build diff --git a/library/build.gradle b/library/build.gradle new file mode 100644 index 0000000..0d88d0d --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,23 @@ +apply plugin: 'com.android.library' +apply plugin: 'com.github.dcendents.android-maven' +group = 'com.github.mobile-dev-pro' + +android { + compileSdkVersion rootProject.compileSdkVersion + + defaultConfig { + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + } + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion" +} diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro new file mode 100644 index 0000000..ff93dc0 --- /dev/null +++ b/library/proguard-rules.pro @@ -0,0 +1,33 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /home/cdv/Dev/Tools/android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +#these classes should be available as public +-keep class com.mobiledevpro.remotelogcat.RemoteLog { + public ; + } +-keep class com.mobiledevpro.remotelogcat.UserInfoModel { + public ; + } \ No newline at end of file diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..cbeecee --- /dev/null +++ b/library/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/library/src/main/java/com/mobiledevpro/remotelogcat/AppInfoModel.java b/library/src/main/java/com/mobiledevpro/remotelogcat/AppInfoModel.java new file mode 100644 index 0000000..84f7ed7 --- /dev/null +++ b/library/src/main/java/com/mobiledevpro/remotelogcat/AppInfoModel.java @@ -0,0 +1,39 @@ +package com.mobiledevpro.remotelogcat; + +/** + * Mdel for app info + *

+ * Created by Dmitriy V. Chernysh on 25.09.17. + * dmitriy.chernysh@gmail.com + *

+ * https://fb.me/mobiledevpro/ + *

+ * #MobileDevPro + */ + +class AppInfoModel { + private String name; + private String version; + private int build; + + /** + * Constructor for getting entry from DB + */ + AppInfoModel(String name, String version, int build) { + this.name = name; + this.version = version; + this.build = build; + } + + String getName() { + return name; + } + + String getVersion() { + return version; + } + + int getBuild() { + return build; + } +} diff --git a/library/src/main/java/com/mobiledevpro/remotelogcat/AsyncTaskCompat.java b/library/src/main/java/com/mobiledevpro/remotelogcat/AsyncTaskCompat.java new file mode 100644 index 0000000..bec5a78 --- /dev/null +++ b/library/src/main/java/com/mobiledevpro/remotelogcat/AsyncTaskCompat.java @@ -0,0 +1,38 @@ +package com.mobiledevpro.remotelogcat; + +import android.os.AsyncTask; + +/** + * Helper for accessing features in {@link AsyncTask} + * introduced after API level 4 in a backwards compatible fashion. + * + * This class has been removed from support library version 26+ + *

+ * Created by Dmitriy V. Chernysh on 07.03.18. + *

+ * https://fb.com/mobiledevpro/ + * https://github.com/dmitriy-chernysh + * #MobileDevPro + */ + +class AsyncTaskCompat { + /** + * Executes the task with the specified parameters, allowing multiple tasks to run in parallel + * on a pool of threads managed by {@link AsyncTask}. + * + * @param task The {@link AsyncTask} to execute. + * @param params The parameters of the task. + * @return the instance of AsyncTask. + */ + static AsyncTask executeParallel( + AsyncTask task, Params... params) { + if (task == null) { + throw new IllegalArgumentException("task can not be null"); + } + + // From API 11 onwards, we need to manually select the THREAD_POOL_EXECUTOR + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params); + + return task; + } +} diff --git a/library/src/main/java/com/mobiledevpro/remotelogcat/Constants.java b/library/src/main/java/com/mobiledevpro/remotelogcat/Constants.java new file mode 100644 index 0000000..b0d7780 --- /dev/null +++ b/library/src/main/java/com/mobiledevpro/remotelogcat/Constants.java @@ -0,0 +1,48 @@ +package com.mobiledevpro.remotelogcat; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +/** + * Class for storing constant values + *

+ * Created by Dmitriy V. Chernysh on 23.09.17. + * dmitriy.chernysh@gmail.com + *

+ * https://fb.me/mobiledevpro/ + *

+ * #MobileDevPro + */ + +class Constants { + static final int LOG_LEVEL_DEBUG = 1; + static final int LOG_LEVEL_ERROR = 2; + static final String LOG_TAG = "remote-logcat"; + + private static final String LOG_LEVEL_DEBUG_TXT = "debug"; + private static final String LOG_LEVEL_ERROR_TXT = "error"; + + static String getLogLevelTxt(int logLevel) { + switch (logLevel) { + case LOG_LEVEL_DEBUG: + return LOG_LEVEL_DEBUG_TXT; + case LOG_LEVEL_ERROR: + return LOG_LEVEL_ERROR_TXT; + default: + return String.valueOf(logLevel); + } + } + + /** + * Method for checking network connection + * + * @param context - application context + * @return true - device online + */ + static boolean isDeviceOnline(Context context) { + ConnectivityManager connMngr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo netInfo = connMngr.getActiveNetworkInfo(); + return (netInfo != null && netInfo.isConnected()); + } +} diff --git a/library/src/main/java/com/mobiledevpro/remotelogcat/DBContract.java b/library/src/main/java/com/mobiledevpro/remotelogcat/DBContract.java new file mode 100644 index 0000000..94d7528 --- /dev/null +++ b/library/src/main/java/com/mobiledevpro/remotelogcat/DBContract.java @@ -0,0 +1,270 @@ +package com.mobiledevpro.remotelogcat; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.provider.BaseColumns; +import android.util.Log; + +/** + * Contract for local database + *

+ * Created by Dmitriy V. Chernysh on 23.09.17. + * dmitriy.chernysh@gmail.com + *

+ * https://fb.me/mobiledevpro/ + *

+ * #MobileDevPro + */ + +class DBContract { + + private DBContract() { + } + + static void onCreate(SQLiteDatabase db) { + db.execSQL(Table.SQL_CREATE_TABLE); + } + + static void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + } + + + /*** + * Query all entries + * + */ + static Cursor selectEntriesToSend(SQLiteDatabase db) { + return db.query( + Table.TABLE_NAME, + Table.QUERY_PROJECTION, + "CAST (" + Table.COLUMN_IS_SENDING + " as TEXT) = ?", + new String[]{"0"}, + null, + null, + Table.COLUMN_DATETIME + " ASC", + null + ); + } + + + /** + * Check if entry exists by id + */ + static boolean isEntryExists(SQLiteDatabase db, int logEntryId) { + Cursor cursor; + int rowCount = 0; + + cursor = db.query( + Table.TABLE_NAME, + new String[]{Table._ID}, + "CAST (" + Table._ID + " as TEXT) = ?", + new String[]{String.valueOf(logEntryId)}, + null, + null, + null, + null + ); + + try { + rowCount = cursor.getCount(); + } catch (Exception e) { + Log.e(Constants.LOG_TAG, "DBContract.isEntryExists: EXCEPTION - " + e.getMessage(), e); + } finally { + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + } + return rowCount > 0; + } + + static boolean updateEntriesStatus(SQLiteDatabase db, int[] ids, boolean isSending) { + int result = 0; + db.beginTransaction(); + ContentValues cv = new ContentValues(); + cv.put(Table.COLUMN_IS_SENDING, isSending ? 1 : 0); + + try { + for (int id : ids) { + result = result + db.update( + Table.TABLE_NAME, + cv, + "CAST(" + Table._ID + " as TEXT) = ?", + new String[]{String.valueOf(id)} + ); + } + db.setTransactionSuccessful(); + } catch (Exception e) { + Log.e(Constants.LOG_TAG, "DBContract.updateEntriesStatus: exception - " + e.getMessage(), e); + result = 0; + } finally { + db.endTransaction(); + } + + return result > 0; + } + + /** + * Insert a new log entry + */ + static boolean insert(SQLiteDatabase db, LogEntryModel logEntry) { + if (logEntry == null) return false; + long insertedRowId = -1; + + ContentValues cv = new ContentValues(); + cv.put(Table.COLUMN_DATETIME, logEntry.getDateTime()); + cv.put(Table.COLUMN_LOG_LEVEL, logEntry.getLogLevel()); + cv.put(Table.COLUMN_LOG_TAG, logEntry.getLogTag()); + cv.put(Table.COLUMN_LOG_MSG, logEntry.getLogMsg()); + UserInfoModel userInfo = logEntry.getAppUserInfo(); + if (userInfo != null) { + cv.put(Table.COLUMN_APP_USER, userInfo.getString()); + } + + AppInfoModel appInfo = logEntry.getAppInfo(); + if (appInfo != null) { + cv.put(Table.COLUMN_APP_NAME, appInfo.getName()); + cv.put(Table.COLUMN_APP_VERSION, appInfo.getVersion()); + cv.put(Table.COLUMN_APP_BUILD, appInfo.getBuild()); + } + + cv.put(Table.COLUMN_IS_SENDING, 0); + + db.beginTransaction(); + try { + insertedRowId = db.insert( + Table.TABLE_NAME, + null, + cv + ); + db.setTransactionSuccessful(); + } catch (Exception e) { + Log.e(Constants.LOG_TAG, "DBContract.insert: EXCEPTION - " + e.getLocalizedMessage(), e); + } finally { + db.endTransaction(); + } + + return insertedRowId > -1; + } + + /** + * Delete log entry by Id + * + * @param db SQLiteDatabase + * @param logEntryId Log entry id + */ + static boolean delete(SQLiteDatabase db, int logEntryId) { + long result = 0; + db.beginTransaction(); + try { + result = db.delete( + Table.TABLE_NAME, + "CAST (" + Table._ID + " as TEXT) =?", + new String[]{String.valueOf(logEntryId)} + ); + + db.setTransactionSuccessful(); + } catch (Exception e) { + Log.e(Constants.LOG_TAG, "DBContract.delete: exception - " + e.getLocalizedMessage(), e); + } finally { + db.endTransaction(); + } + + return result > 0; + } + + /** + * Delete log entry by Id + * + * @param db SQLiteDatabase + * @param logEntryIds Log entry ids + */ + static boolean delete(SQLiteDatabase db, int[] logEntryIds) { + long result = 0; + db.beginTransaction(); + try { + for (int id : logEntryIds) { + result = result + db.delete( + Table.TABLE_NAME, + "CAST (" + Table._ID + " as TEXT) =?", + new String[]{String.valueOf(id)} + ); + } + db.setTransactionSuccessful(); + } catch (Exception e) { + Log.e(Constants.LOG_TAG, "DBContract.delete: exception - " + e.getLocalizedMessage(), e); + } finally { + db.endTransaction(); + } + + return result > 0; + } + + + /** + * Convert cursor data to LogEntry model + * + * @param cursor Cursor + * @return LogEntryModel + */ + static LogEntryModel getLogEntryFromCursor(Cursor cursor) { + if (cursor == null) return null; + return new LogEntryModel( + cursor.getInt(cursor.getColumnIndex(Table._ID)), + cursor.getLong(cursor.getColumnIndex(Table.COLUMN_DATETIME)), + cursor.getInt(cursor.getColumnIndex(Table.COLUMN_LOG_LEVEL)), + cursor.getString(cursor.getColumnIndex(Table.COLUMN_LOG_TAG)), + cursor.getString(cursor.getColumnIndex(Table.COLUMN_LOG_MSG)), + new AppInfoModel( + cursor.getString(cursor.getColumnIndex(Table.COLUMN_APP_NAME)), + cursor.getString(cursor.getColumnIndex(Table.COLUMN_APP_VERSION)), + cursor.getInt(cursor.getColumnIndex(Table.COLUMN_APP_BUILD)) + ), + new UserInfoModel().parseUserInfo(cursor.getString(cursor.getColumnIndex(Table.COLUMN_APP_USER))) + ); + } + + private static class Table implements BaseColumns { + private static final String TABLE_NAME = "logs"; + private static final String COLUMN_DATETIME = "datetime"; //in milliseconds + private static final String COLUMN_LOG_LEVEL = "log_level"; //see Constants class + private static final String COLUMN_LOG_TAG = "log_tag"; + private static final String COLUMN_LOG_MSG = "log_msg"; + private static final String COLUMN_APP_NAME = "app_name"; + private static final String COLUMN_APP_VERSION = "app_version"; + private static final String COLUMN_APP_BUILD = "app_build"; + private static final String COLUMN_APP_USER = "app_user"; //some data about user (divider - ";") + private static final String COLUMN_IS_SENDING = "is_sending"; //indicate that the entry in already is sending to server + + //table create sql + private static final String SQL_CREATE_TABLE = "create table IF NOT EXISTS " + + TABLE_NAME + + "(" + + _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + COLUMN_DATETIME + " INTEGER, " + + COLUMN_LOG_LEVEL + " INTEGER, " + + COLUMN_LOG_TAG + " TEXT, " + + COLUMN_LOG_MSG + " TEXT, " + + COLUMN_APP_NAME + " TEXT, " + + COLUMN_APP_VERSION + " TEXT, " + + COLUMN_APP_BUILD + " INTEGER, " + + COLUMN_APP_USER + " TEXT, " + + COLUMN_IS_SENDING + " INTEGER " + + ");"; + + private static final String[] QUERY_PROJECTION = { + _ID, + COLUMN_DATETIME, + COLUMN_LOG_LEVEL, + COLUMN_LOG_TAG, + COLUMN_LOG_MSG, + COLUMN_APP_NAME, + COLUMN_APP_VERSION, + COLUMN_APP_BUILD, + COLUMN_APP_USER, + COLUMN_IS_SENDING + }; + } + + +} diff --git a/library/src/main/java/com/mobiledevpro/remotelogcat/DBHelper.java b/library/src/main/java/com/mobiledevpro/remotelogcat/DBHelper.java new file mode 100644 index 0000000..e9ad09b --- /dev/null +++ b/library/src/main/java/com/mobiledevpro/remotelogcat/DBHelper.java @@ -0,0 +1,107 @@ +package com.mobiledevpro.remotelogcat; + +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import java.util.ArrayList; + +/** + * SQLite database helper + *

+ * Created by Dmitriy V. Chernysh on 23.09.17. + * dmitriy.chernysh@gmail.com + *

+ * https://fb.me/mobiledevpro/ + *

+ * #MobileDevPro + */ + +class DBHelper extends SQLiteOpenHelper implements IDBHelper { + private static final String DB_NAME = "logs"; + private static final int DB_VERSION = 2; + + private static DBHelper sDBHelperInstance; + + private DBHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + /** + * Get DBHelper instance + * + * @return DBHelper instance + */ + static synchronized DBHelper getInstance(Context appContext) { + if (sDBHelperInstance == null) { + sDBHelperInstance = new DBHelper(appContext); + } + return sDBHelperInstance; + } + + @Override + public void onCreate(SQLiteDatabase db) { + DBContract.onCreate(db); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + DBContract.onUpgrade(db, oldVersion, newVersion); + } + + /** + * Select all log entries + * + * @return list of log entries + */ + @Override + public ArrayList selectLogEntriesList() { + ArrayList list = new ArrayList<>(); + LogEntryModel logEntry; + + Cursor cursor = DBContract.selectEntriesToSend(getReadableDatabase()); + + try { + if (cursor.moveToFirst()) { + do { + logEntry = DBContract.getLogEntryFromCursor(cursor); + list.add(logEntry); + } while (cursor.moveToNext()); + } + } catch (Exception e) { + Log.e(Constants.LOG_TAG, "DBHelper.selectLogEntriesList: EXCEPTION - " + e.getLocalizedMessage(), e); + } finally { + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + } + + return list; + } + + @Override + public boolean insertLogEntry(LogEntryModel logEntry) { + if (!DBContract.isEntryExists(getReadableDatabase(), logEntry.getId())) { + return DBContract.insert(getWritableDatabase(), logEntry); + } + + return false; + } + + @Override + public boolean deleteLogEntry(int id) { + return DBContract.delete(getWritableDatabase(), id); + } + + @Override + public boolean deleteLogEntryList(int[] ids) { + return DBContract.delete(getWritableDatabase(), ids); + } + + @Override + public boolean updateEntriesStatus(int[] ids, boolean isSendingToServer) { + return DBContract.updateEntriesStatus(getWritableDatabase(), ids, isSendingToServer); + } +} diff --git a/library/src/main/java/com/mobiledevpro/remotelogcat/IDBHelper.java b/library/src/main/java/com/mobiledevpro/remotelogcat/IDBHelper.java new file mode 100644 index 0000000..db46394 --- /dev/null +++ b/library/src/main/java/com/mobiledevpro/remotelogcat/IDBHelper.java @@ -0,0 +1,57 @@ +package com.mobiledevpro.remotelogcat; + +import java.util.ArrayList; + +/** + * Interface for DBHelper + *

+ * Created by Dmitriy V. Chernysh on 23.09.17. + * dmitriy.chernysh@gmail.com + *

+ * https://fb.me/mobiledevpro/ + *

+ * #MobileDevPro + */ + +interface IDBHelper { + + /** + * Select all log entries + * + * @return list of log entries + */ + ArrayList selectLogEntriesList(); + + + /** + * Insert a new entry + * + * @param logEntry LogEntryModel + * @return True -success + */ + boolean insertLogEntry(LogEntryModel logEntry); + + /** + * Delete log entry + * + * @param id id + * @return True - success + */ + boolean deleteLogEntry(int id); + + /** + * Delete log entries + * + * @param ids ids + * @return True - success + */ + boolean deleteLogEntryList(int[] ids); + + /** + * Update entries status + * + * @param ids ids + * @return True - success + */ + boolean updateEntriesStatus(int[] ids, boolean isSendingToServer); +} diff --git a/library/src/main/java/com/mobiledevpro/remotelogcat/LogEntryModel.java b/library/src/main/java/com/mobiledevpro/remotelogcat/LogEntryModel.java new file mode 100644 index 0000000..9558cf6 --- /dev/null +++ b/library/src/main/java/com/mobiledevpro/remotelogcat/LogEntryModel.java @@ -0,0 +1,107 @@ +package com.mobiledevpro.remotelogcat; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + +/** + * Model for log entry + *

+ * Created by Dmitriy V. Chernysh on 23.09.17. + * dmitriy.chernysh@gmail.com + *

+ * https://fb.me/mobiledevpro/ + *

+ * #MobileDevPro + */ + +class LogEntryModel { + + private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS"; + + private int id; + private long dateTime; //in ms + private int logLevel; + private String logTag; + private String logMsg; + private AppInfoModel appInfo; + private UserInfoModel appUserInfo; + + /** + * Constructor for getting entry from DB + */ + LogEntryModel(int id, long dateTime, int logLevel, String logTag, String logMsg, AppInfoModel appInfo, UserInfoModel appUserInfo) { + this.id = id; + this.dateTime = dateTime; + this.logLevel = logLevel; + this.logTag = logTag; + this.logMsg = logMsg; + this.appInfo = appInfo; + this.appUserInfo = appUserInfo; + } + + /** + * Constructor for creating a new entry + */ + LogEntryModel(long dateTime, int logLevel, String logTag, String logMsg, AppInfoModel appInfo, UserInfoModel appUserInfo) { + this.dateTime = dateTime; + this.logLevel = logLevel; + this.logTag = logTag; + this.logMsg = logMsg; + this.appInfo = appInfo; + this.appUserInfo = appUserInfo; + } + + int getId() { + return id; + } + + long getDateTime() { + return dateTime; + } + + String getDateTimeTxt() { + return getStringFromDateTime(dateTime); + } + + int getLogLevel() { + return logLevel; + } + + String getLogLevelTxt() { + return Constants.getLogLevelTxt(logLevel); + } + + String getLogTag() { + return logTag; + } + + String getLogMsg() { + return logMsg; + } + + AppInfoModel getAppInfo() { + return appInfo; + } + + UserInfoModel getAppUserInfo() { + return appUserInfo; + } + + + /** + * Convert Date to Date in string format + * + * @param dateTime Date in milliseconds + * @return Date in string format + */ + private String getStringFromDateTime(long dateTime) { + if (dateTime == 0) return ""; + Date date = new Date(dateTime); + SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT, Locale.getDefault()); + format.setTimeZone(Calendar.getInstance().getTimeZone()); + return format.format(date); + } + +} diff --git a/library/src/main/java/com/mobiledevpro/remotelogcat/LogManager.java b/library/src/main/java/com/mobiledevpro/remotelogcat/LogManager.java new file mode 100644 index 0000000..5c45677 --- /dev/null +++ b/library/src/main/java/com/mobiledevpro/remotelogcat/LogManager.java @@ -0,0 +1,113 @@ +package com.mobiledevpro.remotelogcat; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Date; + + +/** + * Class for saving logs and sending them to server + *

+ * Created by Dmitriy V. Chernysh on 23.09.17. + * dmitriy.chernysh@gmail.com + *

+ * https://fb.me/mobiledevpro/ + *

+ * #MobileDevPro + */ + +class LogManager { + + private DBHelper mDBHelper; + private UserInfoModel mUserInfo; + private AppInfoModel mAppInfo; + private Context mContext; + private String mRequestToken; + private NetworkConnectionReceiver mNetworkConnectionReceiver; + + LogManager(Context appContext, String requestToken) { + mDBHelper = DBHelper.getInstance(appContext); + mContext = appContext; + mRequestToken = requestToken; + + PackageManager manager = appContext.getPackageManager(); + try { + PackageInfo info = manager.getPackageInfo(appContext.getPackageName(), 0); + mAppInfo = new AppInfoModel( + info.packageName, + info.versionName, + info.versionCode + ); + } catch (PackageManager.NameNotFoundException e) { + Log.e(Constants.LOG_TAG, "RemoteLog.init: NameNotFoundException - " + e.getLocalizedMessage(), e); + } + + mNetworkConnectionReceiver = new NetworkConnectionReceiver(); + } + + void setUserInfo(UserInfoModel userInfo) { + mUserInfo = userInfo; + } + + void send(int logLevel, String logTag, String logMessage, Throwable tr) { + switch (logLevel) { + case Constants.LOG_LEVEL_DEBUG: + if (tr == null) { + Log.d(logTag, logMessage); + } else { + Log.d(logTag, logMessage, tr); + } + break; + case Constants.LOG_LEVEL_ERROR: + if (tr == null) { + Log.e(logTag, logMessage); + } else { + Log.e(logTag, logMessage, tr); + } + break; + } + + //create model + LogEntryModel logEntryModel = createLogEntry(logLevel, logTag, logMessage); + //save into db + ArrayList logEntriesList = insertEntryIntoDb(logEntryModel); + //send saved entries to server + if (logEntriesList != null && !logEntriesList.isEmpty()) { + sendEntriesToServer(logEntriesList); + } + } + + void reSendLogs() { + // Log.d(Constants.LOG_TAG, "LogManager.reSendLogs(): "); + ArrayList logEntriesList = mDBHelper.selectLogEntriesList(); + //send saved entries to server + if (logEntriesList != null && !logEntriesList.isEmpty()) { + sendEntriesToServer(logEntriesList); + } + } + + private LogEntryModel createLogEntry(int logLevel, String logTag, String logMessage) { + if (mUserInfo == null) mUserInfo = new UserInfoModel(); + return new LogEntryModel( + new Date().getTime(), + logLevel, + logTag, + logMessage, + mAppInfo, + mUserInfo + ); + } + + private ArrayList insertEntryIntoDb(LogEntryModel logEntryModel) { + mDBHelper.insertLogEntry(logEntryModel); + return mDBHelper.selectLogEntriesList(); + } + + private void sendEntriesToServer(ArrayList logEntriesList) { + AsyncTaskCompat.executeParallel(new NetworkHelper(mContext, mRequestToken, logEntriesList, mNetworkConnectionReceiver)); + } +} diff --git a/library/src/main/java/com/mobiledevpro/remotelogcat/NetworkConnectionReceiver.java b/library/src/main/java/com/mobiledevpro/remotelogcat/NetworkConnectionReceiver.java new file mode 100644 index 0000000..b4f87fe --- /dev/null +++ b/library/src/main/java/com/mobiledevpro/remotelogcat/NetworkConnectionReceiver.java @@ -0,0 +1,30 @@ +package com.mobiledevpro.remotelogcat; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; + +/** + * Broadcast receiver for network connection + *

+ * Created by Dmitriy V. Chernysh on 03.10.17. + * dmitriy.chernysh@gmail.com + *

+ * https://fb.me/mobiledevpro/ + *

+ * #MobileDevPro + */ + +public class NetworkConnectionReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + // Log.d(Constants.LOG_TAG, "NetworkConnectionReceiver.onReceive(): " + intent); + if (!intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) return; + + if (Constants.isDeviceOnline(context)) { + RemoteLog.resendLogs(); + } + } +} diff --git a/library/src/main/java/com/mobiledevpro/remotelogcat/NetworkHelper.java b/library/src/main/java/com/mobiledevpro/remotelogcat/NetworkHelper.java new file mode 100644 index 0000000..b202845 --- /dev/null +++ b/library/src/main/java/com/mobiledevpro/remotelogcat/NetworkHelper.java @@ -0,0 +1,182 @@ +package com.mobiledevpro.remotelogcat; + +import android.content.Context; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.os.AsyncTask; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; + +/** + * Class for data sending to server + *

+ * Created by Dmitriy V. Chernysh on 23.09.17. + * dmitriy.chernysh@gmail.com + *

+ * https://fb.me/mobiledevpro/ + *

+ * #MobileDevPro + */ + +class NetworkHelper extends AsyncTask { + + private static final String URL = "http://api.mobile-dev.pro/applog/api"; + private static final String METHOD = "POST"; + private static final int TIMEOUT = 5000; //ms + + private Context mContext; + private String mToken; + private ArrayList mLogEntriesList; + private int[] mEntriesIds; + private NetworkConnectionReceiver mNetworkConnectionReceiver; + + NetworkHelper(Context context, String token, ArrayList logEntriesList, NetworkConnectionReceiver networkConnectionReceiver) { + mContext = context; + mToken = token; + mLogEntriesList = logEntriesList; + mNetworkConnectionReceiver = networkConnectionReceiver; + + if (mLogEntriesList != null && !mLogEntriesList.isEmpty()) { + mEntriesIds = new int[mLogEntriesList.size()]; + for (int i = 0, j = mLogEntriesList.size(); i < j; i++) { + mEntriesIds[i] = mLogEntriesList.get(i).getId(); + } + } + } + + + @Override + protected Boolean doInBackground(Void... params) { + if (!Constants.isDeviceOnline(mContext)) { + if (mNetworkConnectionReceiver != null) { + mContext.registerReceiver(mNetworkConnectionReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + } + return false; + } else { + try { + if (mNetworkConnectionReceiver != null) { + mContext.unregisterReceiver(mNetworkConnectionReceiver); + } + } catch (IllegalArgumentException e) { + //do nothing + } + } + + if (mLogEntriesList == null || mLogEntriesList.isEmpty()) return false; + + //change status for selected entries + DBHelper.getInstance(mContext).updateEntriesStatus(mEntriesIds, true); + + //create json body + JSONArray jsonArray = createRequestBody(); + if (jsonArray == null || jsonArray.length() == 0) return false; + + //send entries to server + return sendRequest(jsonArray); + } + + @Override + protected void onPostExecute(Boolean aBoolean) { + DBHelper dbHelper = DBHelper.getInstance(mContext); + if (aBoolean) { + //remove entries from the local database + dbHelper.deleteLogEntryList(mEntriesIds); + } else { + //change status for selected entries + dbHelper.updateEntriesStatus(mEntriesIds, false); + } + } + + private JSONArray createRequestBody() { + JSONArray jsonArray = new JSONArray(); + + JSONObject jsonEntry; + JSONObject jsonAppData; + JSONObject jsonAppUserData; + AppInfoModel appInfo; + UserInfoModel appUserInfo; + try { + for (LogEntryModel logEntry : mLogEntriesList) { + jsonEntry = new JSONObject(); + jsonEntry.put("datetime", logEntry.getDateTimeTxt()); + jsonEntry.put("loglevel", logEntry.getLogLevelTxt()); + jsonEntry.put("logtag", logEntry.getLogTag()); + jsonEntry.put("error", logEntry.getLogMsg()); + + //app info + jsonAppData = new JSONObject(); + appInfo = logEntry.getAppInfo(); + jsonAppData.put("name", appInfo != null ? appInfo.getName() : ""); + jsonAppData.put("version", appInfo != null ? appInfo.getVersion() + " (" + appInfo.getBuild() + ")" : ""); + jsonEntry.put("app", jsonAppData); + + //app user's info + jsonAppUserData = new JSONObject(); + appUserInfo = logEntry.getAppUserInfo(); + jsonAppUserData.put("androidApi", appUserInfo.getAndroidApiTxt()); + jsonAppUserData.put("device", appUserInfo.getDeviceModel()); + jsonAppUserData.put("login", appUserInfo.getUserName()); + jsonEntry.put("appUser", jsonAppUserData); + + jsonArray.put(jsonEntry); + } + } catch (JSONException e) { + Log.e(Constants.LOG_TAG, "NetworkHelper.createRequestBody: JSONException - " + e.getLocalizedMessage(), e); + } + + return jsonArray; + } + + private boolean sendRequest(JSONArray jsonArray) { + try { + URL url = new URL(URL + "?token=" + mToken); + HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); + urlConnection.setConnectTimeout(TIMEOUT); + urlConnection.setReadTimeout(TIMEOUT); + urlConnection.setRequestMethod(METHOD); + urlConnection.setRequestProperty("Content-Type", "application/json"); + urlConnection.setDoOutput(true); + urlConnection.setChunkedStreamingMode(0); + urlConnection.connect(); + + //write request + OutputStreamWriter outputWriter = new OutputStreamWriter(urlConnection.getOutputStream()); + outputWriter.write(jsonArray.toString()); + outputWriter.close(); + + InputStream inputStream = new BufferedInputStream(urlConnection.getInputStream()); + + //get response + int responseCode = urlConnection.getResponseCode(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length = 0; + while ((length = inputStream.read(buffer)) != -1) { + baos.write(buffer, 0, length); + } + + urlConnection.disconnect(); + + return responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_ACCEPTED; + } catch (MalformedURLException me) { + Log.e(Constants.LOG_TAG, "NetworkHelper.sendRequest: MalformedInputException - " + me.getLocalizedMessage(), me); + } catch (IOException ie) { + Log.e(Constants.LOG_TAG, "NetworkHelper.sendRequest: IOException - " + ie.getLocalizedMessage(), ie); + } + return false; + } +} diff --git a/library/src/main/java/com/mobiledevpro/remotelogcat/RemoteLog.java b/library/src/main/java/com/mobiledevpro/remotelogcat/RemoteLog.java new file mode 100644 index 0000000..08b0fab --- /dev/null +++ b/library/src/main/java/com/mobiledevpro/remotelogcat/RemoteLog.java @@ -0,0 +1,72 @@ +package com.mobiledevpro.remotelogcat; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.Log; + +/** + * Class for sending logs to remote server + *

+ * Created by Dmitriy V. Chernysh on 23.09.17. + * dmitriy.chernysh@gmail.com + *

+ * https://fb.me/mobiledevpro/ + *

+ * #MobileDevPro + */ + +public class RemoteLog { + + private static String sToken; + private static LogManager sLogManager; + + private RemoteLog() { + } + + public static void init(Context context, @NonNull String token) { + sToken = token; + sLogManager = new LogManager(context, token); + } + + public static void setUserInfo(UserInfoModel userInfo) { + if (sLogManager == null) return; + sLogManager.setUserInfo(userInfo); + } + + public static void d(String tag, String msg) { + if (isTokenEmpty()) return; + if (sLogManager == null) return; + sLogManager.send(Constants.LOG_LEVEL_DEBUG, tag, msg, null); + } + + public static void d(String tag, String msg, Throwable tr) { + if (isTokenEmpty()) return; + if (sLogManager == null) return; + sLogManager.send(Constants.LOG_LEVEL_DEBUG, tag, msg, tr); + } + + public static void e(String tag, String msg) { + if (isTokenEmpty()) return; + if (sLogManager == null) return; + sLogManager.send(Constants.LOG_LEVEL_ERROR, tag, msg, null); + } + + public static void e(String tag, String msg, Throwable tr) { + if (isTokenEmpty()) return; + if (sLogManager == null) return; + sLogManager.send(Constants.LOG_LEVEL_ERROR, tag, msg, tr); + } + + static void resendLogs() { + if (sLogManager == null) return; + sLogManager.reSendLogs(); + } + + private static boolean isTokenEmpty() { + boolean b = TextUtils.isEmpty(sToken); + if (b) + Log.e("RemoteLog", "Token is empty. Please, call RemoteLog.init([token here]) in onCreate() method of the main application class"); + return b; + } +} diff --git a/library/src/main/java/com/mobiledevpro/remotelogcat/UserInfoModel.java b/library/src/main/java/com/mobiledevpro/remotelogcat/UserInfoModel.java new file mode 100644 index 0000000..c9db71d --- /dev/null +++ b/library/src/main/java/com/mobiledevpro/remotelogcat/UserInfoModel.java @@ -0,0 +1,79 @@ +package com.mobiledevpro.remotelogcat; + +import android.os.Build; +import android.text.TextUtils; + +/** + * App User info + *

+ * Created by Dmitriy V. Chernysh on 25.09.17. + * dmitriy.chernysh@gmail.com + *

+ * https://fb.me/mobiledevpro/ + *

+ * #MobileDevPro + */ + +public class UserInfoModel { + + private int androidApi; + private String deviceModel; + private String userName; + + public UserInfoModel(String userName) { + this.androidApi = Build.VERSION.SDK_INT; + this.deviceModel = getDeviceName(); + this.userName = userName; + } + + UserInfoModel() { + this.androidApi = Build.VERSION.SDK_INT; + this.deviceModel = getDeviceName(); + } + + /** + * For getting value form DB + * + * @param userInfo some data about user (divider - "|") + */ + UserInfoModel parseUserInfo(String userInfo) { + if (TextUtils.isEmpty(userInfo)) return this; + //android api| + String[] arrayInfo = userInfo.split(";"); + if (arrayInfo.length == 0) return this; + + if (arrayInfo.length > 0) this.androidApi = Integer.valueOf("0" + arrayInfo[0]); + if (arrayInfo.length > 1) this.deviceModel = arrayInfo[1]; + if (arrayInfo.length > 2) this.userName = arrayInfo[2]; + + return this; + } + + String getAndroidApiTxt() { + return "API " + androidApi; + } + + String getDeviceModel() { + return deviceModel; + } + + String getUserName() { + return userName; + } + + String getString() { + return androidApi + + (!TextUtils.isEmpty(deviceModel) ? ";" + deviceModel : "") + + (!TextUtils.isEmpty(userName) ? ";" + userName : ""); + } + + private String getDeviceName() { + String manufacturer = Build.MANUFACTURER; + String model = Build.MODEL; + if (model.startsWith(manufacturer)) { + return model.toUpperCase(); + } else { + return (manufacturer + " " + model).toUpperCase(); + } + } +} diff --git a/library/src/main/res/values/strings.xml b/library/src/main/res/values/strings.xml new file mode 100644 index 0000000..75991a1 --- /dev/null +++ b/library/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Remote Logcat +