com.psiphon3.psiphonlibrary.LoggingProvider.java Source code

Java tutorial

Introduction

Here is the source code for com.psiphon3.psiphonlibrary.LoggingProvider.java

Source

/*
 * Copyright (c) 2016, Psiphon Inc.
 * All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package com.psiphon3.psiphonlibrary;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;

import com.psiphon3.psiphonlibrary.Utils.MyLog;
import com.psiphon3.BuildConfig;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;

/**
 * All logging is done directly to the LoggingProvider from all processes.
 */
public class LoggingProvider extends ContentProvider {
    public static final Uri INSERT_URI = Uri
            .parse("content://" + BuildConfig.APPLICATION_ID + "." + LoggingProvider.class.getSimpleName());

    /**
     * JSON-ify the arguments to be used in a call to the LoggingProvider content provider.
     * @param context The context to be used for access app resources.
     * @param date Timestamp for the log.
     * @param stringResID String resource ID.
     * @param sensitivity Log sensitivity level.
     * @param formatArgs Arguments to be formatted into the log string.
     * @param priority One of the log priority levels supported by MyLog. Like: Log.DEBUG, Log.INFO, Log.WARN, Log.ERROR, Log.VERBOSE
     * @return null on error.
     */
    public static String makeStatusLogJSON(Context context, Date date, int stringResID,
            MyLog.Sensitivity sensitivity, Object[] formatArgs, int priority) {
        String resourceName = context.getResources().getResourceName(stringResID);

        JSONObject json = new JSONObject();
        try {
            JSONArray jsonArray = new JSONArray();
            if (formatArgs != null) {
                for (Object arg : formatArgs) {
                    jsonArray.put(arg);
                }
            }

            json.put("timestamp", date.getTime()); // Store as millis since epoch
            json.put("stringResourceName", resourceName);
            json.put("sensitivity", sensitivity.name());
            json.put("formatArgs", jsonArray);
            json.put("priority", priority);
            return json.toString();
        } catch (JSONException e) {
            // pass
        }

        return null;
    }

    /**
     * JSON-ify the arguments to be used in a call to the LoggingProvider content provider.
     * @param date Timestamp for the log.
     * @param msg String nessage name.
     * @param data String json data.
     * @return null on error.
     */
    public static String makeDiagnosticLogJSON(Date date, String msg, JSONObject data) {
        JSONObject json = new JSONObject();
        try {
            json.put("timestamp", date.getTime()); // Store as millis since epoch
            json.put("msg", msg);
            json.put("data", data);
            return json.toString();
        } catch (JSONException e) {
            // pass
        }

        return null;
    }

    /**
     * To be called by the UI when logs should be read from the provider DB into the StatusList.
     * @param context
     */
    public static void retrieveLogs(Context context) {
        LogDatabaseHelper.retrieveLogs(context);
    }

    @Override
    public boolean onCreate() {
        return false;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs,
            String sortOrder) {
        assert (false);
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        assert (false);
        return null;
    }

