Java tutorial
/* * Copyright 2016 Google Inc. All Rights Reserved. * * 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.google.android.apps.forscience.whistlepunk.metadata; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.support.annotation.VisibleForTesting; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.google.android.apps.forscience.javalib.MaybeConsumer; import com.google.android.apps.forscience.javalib.Success; import com.google.android.apps.forscience.whistlepunk.AppSingleton; import com.google.android.apps.forscience.whistlepunk.DataController; import com.google.android.apps.forscience.whistlepunk.LoggingConsumer; import com.google.android.apps.forscience.whistlepunk.R; import com.google.android.apps.forscience.whistlepunk.StatsAccumulator; import com.google.android.apps.forscience.whistlepunk.data.GoosciSensorLayout; import com.google.android.apps.forscience.whistlepunk.sensorapi.StreamConsumer; import com.google.android.apps.forscience.whistlepunk.sensordb.ScalarReadingList; import com.google.android.apps.forscience.whistlepunk.sensordb.TimeRange; import com.google.common.collect.Range; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; /** * Helper class for cropping. */ public class CropHelper { public static final String TAG = "CropHelper"; // Time buffer for non-overlapping crop: We do not allow crop less than 1 second. // If this is changed, make sure to update R.string.crop_failed_range_too_small as well. public static final long MINIMUM_CROP_MILLIS = 1000; private static final int DATAPOINTS_PER_LOAD = 500; public static final String ACTION_CROP_STATS_RECALCULATED = "action_crop_stats_recalculated"; public static final String EXTRA_SENSOR_ID = "extra_sensor_id"; public static final String EXTRA_RUN_ID = "extra_run_id"; public static final IntentFilter STATS_INTENT_FILTER = new IntentFilter(ACTION_CROP_STATS_RECALCULATED); public static class CropLabels { public ApplicationLabel cropStartLabel; public ApplicationLabel cropEndLabel; public CropLabels() { } } private static class ProcessPriorityThreadFactory implements ThreadFactory { private final int mThreadPriority; public ProcessPriorityThreadFactory(int threadPriority) { super(); mThreadPriority = threadPriority; } @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setPriority(mThreadPriority); return thread; } } public interface CropRunListener { /** * Called when a crop is completed, i.e. the metadata for the experiment is updated. * The min, max and average may not yet be recalculated. */ void onCropCompleted(); /** * Called when a crop fails to complete. No changes were made to the experiment, min, * max or average. */ void onCropFailed(int errorId); } private int mStatsUpdated = 0; private Executor mCropStatsExecutor; private DataController mDataController; public CropHelper(DataController dataController) { this(Executors.newSingleThreadExecutor( new ProcessPriorityThreadFactory(android.os.Process.THREAD_PRIORITY_BACKGROUND)), dataController); } @VisibleForTesting public CropHelper(Executor executor, DataController dataController) { mCropStatsExecutor = executor; mDataController = dataController; } public void cropRun(final Context context, final ExperimentRun run, long startTimestamp, long endTimestamp, final CropRunListener listener) { // Are we trying to crop too wide? Too narrow? Are the timestamps valid? if (startTimestamp < run.getOriginalFirstTimestamp() || run.getOriginalLastTimestamp() < endTimestamp) { listener.onCropFailed(R.string.crop_failed_range_too_large); return; } if (endTimestamp - MINIMUM_CROP_MILLIS <= startTimestamp) { listener.onCropFailed(R.string.crop_failed_range_too_small); return; } final CropLabels cropLabels = run.getCropLabels(); if (cropLabels.cropStartLabel != null && cropLabels.cropEndLabel != null) { // It is already cropped, so we can edit the old crop labels. cropLabels.cropStartLabel.setTimestamp(startTimestamp); cropLabels.cropEndLabel.setTimestamp(endTimestamp); mDataController.editLabel(cropLabels.cropStartLabel, new LoggingConsumer<Label>(TAG, "edit crop start label") { @Override public void success(Label value) { mDataController.editLabel(cropLabels.cropEndLabel, new LoggingConsumer<Label>(TAG, "edit crop end label") { @Override public void success(Label value) { markRunStatsForAdjustment(context, run, listener); } }); } }); } else if (cropLabels.cropStartLabel == null && cropLabels.cropEndLabel == null) { // Otherwise we make new crop labels. ApplicationLabel cropStartLabel = new ApplicationLabel(ApplicationLabel.TYPE_CROP_START, mDataController.generateNewLabelId(), run.getRunId(), startTimestamp); final ApplicationLabel cropEndLabel = new ApplicationLabel(ApplicationLabel.TYPE_CROP_END, mDataController.generateNewLabelId(), run.getRunId(), endTimestamp); cropStartLabel.setExperimentId(run.getExperimentId()); cropEndLabel.setExperimentId(run.getExperimentId()); // Update the run. cropLabels.cropStartLabel = cropStartLabel; cropLabels.cropEndLabel = cropEndLabel; // Add new crop labels to the database. mDataController.addLabel(cropStartLabel, new LoggingConsumer<Label>(TAG, "add crop start label") { @Override public void success(Label value) { mDataController.addLabel(cropEndLabel, new LoggingConsumer<Label>(TAG, "Add crop end label") { @Override public void success(Label value) { markRunStatsForAdjustment(context, run, listener); } }); } }); } else { // One crop label is set and the other is not. This is an error! listener.onCropFailed(R.string.crop_failed_invalid_state); } } private void markRunStatsForAdjustment(final Context context, final ExperimentRun run, final CropRunListener listener) { // First delete the min/max/avg stats, but leave the rest available, because they are used // in loading data by ZoomPresenter. At this point, we can go back to RunReview. final int statsToUpdate = run.getSensorLayouts().size(); mStatsUpdated = 0; for (GoosciSensorLayout.SensorLayout layout : run.getSensorLayouts()) { final String sensorId = layout.sensorId; mDataController.setSensorStatsStatus(run.getRunId(), sensorId, StatsAccumulator.STATUS_NEEDS_UPDATE, new LoggingConsumer<Success>(TAG, "update stats") { @Override public void success(Success success) { mStatsUpdated++; if (mStatsUpdated == statsToUpdate) { listener.onCropCompleted(); } adjustRunStats(context, run, sensorId); } }); } } private void adjustRunStats(final Context context, final ExperimentRun run, final String sensorId) { Runnable runnable = new Runnable() { @Override public void run() { // Moves the current Thread into the background StatsAdjuster adjuster = new StatsAdjuster(sensorId, run, context); adjuster.recalculateStats(mDataController); } }; // Update the stats in the background, without blocking anything. mCropStatsExecutor.execute(runnable); } // A class that recalculates and resaves the stats in a run. private class StatsAdjuster { private final String mSensorId; private final ExperimentRun mExperimentRun; private StatsAccumulator mStatsAccumulator; StreamConsumer mStreamConsumer; private Context mContext; public StatsAdjuster(String sensorId, ExperimentRun run, Context context) { mStatsAccumulator = new StatsAccumulator(); mSensorId = sensorId; mExperimentRun = run; mStreamConsumer = new StreamConsumer() { @Override public void addData(long timestampMillis, double value) { mStatsAccumulator.updateRecordingStreamStats(timestampMillis, value); } }; mContext = context; } public void recalculateStats(DataController dc) { TimeRange range = TimeRange .oldest(Range.closed(mExperimentRun.getFirstTimestamp(), mExperimentRun.getLastTimestamp())); addReadingsToStats(dc, range); } private void addReadingsToStats(final DataController dc, final TimeRange range) { dc.getScalarReadings(mSensorId, /* tier 0 */ 0, range, DATAPOINTS_PER_LOAD, new MaybeConsumer<ScalarReadingList>() { @Override public void success(ScalarReadingList list) { list.deliver(mStreamConsumer); if (list.size() == 0 || list.size() < DATAPOINTS_PER_LOAD || mStatsAccumulator .getLatestTimestamp() >= mExperimentRun.getLastTimestamp()) { if (!mStatsAccumulator.isInitialized()) { // There was no data in this region, so the stats are still // not valid. return; } // Done! Save back to the database. // Note that we only need to save the stats we have changed, because // each stat is stored separately. We do not need to update stats // like zoom tiers and zoom levels. RunStats runStats = mStatsAccumulator.makeSaveableStats(); runStats.putStat(StatsAccumulator.KEY_STATUS, StatsAccumulator.STATUS_VALID); dc.updateRunStats(mExperimentRun.getRunId(), mSensorId, runStats, new LoggingConsumer<Success>(TAG, "update stats") { @Override public void success(Success value) { sendStatsUpdatedBroadcast(mContext, mSensorId, mExperimentRun.getRunId()); } }); } else { TimeRange nextRange = TimeRange.oldest(Range.openClosed( mStatsAccumulator.getLatestTimestamp(), mExperimentRun.getLastTimestamp())); addReadingsToStats(dc, nextRange); } } @Override public void fail(Exception e) { Log.e(TAG, "Error loading data to adjust stats after crop"); } }); } } // Use a Broadcast to tell RunReviewFragment or ExperimentDetailsFragment or anyone who uses // stats that the stats are updated for this sensor on this run. private static void sendStatsUpdatedBroadcast(Context context, String sensorId, String runId) { if (context == null) { return; } // Use a LocalBroadcastManager, because we do not need this broadcast outside the app. LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context); Intent intent = new Intent(); intent.setAction(ACTION_CROP_STATS_RECALCULATED); intent.putExtra(EXTRA_SENSOR_ID, sensorId); intent.putExtra(EXTRA_RUN_ID, runId); lbm.sendBroadcast(intent); } public static void registerStatsBroadcastReceiver(Context context, BroadcastReceiver receiver) { LocalBroadcastManager.getInstance(context).registerReceiver(receiver, STATS_INTENT_FILTER); } public static void unregisterBroadcastReceiver(Context context, BroadcastReceiver receiver) { LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver); } public static boolean experimentIsLongEnoughForCrop(ExperimentRun experimentRun) { return experimentRun.getOriginalLastTimestamp() - experimentRun.getOriginalFirstTimestamp() > CropHelper.MINIMUM_CROP_MILLIS; } public void throwAwayDataOutsideCroppedRegion(DataController dc, ExperimentRun run) { // TODO } }