Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f33f1cab authored by Kurt Partridge's avatar Kurt Partridge
Browse files

[FileEncap9] Extract ResearchLogDirectory class

Previously used a monotonically increasing int.  Now uses uuid and nanoseconds.

squashed in:
    [FileEncap11] Read preference from ResearchSettings
    Change-Id: Ic779e0a69db6b16e92c6f4b63dbe7b7add566ab6

    [FileEncap12] Simplify directory cleanup invocation
    Change-Id: I688047409c0343d32b11447fb625dfb726c731ec

    [FileEncap14] Change log filename syntax
    Change-Id: I9243b20b2eb392f81ab8c5c3d19315211240e0bc

Change-Id: I5c9d70e0cb7b0965158e17dd71dfab796bd9a440
parent 7faa2caa
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -118,6 +118,8 @@ public class ResearchLog {
                } catch (Exception e) {
                    Log.d(TAG, "error when closing ResearchLog:", e);
                } finally {
                    // Marking the file as read-only signals that this log file is ready to be
                    // uploaded.
                    if (mFile != null && mFile.exists()) {
                        mFile.setWritable(false, false);
                    }
+111 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.inputmethod.research;

import android.content.Context;
import android.util.Log;

import java.io.File;
import java.io.FileFilter;

/**
 * Manages log files.
 *
 * This class handles all aspects where and how research log data is stored.  This includes
 * generating log filenames in the correct place with the correct names, and cleaning up log files
 * under this directory.
 */
public class ResearchLogDirectory {
    public static final String TAG = ResearchLogDirectory.class.getSimpleName();
    /* package */ static final String LOG_FILENAME_PREFIX = "researchLog";
    private static final String FILENAME_SUFFIX = ".txt";
    private static final String USER_RECORDING_FILENAME_PREFIX = "recording";

    private static final ReadOnlyLogFileFilter sUploadableLogFileFilter =
            new ReadOnlyLogFileFilter();

    private final File mFilesDir;

    static class ReadOnlyLogFileFilter implements FileFilter {
        @Override
        public boolean accept(final File pathname) {
            return pathname.getName().startsWith(ResearchLogDirectory.LOG_FILENAME_PREFIX)
                    && !pathname.canWrite();
        }
    }

    /**
     * Creates a new ResearchLogDirectory, creating the storage directory if it does not exist.
     */
    public ResearchLogDirectory(final Context context) {
        mFilesDir = getLoggingDirectory(context);
        if (mFilesDir == null) {
            throw new NullPointerException("No files directory specified");
        }
        if (!mFilesDir.exists()) {
            mFilesDir.mkdirs();
        }
    }

    private File getLoggingDirectory(final Context context) {
        // TODO: Switch to using a subdirectory of getFilesDir().
        return context.getFilesDir();
    }

    /**
     * Get an array of log files that are ready for uploading.
     *
     * A file is ready for uploading if it is marked as read-only.
     *
     * @return the array of uploadable files
     */
    public File[] getUploadableLogFiles() {
        try {
            return mFilesDir.listFiles(sUploadableLogFileFilter);
        } catch (final SecurityException e) {
            Log.e(TAG, "Could not cleanup log directory, permission denied", e);
            return new File[0];
        }
    }

    public void cleanupLogFilesOlderThan(final long time) {
        try {
            for (final File file : mFilesDir.listFiles()) {
                final String filename = file.getName();
                if ((filename.startsWith(LOG_FILENAME_PREFIX)
                        || filename.startsWith(USER_RECORDING_FILENAME_PREFIX))
                        && (file.lastModified() < time)) {
                    file.delete();
                }
            }
        } catch (final SecurityException e) {
            Log.e(TAG, "Could not cleanup log directory, permission denied", e);
        }
    }

    public File getLogFilePath(final long time) {
        return new File(mFilesDir, getUniqueFilename(LOG_FILENAME_PREFIX, time));
    }

    public File getUserRecordingFilePath(final long time) {
        return new File(mFilesDir, getUniqueFilename(USER_RECORDING_FILENAME_PREFIX, time));
    }

    private static String getUniqueFilename(final String prefix, final long time) {
        return prefix + "-" + time + FILENAME_SUFFIX;
    }
}
+25 −68
Original line number Diff line number Diff line
@@ -125,12 +125,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
    /* package */ static boolean sIsLogging = false;
    private static final int OUTPUT_FORMAT_VERSION = 5;
    private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
    /* package */ static final String LOG_FILENAME_PREFIX = "researchLog";
    private static final String LOG_FILENAME_SUFFIX = ".txt";
    /* package */ static final String USER_RECORDING_FILENAME_PREFIX = "recording";
    private static final String USER_RECORDING_FILENAME_SUFFIX = ".txt";
    private static final SimpleDateFormat TIMESTAMP_DATEFORMAT =
            new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US);
    // Whether all words should be recorded, leaving unsampled word between bigrams.  Useful for
    // testing.
    /* package for test */ static final boolean IS_LOGGING_EVERYTHING = false
