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.Manifest; import android.content.Intent; import android.os.Bundle; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.app.NavUtils; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.AppCompatSpinner; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; 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.AdapterView; import android.widget.ArrayAdapter; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.TextView; import com.google.android.apps.forscience.javalib.Success; import com.google.android.apps.forscience.whistlepunk.AccessibilityUtils; 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.PermissionUtils; import com.google.android.apps.forscience.whistlepunk.R; import com.google.android.apps.forscience.whistlepunk.RecordFragment; import com.google.android.apps.forscience.whistlepunk.SensorAppearance; 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.protobuf.nano.InvalidProtocolBufferNanoException; import com.google.android.apps.forscience.whistlepunk.metadata.GoosciSensorTriggerInformation.TriggerInformation; import java.util.ArrayList; import java.util.List; /** * Fragment for adding or editing a trigger. */ public class EditTriggerFragment extends Fragment { private static final String TAG = "EditTriggerFragment"; private static final String ARG_SENSOR_ID = "sensor_id"; private static final String ARG_EXPERIMENT_ID = "experiment_id"; private static final String ARG_SENSOR_LAYOUT = "sensor_layout"; private static final String ARG_TRIGGER_INFO = "trigger_info"; private static final String ARG_TRIGGER_ID = "trigger_id"; private static final String ARG_LAYOUT_POSITION = "sensor_layout_position"; private static final int PERMISSION_VIBRATE = 1; private String mSensorId; private String mExperimentId; private GoosciSensorLayout.SensorLayout mSensorLayout; private SensorTrigger mTriggerToEdit; private AppCompatSpinner mTypeSpinner; private AppCompatSpinner mWhenSpinner; private EditText mValue; private EditText mNoteValue; private CheckBox mAudioAlert; private CheckBox mVisualAlert; private CheckBox mHapticAlert; private int mSensorLayoutPosition; private ViewGroup mNoteGroup; private ViewGroup mAlertGroup; public static EditTriggerFragment newInstance(String sensorId, String experimentId, String triggerId, byte[] triggerInfoBlob, byte[] sensorLayoutBlob, int position, ArrayList<String> triggerOrder) { EditTriggerFragment result = new EditTriggerFragment(); Bundle args = new Bundle(); args.putString(ARG_SENSOR_ID, sensorId); args.putString(ARG_EXPERIMENT_ID, experimentId); args.putString(ARG_TRIGGER_ID, triggerId); args.putByteArray(ARG_SENSOR_LAYOUT, sensorLayoutBlob); args.putByteArray(ARG_TRIGGER_INFO, triggerInfoBlob); args.putInt(ARG_LAYOUT_POSITION, position); args.putStringArrayList(TriggerListFragment.ARG_TRIGGER_ORDER, triggerOrder); result.setArguments(args); return result; } public EditTriggerFragment() { } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mSensorId = getArguments().getString(ARG_SENSOR_ID); mExperimentId = getArguments().getString(ARG_EXPERIMENT_ID); try { mSensorLayout = GoosciSensorLayout.SensorLayout .parseFrom(getArguments().getByteArray(ARG_SENSOR_LAYOUT)); } catch (InvalidProtocolBufferNanoException e) { if (Log.isLoggable(TAG, Log.ERROR)) { Log.e(TAG, "Error parsing the SensorLayout", e); } mSensorLayout = RecordFragment.defaultLayout(); } mSensorLayoutPosition = getArguments().getInt(ARG_LAYOUT_POSITION); String triggerId = getArguments().getString(ARG_TRIGGER_ID, ""); if (!TextUtils.isEmpty(triggerId)) { try { mTriggerToEdit = new SensorTrigger(triggerId, mSensorId, 0, GoosciSensorTriggerInformation.TriggerInformation .parseFrom(getArguments().getByteArray(ARG_TRIGGER_INFO))); } catch (InvalidProtocolBufferNanoException e) { if (Log.isLoggable(TAG, Log.ERROR)) { Log.e(TAG, "Error parsing the SensorTrigger", e); } // We will make a new trigger, since we could not parse the one to edit. mTriggerToEdit = null; } } } @Override public void onResume() { super.onResume(); WhistlePunkApplication.getUsageTracker(getActivity()).trackScreenView(TrackerConstants.SCREEN_TRIGGER_EDIT); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_trigger_edit, parent, false); setHasOptionsMenu(true); mNoteGroup = (ViewGroup) view.findViewById(R.id.note_type_trigger_section); mAlertGroup = (ViewGroup) view.findViewById(R.id.alert_type_trigger_section); mTypeSpinner = (AppCompatSpinner) view.findViewById(R.id.trigger_type_spinner); ArrayAdapter<CharSequence> typeAdapter = ArrayAdapter.createFromResource(getActivity(), R.array.trigger_type_list, android.R.layout.simple_spinner_item); typeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); mTypeSpinner.setAdapter(typeAdapter); mWhenSpinner = (AppCompatSpinner) view.findViewById(R.id.trigger_when_spinner); ArrayAdapter<CharSequence> whenAdapter = ArrayAdapter.createFromResource(getActivity(), R.array.trigger_when_list, android.R.layout.simple_spinner_item); whenAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); mWhenSpinner.setAdapter(whenAdapter); mValue = (EditText) view.findViewById(R.id.value_input); mNoteValue = (EditText) view.findViewById(R.id.trigger_note_text); mAudioAlert = (CheckBox) view.findViewById(R.id.alert_type_audio_selector); mVisualAlert = (CheckBox) view.findViewById(R.id.alert_type_visual_selector); mHapticAlert = (CheckBox) view.findViewById(R.id.alert_type_haptic_selector); if (!PermissionUtils.permissionIsGranted(getActivity(), Manifest.permission.VIBRATE)) { mHapticAlert.setEnabled(PermissionUtils.canRequestAgain(getActivity(), Manifest.permission.VIBRATE)); } ; TextView unitsTextView = (TextView) view.findViewById(R.id.units); String units = AppSingleton.getInstance(getActivity()).getSensorAppearanceProvider() .getAppearance(mSensorId).getUnits(getActivity()); unitsTextView.setText(units); if (!isNewTrigger()) { // Populate the view with the trigger's data. int actionType = mTriggerToEdit.getActionType(); mValue.setText(mTriggerToEdit.getValueToTrigger().toString()); mTypeSpinner.setSelection(actionType); mWhenSpinner.setSelection(mTriggerToEdit.getTriggerWhen()); if (actionType == TriggerInformation.TRIGGER_ACTION_ALERT) { int[] alertTypes = mTriggerToEdit.getAlertTypes(); for (int i = 0; i < alertTypes.length; i++) { int alertType = alertTypes[i]; if (alertType == TriggerInformation.TRIGGER_ALERT_AUDIO) { mAudioAlert.setChecked(true); } else if (alertType == TriggerInformation.TRIGGER_ALERT_VISUAL) { mVisualAlert.setChecked(true); } else if (alertType == TriggerInformation.TRIGGER_ALERT_PHYSICAL) { mHapticAlert.setChecked(true); } } } else if (actionType == TriggerInformation.TRIGGER_ACTION_NOTE) { mNoteValue.setText(mTriggerToEdit.getNoteText()); } updateViewVisibilities(actionType); // Now add the save listeners if this is an edited trigger (not a new trigger). TextWatcher watcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { saveTrigger(); } }; mNoteValue.addTextChangedListener(watcher); mValue.addTextChangedListener(watcher); mWhenSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { saveTrigger(); } @Override public void onNothingSelected(AdapterView<?> parent) { } }); CompoundButton.OnCheckedChangeListener checkedChangeListener = new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { saveTrigger(); } }; mAudioAlert.setOnCheckedChangeListener(checkedChangeListener); mVisualAlert.setOnCheckedChangeListener(checkedChangeListener); } else { // Default to an alert spinner that triggers "at" a value, and does a visual alert. mTypeSpinner.setSelection(TriggerInformation.TRIGGER_ACTION_ALERT); mWhenSpinner.setSelection(TriggerInformation.TRIGGER_WHEN_AT); mVisualAlert.setChecked(true); } mWhenSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { // Hide all but alert types if this is a ABOVE or BELOW. if (position == TriggerInformation.TRIGGER_WHEN_ABOVE || position == TriggerInformation.TRIGGER_WHEN_BELOW) { mTypeSpinner.setSelection(TriggerInformation.TRIGGER_ACTION_ALERT); mTypeSpinner.setEnabled(false); updateViewVisibilities(TriggerInformation.TRIGGER_ACTION_ALERT); selectAlertTypeIfNeeded(); } else { mTypeSpinner.setEnabled(true); } if (!isNewTrigger()) { saveTrigger(); } } @Override public void onNothingSelected(AdapterView<?> parent) { } }); mTypeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { updateViewVisibilities(position); if (position == TriggerInformation.TRIGGER_ACTION_ALERT) { selectAlertTypeIfNeeded(); } if (!isNewTrigger()) { saveTrigger(); } } @Override public void onNothingSelected(AdapterView<?> parent) { } }); mHapticAlert.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { // Don't let them check this checkbox if they deny the vibrate permission. if (!PermissionUtils.tryRequestingPermission(getActivity(), Manifest.permission.VIBRATE, PERMISSION_VIBRATE, true)) { mHapticAlert.setChecked(false); } ; } if (!isNewTrigger()) { saveTrigger(); } } }); return view; } private void updateViewVisibilities(int actionType) { if (actionType == TriggerInformation.TRIGGER_ACTION_START_RECORDING || actionType == TriggerInformation.TRIGGER_ACTION_STOP_RECORDING) { mNoteGroup.setVisibility(View.GONE); mAlertGroup.setVisibility(View.GONE); } else if (actionType == TriggerInformation.TRIGGER_ACTION_ALERT) { mNoteGroup.setVisibility(View.GONE); mAlertGroup.setVisibility(View.VISIBLE); } else if (actionType == TriggerInformation.TRIGGER_ACTION_NOTE) { mNoteGroup.setVisibility(View.VISIBLE); mAlertGroup.setVisibility(View.GONE); } } private void selectAlertTypeIfNeeded() { // Alert type triggers should have the visual alert field checked by default // when the user switches to this type. if (!mHapticAlert.isChecked() && !mVisualAlert.isChecked() && !mAudioAlert.isChecked()) { mVisualAlert.setChecked(true); } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.menu_edit_trigger, menu); ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); actionBar.setDisplayHomeAsUpEnabled(true); if (isNewTrigger()) { actionBar.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); actionBar.setHomeActionContentDescription(android.R.string.cancel); } menu.findItem(R.id.action_save).setVisible(isNewTrigger()); SensorAppearance appearance = AppSingleton.getInstance(getActivity()).getSensorAppearanceProvider() .getAppearance(mSensorId); String triggerTitle = getString( isNewTrigger() ? R.string.title_fragment_add_trigger : R.string.title_fragment_edit_trigger); actionBar.setTitle(String.format(triggerTitle, appearance.getName(getActivity()))); super.onCreateOptionsMenu(menu, inflater); } // Whether we are creating a new trigger or editing an existing trigger. private boolean isNewTrigger() { return mTriggerToEdit == null; } // Calculates the current list of alert types based on which alert checkboxes are selected. private int[] getCurrentAlertTypes() { List<Integer> alertTypesList = new ArrayList<>(); if (mHapticAlert.isChecked()) { alertTypesList.add(TriggerInformation.TRIGGER_ALERT_PHYSICAL); } if (mVisualAlert.isChecked()) { alertTypesList.add(TriggerInformation.TRIGGER_ALERT_VISUAL); } if (mAudioAlert.isChecked()) { alertTypesList.add(TriggerInformation.TRIGGER_ALERT_AUDIO); } int[] alertTypes = new int[alertTypesList.size()]; for (int i = 0; i < alertTypesList.size(); i++) { alertTypes[i] = alertTypesList.get(i); } return alertTypes; } // Saves the trigger based on the current state of the UI and returns to the parent activity. private void saveTriggerAndReturn() { saveTrigger(true); } // Saves the trigger based on the current state of the UI. private void saveTrigger() { saveTrigger(false); } private void saveTrigger(boolean returnToParent) { if (!verifyInput()) { return; } final DataController dc = getDataController(); int triggerType = mTypeSpinner.getSelectedItemPosition(); int triggerWhen = mWhenSpinner.getSelectedItemPosition(); double triggerValue = 0; try { triggerValue = Double.parseDouble(String.valueOf(mValue.getText())); } catch (NumberFormatException ex) { mValue.setError(getActivity().getResources().getString(R.string.cannot_save_invalid_value)); return; } if (isNewTrigger()) { createNewTrigger(dc, triggerType, triggerWhen, triggerValue); } else { updateTrigger(dc, triggerType, triggerWhen, triggerValue, returnToParent); } } // Updates a trigger in the database if it has changed, otherwise just returns to the parent // fragment. private void updateTrigger(DataController dc, int triggerType, int triggerWhen, double triggerValue, final boolean returnToParent) { boolean isUpdated = updateLocalTriggerIfChanged(triggerType, triggerWhen, triggerValue); // Only update and activate if changes were made. if (!isUpdated && returnToParent) { goToParent(); return; } TriggerHelper.addTriggerToLayoutActiveTriggers(mSensorLayout, mTriggerToEdit.getTriggerId()); dc.updateSensorTrigger(mTriggerToEdit, new LoggingConsumer<Success>(TAG, "update trigger") { @Override public void success(Success value) { updateSensorLayoutAndGoToParent(returnToParent); } }); if (returnToParent) { WhistlePunkApplication.getUsageTracker(getActivity()).trackEvent(TrackerConstants.CATEGORY_TRIGGERS, TrackerConstants.ACTION_EDITED, null, 0); } } private boolean updateLocalTriggerIfChanged(int triggerType, int triggerWhen, double triggerValue) { boolean isUpdated = false; if (mTriggerToEdit.getTriggerWhen() != triggerWhen) { mTriggerToEdit.setTriggerWhen(triggerWhen); isUpdated = true; } if (mTriggerToEdit.getValueToTrigger() != triggerValue) { mTriggerToEdit.setValueToTrigger(triggerValue); isUpdated = true; } if (mTriggerToEdit.getActionType() != triggerType) { mTriggerToEdit.setTriggerActionType(triggerType); isUpdated = true; } if (triggerType == TriggerInformation.TRIGGER_ACTION_NOTE) { String noteText = String.valueOf(mNoteValue.getText()); if (!TextUtils.equals(noteText, mTriggerToEdit.getNoteText())) { mTriggerToEdit.setNoteText(String.valueOf(mNoteValue.getText())); isUpdated = true; } } else if (triggerType == TriggerInformation.TRIGGER_ACTION_ALERT) { int[] alertTypes = getCurrentAlertTypes(); if (!SensorTrigger.hasSameAlertTypes(alertTypes, mTriggerToEdit.getAlertTypes())) { mTriggerToEdit.setAlertTypes(alertTypes); isUpdated = true; } } return isUpdated; } // Creates a new trigger in the database, and adds it to the active triggers in the SensorLayout // before returning to the parent fragment. private void createNewTrigger(DataController dc, int triggerType, int triggerWhen, double triggerValue) { SensorTrigger triggerToAdd = null; String triggerId = String.valueOf(System.currentTimeMillis()); if (triggerType == TriggerInformation.TRIGGER_ACTION_START_RECORDING || triggerType == TriggerInformation.TRIGGER_ACTION_STOP_RECORDING) { triggerToAdd = new SensorTrigger(triggerId, mSensorId, triggerWhen, triggerType, triggerValue); } else if (triggerType == TriggerInformation.TRIGGER_ACTION_NOTE) { triggerToAdd = SensorTrigger.newNoteTypeTrigger(triggerId, mSensorId, triggerWhen, String.valueOf(mNoteValue.getText()), triggerValue); } else if (triggerType == TriggerInformation.TRIGGER_ACTION_ALERT) { triggerToAdd = SensorTrigger.newAlertTypeTrigger(triggerId, mSensorId, triggerWhen, getCurrentAlertTypes(), triggerValue); } TriggerHelper.addTriggerToLayoutActiveTriggers(mSensorLayout, triggerId); dc.addSensorTrigger(triggerToAdd, mExperimentId, new LoggingConsumer<Success>(TAG, "add trigger") { @Override public void success(Success value) { updateSensorLayoutAndGoToParent(true); } }); String triggerTypeString = getResources().getStringArray(R.array.trigger_type_list)[triggerType]; String triggerWhenString = getResources().getStringArray(R.array.trigger_when_list)[triggerWhen]; WhistlePunkApplication.getUsageTracker(getActivity()).trackEvent(TrackerConstants.CATEGORY_TRIGGERS, TrackerConstants.ACTION_CREATE, triggerTypeString + " when " + triggerWhenString, 0); } private DataController getDataController() { return AppSingleton.getInstance(getActivity()).getDataController(); } // Verifies that the user has entered valid input to the form. private boolean verifyInput() { // The user must enter a value at which the trigger happens. if (TextUtils.isEmpty(mValue.getText())) { mValue.requestFocus(); mValue.setError(getActivity().getResources().getString(R.string.cannot_save_invalid_value)); return false; } if (mTypeSpinner.getSelectedItemPosition() == TriggerInformation.TRIGGER_ACTION_ALERT && getCurrentAlertTypes().length == 0) { AccessibilityUtils.makeSnackbar(getView(), getResources().getString(R.string.alert_trigger_needs_alerts), Snackbar.LENGTH_LONG).show(); return false; } return true; } // Updates the SensorLayout. This is because each time we edit a trigger, it gets enabled // and added to the active triggers in the SensorLayout. Note that if no changes are made, // the trigger is not re-enabled in the SensorLayout. private void updateSensorLayoutAndGoToParent(final boolean goToParent) { getDataController().updateSensorLayout(mExperimentId, mSensorLayoutPosition, mSensorLayout, new LoggingConsumer<Success>(TAG, "update layout") { @Override public void success(Success value) { if (goToParent) { goToParent(); } } }); } // Returns to the TriggerListActivity. private void goToParent() { if (getActivity() == null) { return; } // To restart the TriggerListActivity, need to pass back the trigger and layout info. Intent upIntent = NavUtils.getParentActivityIntent(getActivity()); upIntent.putExtra(TriggerListActivity.EXTRA_SENSOR_ID, mSensorId); upIntent.putExtra(TriggerListActivity.EXTRA_EXPERIMENT_ID, mExperimentId); upIntent.putExtra(TriggerListActivity.EXTRA_LAYOUT_POSITION, mSensorLayoutPosition); upIntent.putExtra(TriggerListActivity.EXTRA_TRIGGER_ORDER, getArguments().getStringArrayList(TriggerListActivity.EXTRA_TRIGGER_ORDER)); if (getActivity() != null) { NavUtils.navigateUpTo(getActivity(), upIntent); } else { Log.e(TAG, "Can't exit activity because it's no longer there."); } } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == android.R.id.home) { // If this is a new trigger and they didn't explicitly save, just don't save it. // Triggers being edited save at each change, so no need to save that either. goToParent(); return true; } else if (id == R.id.action_save) { // Only available for new triggers. saveTriggerAndReturn(); return true; } return super.onOptionsItemSelected(item); } }