    /**
     * Called when a content provider consumer wants to create a log.
     * @param uri Ignored.
     * @param values Must have COLUMN_NAME_LOGJSON value, created by makeStatusLogJSON() or makeDiagnosticLogJSON()
     *               and boolean COLUMN_NAME_IS_DIAGNOSTIC indicating whether the log entry is 'diagnostic' or 'status'
     * @return Always returns null.
     */
    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, ContentValues values) {
        LogDatabaseHelper.insertLog(this.getContext(), values);
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
        assert (false);
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        assert (false);
        return 0;
    }

    /**
     * The database where logs are stored until they can be consumed by the app.
     */
    public static class LogDatabaseHelper extends SQLiteOpenHelper {
        private static final int DAYS_TO_STORE_LOGS = 2;
        private static final String DATABASE_NAME = "loggingprovider.db";
        private static final int DATABASE_VERSION = 2;

        private static final String TABLE_NAME = "log";
        private static final String COLUMN_NAME_ID = "_ID";
        public static final String COLUMN_NAME_LOGJSON = "logjson";
        public static final String COLUMN_NAME_IS_DIAGNOSTIC = "is_diagnostic";
        public static final String COLUMN_NAME_TIMESTAMP = "timestamp";
        private static final String DICTIONARY_TABLE_CREATE = "CREATE TABLE " + TABLE_NAME + " (" + COLUMN_NAME_ID
                + " INTEGER PRIMARY KEY AUTOINCREMENT, " + COLUMN_NAME_LOGJSON + " TEXT NOT NULL, "
                + COLUMN_NAME_IS_DIAGNOSTIC + " BOOLEAN DEFAULT 0, " + COLUMN_NAME_TIMESTAMP
                + " TIMESTAMP DEFAULT CURRENT_TIMESTAMP " + ");";

        /**
         * The database object. Note that SQLite is thread-safe (by default).
         */
        private SQLiteDatabase mDB;

        // Singleton pattern
        private static LogDatabaseHelper mLogDatabaseHelper;

        public Object clone() throws CloneNotSupportedException {
            throw new CloneNotSupportedException();
        }

        public static synchronized LogDatabaseHelper get(Context context) {
            if (mLogDatabaseHelper == null) {
                mLogDatabaseHelper = new LogDatabaseHelper(context);
            }

            return mLogDatabaseHelper;
        }

        public synchronized SQLiteDatabase getDB() {
            if (mDB == null) {
                mDB = mLogDatabaseHelper.getWritableDatabase();
            }

            return mDB;
        }

        public LogDatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(DICTIONARY_TABLE_CREATE);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            if (newVersion == 2 && oldVersion == 1) {
                db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
                db.execSQL(DICTIONARY_TABLE_CREATE);
            }
        }

        /**
         * Insert a new log. May execute asynchronously.
         */
        public static void insertLog(Context context, ContentValues values) {
            // If this function is being called in the UI thread, then we need to do the work in an
            // async task. Otherwise we'll do the work directly.
            // For info about content provider thread use: http://stackoverflow.com/a/3571583
            if (Looper.myLooper() == Looper.getMainLooper()) {
                InsertLogTask task = new InsertLogTask(context);
                task.execute(values);
            } else {
                LogDatabaseHelper.insertLogHelper(context, values);
            }
        }

        /**
         * Task to do the async work.
         */
        private static class InsertLogTask extends AsyncTask<ContentValues, Void, Void> {
            private Context mContext;

            public InsertLogTask(Context context) {
                mContext = context;
            }

            @Override
            protected Void doInBackground(ContentValues... params) {
                // DO NOT LOG WITHIN THIS FUNCTION

                // There will only ever be one item in the array, but...
                for (int i = 0; i < params.length; i++) {
                    LogDatabaseHelper.insertLogHelper(mContext, params[i]);
                }

                return null;
            }
        }

        /**
         * Inserts a new log. Should be called via insertLog or InsertLogTask.
         * @param context
         * @param values
         */
        private static void insertLogHelper(Context context, ContentValues values) {
            // DO NOT LOG WITHIN THIS FUNCTION

            SQLiteDatabase db = LogDatabaseHelper.get(context).getDB();

            db.beginTransaction();

            db.insert(TABLE_NAME, null, values);

            db.setTransactionSuccessful();
            db.endTransaction();

            context.getContentResolver().notifyChange(INSERT_URI, null);
        }

        /**
         * To be called by the UI at a time when it's appropriate to truncate logs database.
         * May execute asynchronously.
         */
        public static void truncateLogs(Context context, boolean full) {
            // If this function is being called in the UI thread, then we need to do the work in an
            // async task. Otherwise we'll do the work directly.
            // For info about content provider thread use: http://stackoverflow.com/a/3571583
            if (Looper.myLooper() == Looper.getMainLooper()) {
                TruncateLogsTask task = new TruncateLogsTask(context, full);
                task.execute();
            } else {
                LogDatabaseHelper.truncateLogsHelper(context, full);
            }

        }

        /**
         * Task to do the async work.
         */
        private static class TruncateLogsTask extends AsyncTask<Void, Void, Void> {
            private Context mContext;
            private boolean mFull;

            public TruncateLogsTask(Context context, boolean full) {
                mContext = context;
                mFull = full;
            }

            @Override
            protected Void doInBackground(Void... params) {
                LogDatabaseHelper.truncateLogsHelper(mContext, mFull);
                return null;
            }
        }

        /**
         * Does the log truncation work. Should be called via truncateLogs or TruncateLogsTask.
         * @param context
         */
        private static void truncateLogsHelper(Context context, boolean full) {
            SQLiteDatabase db = LogDatabaseHelper.get(context).getDB();

            String whereClause = null;
            String[] whereArgs = null;

            if (!full) {
                whereClause = COLUMN_NAME_TIMESTAMP + "<?";

                SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
                Date date = new Date();
                Calendar cal = Calendar.getInstance();
                cal.setTime(date);
                cal.add(Calendar.DATE, -DAYS_TO_STORE_LOGS);

                whereArgs = new String[] { dateFormat.format(cal.getTime()) };
            }

            db.delete(TABLE_NAME, whereClause, whereArgs);
        }

        /**
         * To be called by the UI at a time when it's appropriate to consume logs that were stored
         * by the provider. May execute asynchronously.
         */
        public static void retrieveLogs(Context context) {
            // If this function is being called in the UI thread, then we need to do the work in an
            // async task. Otherwise we'll do the work directly.
            // For info about content provider thread use: http://stackoverflow.com/a/3571583
            if (Looper.myLooper() == Looper.getMainLooper()) {
                RetrieveLogsTask task = new RetrieveLogsTask(context);
                task.execute();
            } else {
                LogDatabaseHelper.retrieveLogsHelper(context);
            }

        }

        /**
         * Task to do the async work.
         */
        private static class RetrieveLogsTask extends AsyncTask<Void, Void, Void> {
            private Context mContext;

            public RetrieveLogsTask(Context context) {
                mContext = context;
            }

            @Override
            protected Void doInBackground(Void... params) {
                // DO NOT LOG WITHIN THIS FUNCTION

                LogDatabaseHelper.retrieveLogsHelper(mContext);

                return null;
            }
        }

        /**
         * Does the log retrieval work. Should be called via retrieveLogs or RetrieveLogsTask.
         * @param context
         */
        private static void retrieveLogsHelper(Context context) {
            // DO NOT LOG WITHIN THIS FUNCTION

            // We will cursor through DB records, passing them off to StatusList

            SQLiteDatabase db = LogDatabaseHelper.get(context).getDB();

            String[] projection = { COLUMN_NAME_ID, COLUMN_NAME_IS_DIAGNOSTIC, COLUMN_NAME_LOGJSON };

            // retrieve status logs  (COLUMN_NAME_IS_DIAGNOSTIC == false)
            String whereClause = "NOT(" + COLUMN_NAME_IS_DIAGNOSTIC + ") ";
            String[] whereArgs = null;

            StatusList.StatusEntry lastEntry = StatusList.getStatusEntry(-1);
            if (lastEntry != null) {
                whereClause += " AND " + COLUMN_NAME_ID + " >?";
                whereArgs = new String[] { String.valueOf(lastEntry.key()) };
            }

            String sortOrder = COLUMN_NAME_ID + " ASC";

            Cursor cursor = db.query(TABLE_NAME, projection, whereClause, whereArgs, null, null, sortOrder);

            int numberOfLogsRetrieved = 0;

            // Iterate over the cursor
            try {
                while (cursor.moveToNext()) {

                    long ID = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_NAME_ID));
                    String logJSON = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME_LOGJSON));

                    // Extract log args from JSON.
                    String stringResourceName;
                    int priority;
                    MyLog.Sensitivity sensitivity;
                    Object[] formatArgs;
                    Date timestamp;
                    try {
                        JSONObject jsonObj = new JSONObject(logJSON);
                        stringResourceName = jsonObj.getString("stringResourceName");
                        sensitivity = MyLog.Sensitivity.valueOf(jsonObj.getString("sensitivity"));
                        priority = jsonObj.getInt("priority");
                        timestamp = new Date(jsonObj.getLong("timestamp"));

                        JSONArray formatArgsJSONArray = jsonObj.getJSONArray("formatArgs");
                        formatArgs = new Object[formatArgsJSONArray.length()];
                        for (int i = 0; i < formatArgsJSONArray.length(); i++) {
                            formatArgs[i] = formatArgsJSONArray.get(i);
                        }

                        // Convert the resource name to ID.
                        int resourceID = context.getResources().getIdentifier(stringResourceName, null, null);
                        if (resourceID == 0) {
                            // Failed to convert from resource name to ID. This can happen if a
                            // string resource has been renamed since the log entry was created.
                            continue;
                        }

                        // Pass the log info on to StatusList.
                        // Keep this call in the try block so it gets skipped if there's an exception above.
                        StatusList.addStatusEntry(ID, timestamp, resourceID, sensitivity, formatArgs, null,
                                priority);

                        numberOfLogsRetrieved++;
                    } catch (JSONException e) {
                        // just skip this entry
                    }
                }
            } finally {
                cursor.close();
            }

            if (numberOfLogsRetrieved > 0) {
                LocalBroadcastManager.getInstance(context)
                        .sendBroadcast(new Intent(MainBase.TabbedActivityBase.STATUS_ENTRY_AVAILABLE));
            }

            // retrieve diagnostic logs  (COLUMN_NAME_IS_DIAGNOSTIC == true)
            whereClause = COLUMN_NAME_IS_DIAGNOSTIC;
            whereArgs = null;
            StatusList.DiagnosticEntry lastDiagnosticEntry = StatusList.getDiagnosticEntry(-1);
            if (lastDiagnosticEntry != null) {
                whereClause += " AND " + COLUMN_NAME_ID + " >?";
                whereArgs = new String[] { String.valueOf(lastDiagnosticEntry.key()) };
            }

            sortOrder = COLUMN_NAME_ID + " ASC";

            cursor = db.query(TABLE_NAME, projection, whereClause, whereArgs, null, null, sortOrder);

            // Iterate over the cursor
            try {
                while (cursor.moveToNext()) {

                    long ID = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_NAME_ID));
                    String logJSON = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME_LOGJSON));

                    // Extract log args from JSON.
                    Date timestamp;
                    String msg;
                    JSONObject data;
                    try {
                        JSONObject jsonObj = new JSONObject(logJSON);
                        timestamp = new Date(jsonObj.getLong("timestamp"));
                        msg = jsonObj.getString("msg");
                        data = jsonObj.getJSONObject("data");

                        // Pass the log info on to StatusList.
                        // Keep this call in the try block so it gets skipped if there's an exception above.
                        StatusList.addDiagnosticEntry(ID, timestamp, msg, data);
                    } catch (JSONException e) {
                        // just skip this entry
                    }
                }
            } finally {
                cursor.close();
            }
        }
    }
}