@@ -156,12 +150,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang

    private static final long RESEARCHLOG_CLOSE_TIMEOUT_IN_MS = 5 * 1000;
    private static final long RESEARCHLOG_ABORT_TIMEOUT_IN_MS = 5 * 1000;
    private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS;
    private static final long MAX_LOGFILE_AGE_IN_MS = 4 * DateUtils.DAY_IN_MILLIS;

    private static final ResearchLogger sInstance = new ResearchLogger();
    private static String sAccountType = null;
    private static String sAllowedAccountDomain = null;
    // to write to a different filename, e.g., for testing, set mFile before calling start()
    /* package */ File mFilesDir;
    /* package */ ResearchLog mMainResearchLog;
    // mFeedbackLog records all events for the session, private or not (excepting
    // passwords).  It is written to permanent storage only if the user explicitly commands
@@ -187,9 +181,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
            Character.codePointAt("\uE000", 0);  // U+E000 is in the "private-use area"
    // U+E001 is in the "private-use area"
    /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001";
    private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time";
    private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS;
    private static final long MAX_LOGFILE_AGE_IN_MS = 4 * DateUtils.DAY_IN_MILLIS;
    protected static final int SUSPEND_DURATION_IN_MINUTES = 1;
    // set when LatinIME should ignore an onUpdateSelection() callback that
    // arises from operations in this class
@@ -203,6 +194,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
    private final Statistics mStatistics;
    private final MotionEventReader mMotionEventReader = new MotionEventReader();
    private final Replayer mReplayer = Replayer.getInstance();
    private ResearchLogDirectory mResearchLogDirectory;

    private Intent mUploadIntent;
    private Intent mUploadNowIntent;
