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.project.experiment; import android.app.Fragment; import android.app.TaskStackBuilder; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.design.widget.AppBarLayout; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; import android.support.v4.app.ActivityOptionsCompat; import android.support.v4.app.NavUtils; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v4.view.ViewCompat; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.PopupMenu; import android.widget.ProgressBar; import android.widget.TextView; import com.bumptech.glide.Glide; import com.google.android.apps.forscience.javalib.MaybeConsumer; import com.google.android.apps.forscience.javalib.Success; import com.google.android.apps.forscience.whistlepunk.AccessibilityUtils; import com.google.android.apps.forscience.whistlepunk.AddNoteDialog; import com.google.android.apps.forscience.whistlepunk.AppSingleton; import com.google.android.apps.forscience.whistlepunk.AxisNumberFormat; import com.google.android.apps.forscience.whistlepunk.DataController; import com.google.android.apps.forscience.whistlepunk.EditNoteDialog; import com.google.android.apps.forscience.whistlepunk.ElapsedTimeFormatter; import com.google.android.apps.forscience.whistlepunk.metadata.SensorTriggerLabel; import com.google.android.apps.forscience.whistlepunk.metadata.TriggerHelper; import com.google.android.apps.forscience.whistlepunk.review.DeleteMetadataItemDialog; import com.google.android.apps.forscience.whistlepunk.scalarchart.ChartController; import com.google.android.apps.forscience.whistlepunk.scalarchart.ChartOptions; import com.google.android.apps.forscience.whistlepunk.scalarchart.ChartView; import com.google.android.apps.forscience.whistlepunk.scalarchart.GraphOptionsController; import com.google.android.apps.forscience.whistlepunk.LoggingConsumer; import com.google.android.apps.forscience.whistlepunk.MainActivity; import com.google.android.apps.forscience.whistlepunk.PictureUtils; import com.google.android.apps.forscience.whistlepunk.R; import com.google.android.apps.forscience.whistlepunk.RecordFragment; import com.google.android.apps.forscience.whistlepunk.scalarchart.ScalarDisplayOptions; import com.google.android.apps.forscience.whistlepunk.SensorAppearance; import com.google.android.apps.forscience.whistlepunk.StatsAccumulator; import com.google.android.apps.forscience.whistlepunk.TransitionUtils; import com.google.android.apps.forscience.whistlepunk.WhistlePunkApplication; import com.google.android.apps.forscience.whistlepunk.analytics.TrackerConstants; import com.google.android.apps.forscience.whistlepunk.data.GoosciSensorLayout; import com.google.android.apps.forscience.whistlepunk.featurediscovery.FeatureDiscoveryListener; import com.google.android.apps.forscience.whistlepunk.featurediscovery.FeatureDiscoveryProvider; import com.google.android.apps.forscience.whistlepunk.metadata.Experiment; import com.google.android.apps.forscience.whistlepunk.metadata.ExperimentRun; import com.google.android.apps.forscience.whistlepunk.metadata.GoosciLabelValue; import com.google.android.apps.forscience.whistlepunk.metadata.Label; import com.google.android.apps.forscience.whistlepunk.metadata.PictureLabel; import com.google.android.apps.forscience.whistlepunk.metadata.RunStats; import com.google.android.apps.forscience.whistlepunk.metadata.TextLabel; import com.google.android.apps.forscience.whistlepunk.project.ProjectDetailsFragment; import com.google.android.apps.forscience.whistlepunk.review.RunReviewActivity; import com.google.android.apps.forscience.whistlepunk.review.RunReviewFragment; import java.lang.ref.WeakReference; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; /** * A fragment to handle displaying Experiment details, runs and labels. */ public class ExperimentDetailsFragment extends Fragment implements AddNoteDialog.AddNoteDialogListener, EditNoteDialog.EditNoteDialogListener, Handler.Callback, DeleteMetadataItemDialog.DeleteDialogListener { public static final String ARG_EXPERIMENT_ID = "experiment_id"; public static final String ARG_CREATE_TASK = "create_task"; private static final String TAG = "ExperimentDetails"; private static final int MSG_SHOW_FEATURE_DISCOVERY = 111; /** * Boolen extra for savedInstanceState with the state of includeArchived experiments. */ private static final String EXTRA_INCLUDE_ARCHIVED = "includeArchived"; private RecyclerView mDetails; private DetailsAdapter mAdapter; FloatingActionButton mObserveButton; private Handler mHandler; private String mExperimentId; private Experiment mExperiment; private ScalarDisplayOptions mScalarDisplayOptions; private GraphOptionsController mGraphOptionsController; private boolean mIncludeArchived; private Toolbar mToolbar; /** * Creates a new instance of this fragment. * * @param experimentId Experiment ID to display * @param createTaskStack If {@code true}, then navigating home requires building a task stack * up to the project details. If {@code false}, use the default * navigation. */ public static ExperimentDetailsFragment newInstance(String experimentId, boolean createTaskStack) { ExperimentDetailsFragment fragment = new ExperimentDetailsFragment(); Bundle args = new Bundle(); args.putString(ARG_EXPERIMENT_ID, experimentId); args.putBoolean(ARG_CREATE_TASK, createTaskStack); fragment.setArguments(args); return fragment; } public ExperimentDetailsFragment() { } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mExperimentId = getArguments().getString(ARG_EXPERIMENT_ID); mHandler = new Handler(this); setHasOptionsMenu(true); } @Override public void onResume() { super.onResume(); getDataController().getExperimentById(mExperimentId, new LoggingConsumer<Experiment>(TAG, "retrieve experiment") { @Override public void success(final Experiment experiment) { if (experiment == null) { // This was deleted on us. getActivity().finish(); } attachExperimentDetails(experiment); loadExperimentData(experiment); } }); WhistlePunkApplication.getUsageTracker(getActivity()) .trackScreenView(TrackerConstants.SCREEN_EXPERIMENT_DETAIL); } @Override public void onPause() { clearDiscovery(); super.onPause(); } @Override public void onDestroy() { mHandler = null; super.onDestroy(); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(EXTRA_INCLUDE_ARCHIVED, mIncludeArchived); mAdapter.onSaveInstanceState(outState); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_experiment_details, container, false); AppCompatActivity activity = (AppCompatActivity) getActivity(); mToolbar = (Toolbar) view.findViewById(R.id.toolbar); activity.setSupportActionBar(mToolbar); ActionBar actionBar = activity.getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setHomeButtonEnabled(true); } mDetails = (RecyclerView) view.findViewById(R.id.details_list); mDetails.setLayoutManager(new LinearLayoutManager(view.getContext(), LinearLayoutManager.VERTICAL, false)); mAdapter = new DetailsAdapter(this, savedInstanceState); mDetails.setAdapter(mAdapter); mObserveButton = (FloatingActionButton) view.findViewById(R.id.observe); mObserveButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { launchObserve(); } }); // TODO: Because mScalarDisplayOptions are static, if the options are changed during the // time we are on this page it probably won't have an effect. Since graph options are // hidden from non-userdebug users, and not shown in the ExperimentDetails menu even when // enabled, this is OK for now. mScalarDisplayOptions = new ScalarDisplayOptions(); mGraphOptionsController = new GraphOptionsController(getActivity()); mGraphOptionsController.loadIntoScalarDisplayOptions(mScalarDisplayOptions, view); FeatureDiscoveryProvider provider = WhistlePunkApplication.getFeatureDiscoveryProvider(getActivity()); if (provider.isEnabled(getActivity(), FeatureDiscoveryProvider.FEATURE_OBSERVE_FAB)) { scheduleDiscovery(); } if (savedInstanceState != null) { mIncludeArchived = savedInstanceState.getBoolean(EXTRA_INCLUDE_ARCHIVED, false); } return view; } private void loadExperimentData(final Experiment experiment) { final DataController dc = getDataController(); dc.getExperimentRuns(experiment.getExperimentId(), mIncludeArchived, new LoggingConsumer<List<ExperimentRun>>(TAG, "loading runs") { @Override public void success(final List<ExperimentRun> runs) { dc.getLabelsForExperiment(experiment, new LoggingConsumer<List<Label>>(TAG, "loading labels") { @Override public void success(List<Label> labels) { mAdapter.setData(experiment, runs, labels, mScalarDisplayOptions); } }); } }); } private void attachExperimentDetails(Experiment experiment) { mExperiment = experiment; final View rootView = getView(); if (rootView == null) { return; } getActivity().setTitle(experiment.getDisplayTitle(getActivity())); Toolbar toolbar = (Toolbar) rootView.findViewById(R.id.toolbar); toolbar.setTitle(experiment.getDisplayTitle(getActivity())); } private DataController getDataController() { return AppSingleton.getInstance(getActivity()).getDataController(); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.menu_experiment_details, menu); } @Override public void onPrepareOptionsMenu(Menu menu) { menu.findItem(R.id.action_archive_experiment).setVisible(mExperiment != null && !mExperiment.isArchived()); menu.findItem(R.id.action_unarchive_experiment).setVisible(mExperiment != null && mExperiment.isArchived()); menu.findItem(R.id.action_delete_experiment).setEnabled(mExperiment != null && mExperiment.isArchived()); } @Override public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { goToProjectDetails(); return true; } else if (itemId == R.id.action_edit_experiment) { UpdateExperimentActivity.launch(getActivity(), mExperimentId, false /* not new */); return true; } else if (itemId == R.id.action_archive_experiment || itemId == R.id.action_unarchive_experiment) { setExperimentArchived(item.getItemId() == R.id.action_archive_experiment); return true; } else if (itemId == R.id.action_experiment_add_note) { launchLabelAdd(); return true; } else if (itemId == R.id.action_include_archived) { item.setChecked(!item.isChecked()); mIncludeArchived = item.isChecked(); loadExperimentData(mExperiment); return true; } else if (itemId == R.id.action_delete_experiment) { confirmDelete(); } return super.onOptionsItemSelected(item); } private void goToProjectDetails() { Intent upIntent = NavUtils.getParentActivityIntent(getActivity()); upIntent.putExtra(ProjectDetailsFragment.ARG_PROJECT_ID, mExperiment.getProjectId()); if (NavUtils.shouldUpRecreateTask(getActivity(), upIntent) || getArguments().getBoolean(ARG_CREATE_TASK, false)) { TaskStackBuilder.create(getActivity()).addNextIntentWithParentStack(upIntent).startActivities(); } else { NavUtils.navigateUpTo(getActivity(), upIntent); } } private void setExperimentArchived(final boolean archived) { mExperiment.setArchived(archived); getDataController().updateExperiment(mExperiment, new LoggingConsumer<Success>(TAG, "Editing experiment") { @Override public void success(Success value) { Snackbar bar = AccessibilityUtils.makeSnackbar(getView(), getActivity().getResources().getString( archived ? R.string.archived_experiment_message : R.string.unarchived_experiment_message), Snackbar.LENGTH_LONG); if (archived) { bar.setAction(R.string.action_undo, new View.OnClickListener() { @Override public void onClick(View v) { setExperimentArchived(false); } }); } bar.show(); getActivity().invalidateOptionsMenu(); } }); // Reload the data to refresh experiment item and insert it if necessary. loadExperimentData(mExperiment); WhistlePunkApplication.getUsageTracker(getActivity()).trackEvent(TrackerConstants.CATEGORY_EXPERIMENTS, archived ? TrackerConstants.ACTION_ARCHIVE : TrackerConstants.ACTION_UNARCHIVE, null, 0); } /** * Sets this as the active experiment and launches observe when done. */ private void launchObserve() { if (mExperiment == null) { return; } getDataController().updateLastUsedExperiment(mExperiment, new LoggingConsumer<Success>(TAG, "updating active experiment") { @Override public void success(Success value) { MainActivity.launch(getActivity(), R.id.navigation_item_observe); } }); } private void launchPicturePreview(PictureLabel label, String timeString, String timeTextContentDescription) { EditNoteDialog dialog = EditNoteDialog.newInstance(label, label.getValue(), timeString, label.getTimeStamp(), timeTextContentDescription); dialog.show(getChildFragmentManager(), EditNoteDialog.TAG); } private void launchLabelEdit(final Label label, String timeString, String timeTextContentDescription) { EditNoteDialog dialog = EditNoteDialog.newInstance(label, label.getValue(), timeString, label.getTimeStamp(), timeTextContentDescription); dialog.show(getChildFragmentManager(), EditNoteDialog.TAG); } private void launchLabelAdd() { Long now = AppSingleton.getInstance(getActivity()).getSensorEnvironment().getDefaultClock().getNow(); AddNoteDialog dialog = AddNoteDialog.newInstance(now, RecordFragment.NOT_RECORDING_RUN_ID, mExperimentId, R.string.add_experiment_note_placeholder_text); dialog.show(getChildFragmentManager(), AddNoteDialog.TAG); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == PictureUtils.REQUEST_TAKE_PHOTO) { Fragment dialog = getChildFragmentManager().findFragmentByTag(AddNoteDialog.TAG); if (dialog != null) { dialog.onActivityResult(requestCode, resultCode, data); } } } @Override public LoggingConsumer<Label> onLabelAdd() { return new LoggingConsumer<Label>(TAG, "add label") { @Override public void success(Label value) { mAdapter.insertNote(value); WhistlePunkApplication.getUsageTracker(getActivity()).trackEvent(TrackerConstants.CATEGORY_NOTES, TrackerConstants.ACTION_CREATE, TrackerConstants.LABEL_EXPERIMENT_DETAIL, TrackerConstants.getLabelValueType(value)); } }; } @Override public MaybeConsumer<Label> onLabelEdit(final Label label) { return new LoggingConsumer<Label>(TAG, "edit label text") { @Override public void success(Label value) { mAdapter.replaceLabelText(label); WhistlePunkApplication.getUsageTracker(getActivity()).trackEvent(TrackerConstants.CATEGORY_NOTES, TrackerConstants.ACTION_EDITED, TrackerConstants.LABEL_EXPERIMENT_DETAIL, TrackerConstants.getLabelValueType(value)); } }; } @Override public void onEditNoteTimestampClicked(Label label, GoosciLabelValue.LabelValue selectedValue, long labelTimestamp) { // No timestamp editing available in Experiments. } @Override public void onAddNoteTimestampClicked(GoosciLabelValue.LabelValue selectedValue, int labelType, long selectedTimestamp) { // No timestamp editing available in Experiments. } private void onLabelDelete(final Label item) { final DataController dc = getDataController(); Snackbar bar = AccessibilityUtils.makeSnackbar(getView(), getActivity().getResources().getString(R.string.snackbar_note_deleted), Snackbar.LENGTH_SHORT); // On undo, re-add the item to the database and the pinned note list. bar.setAction(R.string.snackbar_undo, new View.OnClickListener() { boolean mUndone = false; @Override public void onClick(View v) { if (mUndone) { return; } mUndone = true; Label label; if (item instanceof TextLabel) { String title = ""; title = ((TextLabel) item).getText(); label = new TextLabel(title, dc.generateNewLabelId(), item.getRunId(), item.getTimeStamp()); } else if (item instanceof PictureLabel) { label = new PictureLabel(((PictureLabel) item).getFilePath(), ((PictureLabel) item).getCaption(), dc.generateNewLabelId(), item.getRunId(), item.getTimeStamp()); } else if (item instanceof SensorTriggerLabel) { label = new SensorTriggerLabel(dc.generateNewLabelId(), item.getRunId(), item.getTimeStamp(), item.getValue()); } else { // This is not a label we know how to deal with. return; } label.setExperimentId(item.getExperimentId()); dc.addLabel(label, new LoggingConsumer<Label>(TAG, "re-add deleted label") { @Override public void success(Label label) { mAdapter.insertNote(label); WhistlePunkApplication.getUsageTracker(getActivity()).trackEvent( TrackerConstants.CATEGORY_NOTES, TrackerConstants.ACTION_DELETE_UNDO, TrackerConstants.LABEL_EXPERIMENT_DETAIL, TrackerConstants.getLabelValueType(item)); } }); } }); // Delete the item immediately, and remove it from the pinned note list. dc.deleteLabel(item, new LoggingConsumer<Success>(TAG, "delete label") { @Override public void success(Success value) { mAdapter.deleteNote(item); } }); bar.show(); WhistlePunkApplication.getUsageTracker(getActivity()).trackEvent(TrackerConstants.CATEGORY_NOTES, TrackerConstants.ACTION_DELETED, TrackerConstants.LABEL_EXPERIMENT_DETAIL, TrackerConstants.getLabelValueType(item)); } @Override public boolean handleMessage(Message msg) { if (msg.what == MSG_SHOW_FEATURE_DISCOVERY) { showFeatureDiscovery(); } return false; } private void scheduleDiscovery() { mHandler.sendEmptyMessageDelayed(MSG_SHOW_FEATURE_DISCOVERY, FeatureDiscoveryProvider.FEATURE_DISCOVERY_SHOW_DELAY_MS); } private void clearDiscovery() { mHandler.removeMessages(MSG_SHOW_FEATURE_DISCOVERY); } private void showFeatureDiscovery() { if (getActivity() == null) { return; } FeatureDiscoveryProvider provider = WhistlePunkApplication.getFeatureDiscoveryProvider(getActivity()); provider.show(FeatureDiscoveryProvider.FEATURE_OBSERVE_FAB, ((AppCompatActivity) getActivity()).getSupportFragmentManager(), mObserveButton, new FeatureDiscoveryListener() { @Override public void onClick(String feature) { launchObserve(); } }, getResources().getDrawable(R.drawable.ic_observe_white_24dp)); } private void confirmDelete() { DeleteMetadataItemDialog dialog = DeleteMetadataItemDialog .newInstance(R.string.delete_experiment_dialog_title, R.string.delete_experiment_dialog_message); dialog.show(getChildFragmentManager(), DeleteMetadataItemDialog.TAG); } @Override public void requestDelete(Bundle extras) { getDataController().deleteExperiment(mExperiment, new LoggingConsumer<Success>(TAG, "Delete experiment") { @Override public void success(Success value) { getActivity().finish(); } }); } private void setToolbarScrollFlags(boolean emptyView) { AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) mToolbar.getLayoutParams(); if (emptyView) { // Don't scroll the toolbar if empty view is showing. params.setScrollFlags(0); } else { params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS); } mToolbar.setLayoutParams(params); } public static class DetailsAdapter extends RecyclerView.Adapter<DetailsAdapter.ViewHolder> { private static final String KEY_SAVED_SENSOR_INDICES = "savedSensorIndices"; private static final int VIEW_TYPE_EXPERIMENT_DESCRIPTION = 0; private static final int VIEW_TYPE_EXPERIMENT_TEXT_LABEL = 1; private static final int VIEW_TYPE_EXPERIMENT_PICTURE_LABEL = 2; private static final int VIEW_TYPE_RUN_CARD = 3; private static final int VIEW_TYPE_EMPTY = 4; private static final int VIEW_TYPE_EXPERIMENT_TRIGGER_LABEL = 5; private final WeakReference<ExperimentDetailsFragment> mParentReference; private Experiment mExperiment; private List<ExperimentDetailItem> mItems; private List<Integer> mSensorIndices = null; DetailsAdapter(ExperimentDetailsFragment parent, Bundle savedInstanceState) { mItems = new ArrayList<>(); mParentReference = new WeakReference<ExperimentDetailsFragment>(parent); if (savedInstanceState != null && savedInstanceState.containsKey(KEY_SAVED_SENSOR_INDICES)) { mSensorIndices = savedInstanceState.getIntegerArrayList(KEY_SAVED_SENSOR_INDICES); } } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view; LayoutInflater inflater = LayoutInflater.from(parent.getContext()); if (viewType == VIEW_TYPE_EXPERIMENT_TEXT_LABEL || viewType == VIEW_TYPE_EXPERIMENT_PICTURE_LABEL || viewType == VIEW_TYPE_EXPERIMENT_TRIGGER_LABEL) { view = inflater.inflate(R.layout.exp_card_pinned_note, parent, false); } else if (viewType == VIEW_TYPE_EXPERIMENT_DESCRIPTION) { view = inflater.inflate(R.layout.metadata_description, parent, false); } else if (viewType == VIEW_TYPE_EMPTY) { view = inflater.inflate(R.layout.empty_list, parent, false); } else { view = inflater.inflate(R.layout.exp_card_run, parent, false); } return new ViewHolder(view, viewType); } @Override public void onBindViewHolder(ViewHolder holder, int position) { ExperimentDetailItem item = mItems.get(position); int type = item.getViewType(); if (type == VIEW_TYPE_RUN_CARD) { bindRun(holder, item); } boolean isPictureLabel = type == VIEW_TYPE_EXPERIMENT_PICTURE_LABEL; boolean isTextLabel = type == VIEW_TYPE_EXPERIMENT_TEXT_LABEL; boolean isTriggerLabel = type == VIEW_TYPE_EXPERIMENT_TRIGGER_LABEL; if (isPictureLabel || isTextLabel || isTriggerLabel) { // TODO: Can this code be reused from PinnedNoteAdapter? TextView textView = (TextView) holder.itemView.findViewById(R.id.note_text); TextView autoTextView = (TextView) holder.itemView.findViewById(R.id.auto_note_text); final Label label = isPictureLabel ? mItems.get(position).mPictureLabel : isTextLabel ? mItems.get(position).mTextLabel : mItems.get(position).mSensorTriggerLabel; String text = isPictureLabel ? ((PictureLabel) label).getCaption() : isTextLabel ? ((TextLabel) label).getText() : ((SensorTriggerLabel) label).getCustomText(); if (!TextUtils.isEmpty(text)) { textView.setText(text); textView.setTextColor(textView.getResources().getColor(R.color.text_color_black)); } else { textView.setText( textView.getResources().getString(isPictureLabel ? R.string.picture_note_caption_hint : R.string.pinned_note_placeholder_text)); textView.setTextColor(textView.getResources().getColor(R.color.text_color_light_grey)); } Context appContext = holder.itemView.getContext().getApplicationContext(); final String timeString = formatAbsoluteTime(appContext, label.getTimeStamp()).toString(); ((TextView) holder.itemView.findViewById(R.id.duration_text)).setText(timeString); setupNoteMenu(label, holder.itemView.findViewById(R.id.note_menu_button), timeString); ImageView imageView = (ImageView) holder.itemView.findViewById(R.id.note_image); if (isPictureLabel) { imageView.setVisibility(View.VISIBLE); Glide.with(imageView.getContext()).load(((PictureLabel) label).getFilePath()).into(imageView); View.OnClickListener clickListener = new View.OnClickListener() { @Override public void onClick(View v) { if (mParentReference.get() != null) { mParentReference.get().launchPicturePreview((PictureLabel) label, timeString, timeString); } } }; holder.itemView.setOnClickListener(clickListener); autoTextView.setVisibility(View.GONE); } else { holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mParentReference.get() != null) { mParentReference.get().launchLabelEdit(label, timeString, timeString); } } }); imageView.setVisibility(View.GONE); if (isTriggerLabel) { autoTextView.setVisibility(View.VISIBLE); String autoText = ((SensorTriggerLabel) label).getAutogenText(); TriggerHelper.populateAutoTextViews(autoTextView, autoText, R.drawable.ic_label_black_18dp, autoTextView.getResources()); } } } if (type == VIEW_TYPE_EXPERIMENT_DESCRIPTION) { TextView description = (TextView) holder.itemView.findViewById(R.id.metadata_description); description.setBackgroundColor(description.getResources().getColor(R.color.color_accent_dark)); description.setText(mExperiment.getDescription()); description .setVisibility(TextUtils.isEmpty(mExperiment.getDescription()) ? View.GONE : View.VISIBLE); View archivedIndicator = holder.itemView.findViewById(R.id.archived_indicator); archivedIndicator.setVisibility(mExperiment.isArchived() ? View.VISIBLE : View.GONE); } else if (type == VIEW_TYPE_EMPTY) { TextView view = (TextView) holder.itemView; view.setText(R.string.empty_experiment); view.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, view.getResources().getDrawable(R.drawable.empty_run)); } } private void setupNoteMenu(final Label label, final View menu, final String timeString) { menu.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Context context = menu.getContext(); PopupMenu popup = new PopupMenu(context, menu); popup.getMenuInflater().inflate(R.menu.menu_note, popup.getMenu()); popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { if (item.getItemId() == R.id.btn_edit_note) { if (mParentReference.get() != null) { mParentReference.get().launchLabelEdit(label, timeString, timeString); } return true; } else if (item.getItemId() == R.id.btn_delete_note) { if (mParentReference.get() != null) { mParentReference.get().onLabelDelete(label); } return true; } return false; } }); popup.show(); } }); } public void replaceLabelText(Label label) { if (!(label instanceof TextLabel || label instanceof PictureLabel || label instanceof SensorTriggerLabel)) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "How did we try to replace text on a non-text label?"); } return; } int position = findLabelIndex(label); if (position == -1) { return; } if (label instanceof TextLabel) { mItems.get(position).mTextLabel.setText(((TextLabel) label).getText()); } else if (label instanceof PictureLabel) { mItems.get(position).mPictureLabel.setCaption(((PictureLabel) label).getCaption()); } else if (label instanceof SensorTriggerLabel) { mItems.get(position).mSensorTriggerLabel .setCustomText(((SensorTriggerLabel) label).getCustomText()); } notifyItemChanged(position); } public void deleteNote(Label label) { int position = findLabelIndex(label); if (position == -1) { return; } mItems.remove(position); notifyItemRemoved(position); updateEmptyView(); } private int findLabelIndex(Label label) { int expectedViewType = label instanceof TextLabel ? VIEW_TYPE_EXPERIMENT_TEXT_LABEL : label instanceof PictureLabel ? VIEW_TYPE_EXPERIMENT_PICTURE_LABEL : VIEW_TYPE_EXPERIMENT_TRIGGER_LABEL; int position = -1; int size = mItems.size(); for (int i = 0; i < size; i++) { ExperimentDetailItem item = mItems.get(i); if (item.getViewType() == expectedViewType) { Label itemLabel = label instanceof TextLabel ? item.mTextLabel : label instanceof PictureLabel ? item.mPictureLabel : item.mSensorTriggerLabel; if (TextUtils.equals(label.getLabelId(), itemLabel.getLabelId())) { position = i; break; } } } return position; } public void insertNote(Label label) { int size = mItems.size(); long timestamp = label.getTimeStamp(); boolean inserted = false; int emptyIndex = -1; for (int i = 0; i < size; i++) { ExperimentDetailItem item = mItems.get(i); if (item.getViewType() == VIEW_TYPE_EXPERIMENT_DESCRIPTION) { continue; } else if (item.getViewType() == VIEW_TYPE_EMPTY) { emptyIndex = i; continue; } if (timestamp > item.getTimestamp()) { mItems.add(i, new ExperimentDetailItem(label)); notifyItemInserted(i); inserted = true; break; } } if (!inserted) { mItems.add(size, new ExperimentDetailItem(label)); notifyItemInserted(size); } // Remove the empty type if necessary. if (emptyIndex > -1) { removeEmptyView(emptyIndex, true); } } @Override public int getItemCount() { return mItems.size(); } @Override public int getItemViewType(int position) { return mItems.get(position).getViewType(); } public void setData(Experiment experiment, List<ExperimentRun> runs, List<Label> labels, ScalarDisplayOptions scalarDisplayOptions) { boolean hasRunsOrLabels = false; mExperiment = experiment; // TODO: compare data and see if anything has changed. If so, don't reload at all. mItems.clear(); // As a safety check, if mSensorIndices is not the same size as the run list, // just ignore it. if (mSensorIndices != null && mSensorIndices.size() != runs.size()) { mSensorIndices = null; } int i = 0; for (ExperimentRun run : runs) { ExperimentDetailItem item = new ExperimentDetailItem(run, scalarDisplayOptions); item.setSensorTagIndex(mSensorIndices != null ? mSensorIndices.get(i++) : 0); mItems.add(item); hasRunsOrLabels = true; } for (Label label : labels) { if (TextUtils.equals(label.getRunId(), RecordFragment.NOT_RECORDING_RUN_ID)) { if (ExperimentDetailItem.canShowLabel(label)) { mItems.add(new ExperimentDetailItem(label)); hasRunsOrLabels = true; } } } sortItems(); if (!TextUtils.isEmpty(experiment.getDescription()) || experiment.isArchived()) { mItems.add(0, new ExperimentDetailItem(VIEW_TYPE_EXPERIMENT_DESCRIPTION)); } if (!hasRunsOrLabels) { addEmptyView(false /* don't notify */); } if (mParentReference.get() != null) { mParentReference.get().setToolbarScrollFlags(!hasRunsOrLabels); } notifyDataSetChanged(); } /** * Checks to see if we have any labels or runs. If so, hides the empty view. Otherwise, * add the empty view at the right location. */ private void updateEmptyView() { boolean hasRunsOrLabels = false; int emptyIndex = -1; final int count = mItems.size(); for (int index = 0; index < count; ++index) { int viewType = mItems.get(index).getViewType(); switch (viewType) { case VIEW_TYPE_EMPTY: emptyIndex = index; break; case VIEW_TYPE_RUN_CARD: case VIEW_TYPE_EXPERIMENT_PICTURE_LABEL: case VIEW_TYPE_EXPERIMENT_TEXT_LABEL: case VIEW_TYPE_EXPERIMENT_TRIGGER_LABEL: hasRunsOrLabels = true; break; } if (hasRunsOrLabels) { // Don't need to look anymore. break; } } if (hasRunsOrLabels && emptyIndex != -1) { // We have runs but there is an empty item. Remove it. removeEmptyView(emptyIndex, true); } else if (!hasRunsOrLabels && emptyIndex == -1) { // We have no runs or labels and no empty item. Add it to the end. addEmptyView(true); } } void addEmptyView(boolean notifyAdd) { mItems.add(new ExperimentDetailItem(VIEW_TYPE_EMPTY)); if (notifyAdd) { notifyItemInserted(mItems.size() - 1); } if (mParentReference.get() != null) { mParentReference.get().setToolbarScrollFlags(true /* has an empty view*/); } } void removeEmptyView(int location, boolean notifyRemove) { mItems.remove(location); if (notifyRemove) { notifyItemRemoved(location); } if (mParentReference.get() != null) { mParentReference.get().setToolbarScrollFlags(false /* has an empty view*/); } } void bindRun(final ViewHolder holder, final ExperimentDetailItem item) { final ExperimentRun run = item.getRun(); final Context applicationContext = holder.itemView.getContext().getApplicationContext(); holder.setRunId(run.getRunId()); String title = run.getRunTitle(applicationContext); holder.runTitle.setText(title); holder.date.setText(run.getDisplayTime(applicationContext)); ElapsedTimeFormatter formatter = ElapsedTimeFormatter.getInstance(applicationContext); holder.duration.setText(formatter.format(run.elapsedSeconds())); holder.duration.setContentDescription(formatter.formatForAccessibility(run.elapsedSeconds())); if (run.getNoteCount() > 0) { holder.noteCount.setVisibility(View.VISIBLE); holder.noteCount.setText(applicationContext.getResources().getQuantityString(R.plurals.notes_count, run.getNoteCount(), run.getNoteCount())); } else { holder.noteCount.setVisibility(View.GONE); } holder.itemView.setOnClickListener(createRunClickListener(item.getSensorTagIndex())); holder.itemView.setTag(R.id.run_title_text, run.getRunId()); holder.itemView.setAlpha(applicationContext.getResources().getFraction( run.isArchived() ? R.fraction.metadata_card_archived_alpha : R.fraction.metadata_card_alpha, 1, 1)); View archivedIndicator = holder.itemView.findViewById(R.id.archived_indicator); archivedIndicator.setVisibility(run.isArchived() ? View.VISIBLE : View.GONE); if (run.isArchived()) { holder.runTitle.setContentDescription( applicationContext.getResources().getString(R.string.archived_content_description, title)); } ViewCompat.setTransitionName(holder.itemView, run.getRunId()); if (run.getSensorTags().size() > 0) { loadSensorData(applicationContext, holder, item); holder.sensorNext.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //Sometimes we tap the button before it can disable so return if the button //should be disabled. if (item.getSensorTagIndex() >= item.getRun().getSensorTags().size() - 1) return; item.setSensorTagIndex(item.getSensorTagIndex() + 1); loadSensorData(applicationContext, holder, item); GoosciSensorLayout.SensorLayout layout = item.getSelectedSensorLayout(); holder.itemView.setOnClickListener(createRunClickListener(item.getSensorTagIndex())); holder.setSensorLayout(layout); } }); holder.sensorPrev.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //Sometimes we tap the button before it can disable so return if the button //should be disabled. if (item.getSensorTagIndex() == 0) return; item.setSensorTagIndex(item.getSensorTagIndex() - 1); loadSensorData(applicationContext, holder, item); GoosciSensorLayout.SensorLayout layout = item.getSelectedSensorLayout(); holder.itemView.setOnClickListener(createRunClickListener(item.getSensorTagIndex())); holder.setSensorLayout(layout); } }); } else { holder.sensorName.setText(""); holder.clearSensorLayout(); setIndeterminateSensorData(holder); } } private View.OnClickListener createRunClickListener(final int selectedSensorIndex) { return new View.OnClickListener() { @Override public void onClick(View v) { AppCompatActivity activity = (AppCompatActivity) v.getContext(); String runId = (String) v.getTag(R.id.run_title_text); ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, TransitionUtils.getTransitionPairs(activity, v, runId)); RunReviewActivity.launch(v.getContext(), runId, selectedSensorIndex, false /* from record */, false /* create task */, options.toBundle()); } }; } private void setIndeterminateSensorData(ViewHolder holder) { holder.min.setText(R.string.indeterminate_value); holder.max.setText(R.string.indeterminate_value); holder.avg.setText(R.string.indeterminate_value); } private void loadSensorData(Context appContext, final ViewHolder holder, final ExperimentDetailItem item) { final ExperimentRun run = item.getRun(); final NumberFormat numberFormat = new AxisNumberFormat(); holder.statsLoadStatus = ViewHolder.STATS_LOAD_STATUS_LOADING; final String sensorTag = run.getSensorTags().get(item.getSensorTagIndex()); final String runId = run.getRunId(); final SensorAppearance appearance = AppSingleton.getInstance(appContext).getSensorAppearanceProvider() .getAppearance(sensorTag); holder.sensorName.setText(appearance.getSensorDisplayName(appContext)); final GoosciSensorLayout.SensorLayout sensorLayout = item.getSelectedSensorLayout(); appearance.applyDrawableToImageView(holder.sensorImage, sensorLayout.color); boolean hasNextButton = item.getSensorTagIndex() < run.getSensorTags().size() - 1; boolean hasPrevButton = item.getSensorTagIndex() > 0; holder.sensorPrev.setVisibility(hasPrevButton ? View.VISIBLE : View.INVISIBLE); holder.sensorNext.setVisibility(hasNextButton ? View.VISIBLE : View.INVISIBLE); if (hasNextButton) { RunReviewFragment.updateContentDescription(holder.sensorNext, R.string.run_review_switch_sensor_btn_next, item.getNextSensorId(), appContext); } if (hasPrevButton) { RunReviewFragment.updateContentDescription(holder.sensorPrev, R.string.run_review_switch_sensor_btn_prev, item.getPrevSensorId(), appContext); } setIndeterminateSensorData(holder); final DataController dc = AppSingleton.getInstance(appContext).getDataController(); dc.getStats(run.getRunId(), run.getSensorTags().get(item.getSensorTagIndex()), new LoggingConsumer<RunStats>(TAG, "loading stats") { @Override public void success(RunStats stats) { // Only load this if the holder run value is the same. Otherwise, // bail. if (!runId.equals(holder.getRunId())) { return; } holder.statsLoadStatus = ViewHolder.STATS_LOAD_STATUS_IDLE; holder.min.setText(stats.hasStat(StatsAccumulator.KEY_MIN) ? numberFormat.format(stats.getStat(StatsAccumulator.KEY_MIN)) : ""); holder.max.setText(stats.hasStat(StatsAccumulator.KEY_MAX) ? numberFormat.format(stats.getStat(StatsAccumulator.KEY_MAX)) : ""); holder.avg.setText(stats.hasStat(StatsAccumulator.KEY_AVERAGE) ? numberFormat.format(stats.getStat(StatsAccumulator.KEY_AVERAGE)) : ""); // Save the sensor stats so they can be used to set the Y axis on load. // If too many sensors are loaded in quick succession, having // RunStats be a final local variable may cause the Y axis to be set // inappropriately. holder.currentSensorStats = stats; // Load sensor readings into a chart. final ChartController chartController = item.getChartController(); chartController.setChartView(holder.chartView); chartController.setProgressView(holder.progressView); holder.setSensorLayout(sensorLayout); chartController.loadRunData(run, sensorLayout, dc, holder, stats, new ChartController.ChartDataLoadedCallback() { @Override public void onChartDataLoaded(long firstTimestamp, long lastTimestamp) { // Display the graph. chartController.setXAxisWithBuffer(firstTimestamp, lastTimestamp); chartController.setYAxisWithBuffer( holder.currentSensorStats.getStat(StatsAccumulator.KEY_MIN), holder.currentSensorStats.getStat(StatsAccumulator.KEY_MAX)); } @Override public void onLoadAttemptStarted() { } }); } }); } void sortItems() { Collections.sort(mItems, new Comparator<ExperimentDetailItem>() { @Override public int compare(ExperimentDetailItem lhs, ExperimentDetailItem rhs) { return Long.compare(rhs.getTimestamp(), lhs.getTimestamp()); } }); } public void onSaveInstanceState(Bundle outState) { ArrayList<Integer> selectedIndices = new ArrayList<>(); int size = getItemCount(); for (int i = 0; i < size; i++) { if (getItemViewType(i) == VIEW_TYPE_RUN_CARD) { selectedIndices.add(mItems.get(i).getSensorTagIndex()); } } outState.putIntegerArrayList(KEY_SAVED_SENSOR_INDICES, selectedIndices); } public static class ViewHolder extends RecyclerView.ViewHolder implements ChartController.ChartLoadingStatus { static final int STATS_LOAD_STATUS_IDLE = 0; static final int STATS_LOAD_STATUS_LOADING = 1; private int mGraphLoadStatus; // Keep track of the loading state and what should currently be displayed: // Loads are done on a background thread, so as cards are scrolled or sensors are // updated we need to track what needs to be reloaded. private String mRunId; private GoosciSensorLayout.SensorLayout mSensorLayout; final int mViewType; int statsLoadStatus; // Run members. TextView runTitle; TextView date; TextView duration; TextView noteCount; ChartView chartView; // Stats members. TextView min; TextView max; TextView avg; TextView sensorName; ImageView sensorImage; ImageButton sensorPrev; ImageButton sensorNext; public ProgressBar progressView; public RunStats currentSensorStats; public ViewHolder(View itemView, int viewType) { super(itemView); mViewType = viewType; if (mViewType == VIEW_TYPE_RUN_CARD) { runTitle = (TextView) itemView.findViewById(R.id.run_title_text); date = (TextView) itemView.findViewById(R.id.run_details_text); noteCount = (TextView) itemView.findViewById(R.id.notes_count); DrawableCompat.setTint(noteCount.getCompoundDrawablesRelative()[0].mutate(), noteCount.getResources().getColor(R.color.text_color_light_grey)); duration = (TextView) itemView.findViewById(R.id.run_review_duration); min = (TextView) itemView.findViewById(R.id.stats_view_min); max = (TextView) itemView.findViewById(R.id.stats_view_max); avg = (TextView) itemView.findViewById(R.id.stats_view_avg); sensorName = (TextView) itemView.findViewById(R.id.run_review_sensor_name); sensorPrev = (ImageButton) itemView.findViewById(R.id.run_review_switch_sensor_btn_prev); sensorNext = (ImageButton) itemView.findViewById(R.id.run_review_switch_sensor_btn_next); chartView = (ChartView) itemView.findViewById(R.id.chart_view); sensorImage = (ImageView) itemView.findViewById(R.id.sensor_icon); progressView = (ProgressBar) itemView.findViewById(R.id.chart_progress); } } public void setRunId(String runId) { this.mRunId = runId; } public void setSensorLayout(GoosciSensorLayout.SensorLayout sensorLayout) { this.mSensorLayout = sensorLayout; } public void clearSensorLayout() { this.mSensorLayout.clear(); } @Override public int getGraphLoadStatus() { return mGraphLoadStatus; } @Override public void setGraphLoadStatus(int graphLoadStatus) { this.mGraphLoadStatus = graphLoadStatus; } @Override public String getRunId() { return mRunId; } @Override public GoosciSensorLayout.SensorLayout getSensorLayout() { return mSensorLayout; } } /** * Represents a detail item: either a run or an experiment level label or a special card. * <p> * TODO: might be able to rework this when Run objects exist. */ public static class ExperimentDetailItem { private final int mViewType; private ExperimentRun mRun; private int mSensorTagIndex = -1; private TextLabel mTextLabel; private PictureLabel mPictureLabel; private SensorTriggerLabel mSensorTriggerLabel; private long mTimestamp; private ChartController mChartController; ExperimentDetailItem(ExperimentRun run, ScalarDisplayOptions scalarDisplayOptions) { mRun = run; mTimestamp = mRun.getFirstTimestamp(); mViewType = VIEW_TYPE_RUN_CARD; mSensorTagIndex = run.getSensorTags().size() > 0 ? 0 : -1; mChartController = new ChartController(ChartOptions.ChartPlacementType.TYPE_PREVIEW_REVIEW, scalarDisplayOptions); } public static boolean canShowLabel(Label label) { return label instanceof TextLabel || label instanceof PictureLabel || label instanceof SensorTriggerLabel; } ExperimentDetailItem(Label label) { if (label instanceof TextLabel) { mTextLabel = (TextLabel) label; mViewType = VIEW_TYPE_EXPERIMENT_TEXT_LABEL; } else if (label instanceof PictureLabel) { mPictureLabel = (PictureLabel) label; mViewType = VIEW_TYPE_EXPERIMENT_PICTURE_LABEL; } else { mSensorTriggerLabel = (SensorTriggerLabel) label; mViewType = VIEW_TYPE_EXPERIMENT_TRIGGER_LABEL; } mTimestamp = label.getTimeStamp(); } ExperimentDetailItem(int viewType) { mViewType = viewType; } int getViewType() { return mViewType; } long getTimestamp() { return mTimestamp; } ExperimentRun getRun() { return mRun; } int getSensorTagIndex() { return mSensorTagIndex; } GoosciSensorLayout.SensorLayout getSelectedSensorLayout() { return mRun.getSensorLayouts().get(mSensorTagIndex); } String getNextSensorId() { return mRun.getSensorTags().get(mSensorTagIndex + 1); } String getPrevSensorId() { return mRun.getSensorTags().get(mSensorTagIndex - 1); } void setSensorTagIndex(int index) { mSensorTagIndex = index; } ChartController getChartController() { return mChartController; } } public static CharSequence formatAbsoluteTime(Context applicationContext, long timestampms) { return DateUtils.getRelativeDateTimeString(applicationContext, timestampms, DateUtils.MINUTE_IN_MILLIS, DateUtils.MINUTE_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE); } } }