@@ -240,11 +232,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
            final Suggest suggest) {
        assert latinIME != null;
        mLatinIME = latinIME;
        mFilesDir = latinIME.getFilesDir();
        if (mFilesDir == null || !mFilesDir.exists()) {
            Log.w(TAG, "IME storage directory does not exist.  Cannot start logging.");
            return;
        }
        mPrefs = PreferenceManager.getDefaultSharedPreferences(latinIME);
        mPrefs.registerOnSharedPreferenceChangeListener(this);

@@ -256,15 +243,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
        sAccountType = res.getString(R.string.research_account_type);
        sAllowedAccountDomain = res.getString(R.string.research_allowed_account_domain);

        // Cleanup logging directory
        // TODO: Move this and other file-related components to separate file.
        final long lastCleanupTime = mPrefs.getLong(PREF_LAST_CLEANUP_TIME, 0L);
        final long now = System.currentTimeMillis();
        if (now - lastCleanupTime > DURATION_BETWEEN_DIR_CLEANUP_IN_MS) {
            final long timeHorizon = now - MAX_LOGFILE_AGE_IN_MS;
            cleanupLoggingDir(mFilesDir, timeHorizon);
            mPrefs.edit().putLong(PREF_LAST_CLEANUP_TIME, now).apply();
        }
        // Initialize directory manager
        mResearchLogDirectory = new ResearchLogDirectory(mLatinIME);
        cleanLogDirectoryIfNeeded(mResearchLogDirectory, System.currentTimeMillis());

        // Initialize external services
        mUploadIntent = new Intent(mLatinIME, UploaderService.class);
@@ -276,6 +257,15 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
        mReplayer.setKeyboardSwitcher(keyboardSwitcher);
    }

    private void cleanLogDirectoryIfNeeded(final ResearchLogDirectory researchLogDirectory,
            final long now) {
        final long lastCleanupTime = ResearchSettings.readResearchLastDirCleanupTime(mPrefs);
        if (now - lastCleanupTime < DURATION_BETWEEN_DIR_CLEANUP_IN_MS) return;
        final long oldestAllowedFileTime = now - MAX_LOGFILE_AGE_IN_MS;
        mResearchLogDirectory.cleanupLogFilesOlderThan(oldestAllowedFileTime);
        ResearchSettings.writeResearchLastDirCleanupTime(mPrefs, now);
    }

    /**
     * Arrange for the UploaderService to be run on a regular basis.
     *
@@ -295,17 +285,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
                UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent);
    }

    private void cleanupLoggingDir(final File dir, final long time) {
        for (File file : dir.listFiles()) {
            final String filename = file.getName();
            if ((filename.startsWith(ResearchLogger.LOG_FILENAME_PREFIX)
                    || filename.startsWith(ResearchLogger.USER_RECORDING_FILENAME_PREFIX))
                    && file.lastModified() < time) {
                file.delete();
            }
        }
    }

    public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) {
        mMainKeyboardView = mainKeyboardView;
        maybeShowSplashScreen();
@@ -387,35 +366,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
        restart();
    }

    private static int sLogFileCounter = 0;

    private File createLogFile(final File filesDir) {
        final StringBuilder sb = new StringBuilder();
        sb.append(LOG_FILENAME_PREFIX).append('-');
        final String uuid = ResearchSettings.readResearchLoggerUuid(mPrefs);
        sb.append(uuid).append('-');
        sb.append(TIMESTAMP_DATEFORMAT.format(new Date())).append('-');
        // Sometimes logFiles are created within milliseconds of each other.  Append a counter to
        // separate these.
        if (sLogFileCounter < Integer.MAX_VALUE) {
            sLogFileCounter++;
        } else {
            // Wrap the counter, in the unlikely event of overflow.
            sLogFileCounter = 0;
        }
        sb.append(sLogFileCounter);
        sb.append(LOG_FILENAME_SUFFIX);
        return new File(filesDir, sb.toString());
    }

    private File createUserRecordingFile(final File filesDir) {
        final StringBuilder sb = new StringBuilder();
        sb.append(USER_RECORDING_FILENAME_PREFIX).append('-');
        final String uuid = ResearchSettings.readResearchLoggerUuid(mPrefs);
        sb.append(uuid).append('-');
        sb.append(TIMESTAMP_DATEFORMAT.format(new Date()));
        sb.append(USER_RECORDING_FILENAME_SUFFIX);
        return new File(filesDir, sb.toString());
    private void setLoggingAllowed(final boolean enableLogging) {
        if (mPrefs == null) return;
        sIsLogging = enableLogging;
        ResearchSettings.writeResearchLoggerEnabledFlag(mPrefs, enableLogging);
    }

    private void checkForEmptyEditor() {
@@ -455,7 +409,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
            return;
        }
        if (mMainLogBuffer == null) {
            mMainResearchLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME);
            mMainResearchLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
                    System.currentTimeMillis()), mLatinIME);
            final int numWordsToIgnore = new Random().nextInt(NUMBER_OF_WORDS_BETWEEN_SAMPLES + 1);
            mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore,
                    mSuggest) {
@@ -488,7 +443,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
    }

    private void resetFeedbackLogging() {
        mFeedbackLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME);
        mFeedbackLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
                System.currentTimeMillis()), mLatinIME);
        mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE);
    }

@@ -612,7 +568,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
        if (mUserRecordingLog != null) {
            mUserRecordingLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS);
        }
        mUserRecordingFile = createUserRecordingFile(mFilesDir);
        mUserRecordingFile = mResearchLogDirectory.getUserRecordingFilePath(
                System.currentTimeMillis());
        mUserRecordingLog = new ResearchLog(mUserRecordingFile, mLatinIME);
        mUserRecordingLogBuffer = new LogBuffer();
        resetRecordingTimer();
+11 −0
Original line number Diff line number Diff line
@@ -26,6 +26,8 @@ public final class ResearchSettings {
            "pref_research_logger_enabled_flag";
    public static final String PREF_RESEARCH_LOGGER_HAS_SEEN_SPLASH =
            "pref_research_logger_has_seen_splash";
    public static final String PREF_RESEARCH_LAST_DIR_CLEANUP_TIME =
            "pref_research_last_dir_cleanup_time";

    private ResearchSettings() {
        // Intentional empty constructor for singleton.
@@ -58,4 +60,13 @@ public final class ResearchSettings {
            final boolean hasSeenSplash) {
        prefs.edit().putBoolean(PREF_RESEARCH_LOGGER_HAS_SEEN_SPLASH, hasSeenSplash).apply();
    }

    public static long readResearchLastDirCleanupTime(final SharedPreferences prefs) {
        return prefs.getLong(PREF_RESEARCH_LAST_DIR_CLEANUP_TIME, 0L);
    }

    public static void writeResearchLastDirCleanupTime(final SharedPreferences prefs,
            final long lastDirCleanupTime) {
        prefs.edit().putLong(PREF_RESEARCH_LAST_DIR_CLEANUP_TIME, lastDirCleanupTime).apply();
    }
}
+4 −14
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
package com.android.inputmethod.research;

import android.Manifest;
import android.app.AlarmManager;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -33,7 +32,6 @@ import com.android.inputmethod.latin.define.ProductionFlag;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -55,12 +53,12 @@ public final class Uploader {
    private static final int BUF_SIZE = 1024 * 8;

    private final Context mContext;
    private final File mFilesDir;
    private final ResearchLogDirectory mResearchLogDirectory;
    private final URL mUrl;

    public Uploader(final Context context) {
        mContext = context;
        mFilesDir = context.getFilesDir();
        mResearchLogDirectory = new ResearchLogDirectory(context);

        final String urlString = context.getString(R.string.research_logger_upload_url);
        if (TextUtils.isEmpty(urlString)) {
@@ -106,16 +104,8 @@ public final class Uploader {
    }

    public void doUpload() {
        if (mFilesDir == null) {
            return;
        }
        final File[] files = mFilesDir.listFiles(new FileFilter() {
            @Override
            public boolean accept(final File pathname) {
                return pathname.getName().startsWith(ResearchLogger.LOG_FILENAME_PREFIX)
                        && !pathname.canWrite();
            }
        });
        final File[] files = mResearchLogDirectory.getUploadableLogFiles();
        if (files == null) return;
        for (final File file : files) {
            uploadFile(file);
        }