Merge branch 'feature/refactoring' into dev

pull/30/head
Alinson S. Xavier 10 years ago
commit ba31dee16a

@ -41,7 +41,7 @@ public class DeleteHabitsCommand extends Command
@Override
public void undo()
{
throw new UnsupportedOperationException();
}
public Integer getExecuteStringId()

@ -33,8 +33,8 @@ public class EditHabitCommand extends Command
this.modified = new Habit(modified);
this.original = new Habit(original);
hasIntervalChanged = (this.original.freqDen != this.modified.freqDen ||
this.original.freqNum != this.modified.freqNum);
hasIntervalChanged = (!this.original.freqDen.equals(this.modified.freqDen) ||
!this.original.freqNum.equals(this.modified.freqNum));
}
public void execute()

@ -0,0 +1,268 @@
/* Copyright (C) 2016 Alinson Santos Xavier
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.dialogs;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ProgressBar;
import com.android.colorpicker.ColorPickerDialog;
import com.android.colorpicker.ColorPickerSwatch;
import org.isoron.helpers.ColorHelper;
import org.isoron.helpers.DialogHelper;
import org.isoron.helpers.ReplayableActivity;
import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.ArchiveHabitsCommand;
import org.isoron.uhabits.commands.ChangeHabitColorCommand;
import org.isoron.uhabits.commands.DeleteHabitsCommand;
import org.isoron.uhabits.commands.UnarchiveHabitsCommand;
import org.isoron.uhabits.fragments.EditHabitFragment;
import org.isoron.uhabits.io.CSVExporter;
import org.isoron.uhabits.loaders.HabitListLoader;
import org.isoron.uhabits.models.Habit;
import java.io.File;
import java.util.LinkedList;
import java.util.List;
public class HabitSelectionCallback implements ActionMode.Callback
{
private HabitListLoader loader;
private List<Integer> selectedPositions;
private ReplayableActivity activity;
private Listener listener;
private DialogHelper.OnSavedListener onSavedListener;
private ProgressBar progressBar;
public interface Listener
{
void onActionModeDestroyed(ActionMode mode);
}
public HabitSelectionCallback(ReplayableActivity activity, HabitListLoader loader)
{
this.activity = activity;
this.loader = loader;
selectedPositions = new LinkedList<>();
}
public void setListener(Listener listener)
{
this.listener = listener;
}
public void setProgressBar(ProgressBar progressBar)
{
this.progressBar = progressBar;
}
public void setOnSavedListener(DialogHelper.OnSavedListener onSavedListener)
{
this.onSavedListener = onSavedListener;
}
public void setSelectedPositions(List<Integer> selectedPositions)
{
this.selectedPositions = selectedPositions;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu)
{
activity.getMenuInflater().inflate(R.menu.list_habits_context, menu);
updateTitle(mode);
updateActions(menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu)
{
updateTitle(mode);
updateActions(menu);
return true;
}
private void updateActions(Menu menu)
{
boolean showEdit = (selectedPositions.size() == 1);
boolean showColor = true;
boolean showArchive = true;
boolean showUnarchive = true;
if (showEdit) showColor = false;
for (int i : selectedPositions)
{
Habit h = loader.habitsList.get(i);
if (h.isArchived())
{
showColor = false;
showArchive = false;
}
else showUnarchive = false;
}
MenuItem itemEdit = menu.findItem(R.id.action_edit_habit);
MenuItem itemColor = menu.findItem(R.id.action_color);
MenuItem itemArchive = menu.findItem(R.id.action_archive_habit);
MenuItem itemUnarchive = menu.findItem(R.id.action_unarchive_habit);
itemEdit.setVisible(showEdit);
itemColor.setVisible(showColor);
itemArchive.setVisible(showArchive);
itemUnarchive.setVisible(showUnarchive);
}
private void updateTitle(ActionMode mode)
{
mode.setTitle("" + selectedPositions.size());
}
@Override
public boolean onActionItemClicked(final ActionMode mode, MenuItem item)
{
final LinkedList<Habit> selectedHabits = new LinkedList<>();
for (int i : selectedPositions)
selectedHabits.add(loader.habitsList.get(i));
Habit firstHabit = selectedHabits.getFirst();
switch (item.getItemId())
{
case R.id.action_archive_habit:
activity.executeCommand(new ArchiveHabitsCommand(selectedHabits), null);
mode.finish();
return true;
case R.id.action_unarchive_habit:
activity.executeCommand(new UnarchiveHabitsCommand(selectedHabits), null);
mode.finish();
return true;
case R.id.action_edit_habit:
{
EditHabitFragment frag = EditHabitFragment.editSingleHabitFragment(firstHabit.getId());
frag.setOnSavedListener(onSavedListener);
frag.show(activity.getFragmentManager(), "dialog");
return true;
}
case R.id.action_color:
{
ColorPickerDialog picker = ColorPickerDialog.newInstance(R.string.color_picker_default_title,
ColorHelper.palette, firstHabit.color, 4, ColorPickerDialog.SIZE_SMALL);
picker.setOnColorSelectedListener(new ColorPickerSwatch.OnColorSelectedListener()
{
public void onColorSelected(int color)
{
activity.executeCommand(
new ChangeHabitColorCommand(selectedHabits, color), null);
mode.finish();
}
});
picker.show(activity.getFragmentManager(), "picker");
return true;
}
case R.id.action_delete:
{
new AlertDialog.Builder(activity).setTitle(R.string.delete_habits)
.setMessage(R.string.delete_habits_message)
.setPositiveButton(android.R.string.yes,
new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
activity.executeCommand(
new DeleteHabitsCommand(selectedHabits), null);
mode.finish();
}
}).setNegativeButton(android.R.string.no, null)
.show();
return true;
}
case R.id.action_export_csv:
{
onExportHabitsClick(selectedHabits);
return true;
}
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode)
{
if(listener != null) listener.onActionModeDestroyed(mode);
}
private void onExportHabitsClick(final LinkedList<Habit> selectedHabits)
{
new AsyncTask<Void, Void, Void>()
{
String filename;
@Override
protected void onPreExecute()
{
if(progressBar != null)
{
progressBar.setIndeterminate(true);
progressBar.setVisibility(View.VISIBLE);
}
}
@Override
protected void onPostExecute(Void aVoid)
{
if(filename != null)
{
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setType("application/zip");
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(filename)));
activity.startActivity(intent);
}
if(progressBar != null)
progressBar.setVisibility(View.GONE);
}
@Override
protected Void doInBackground(Void... params)
{
CSVExporter exporter = new CSVExporter(activity, selectedHabits);
filename = exporter.writeArchive();
return null;
}
}.execute();
}
}

@ -0,0 +1,78 @@
/* Copyright (C) 2016 Alinson Santos Xavier
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.dialogs;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.view.View;
import android.widget.TextView;
import org.isoron.helpers.DateHelper;
import org.isoron.uhabits.R;
public class HintManager
{
private Context context;
private SharedPreferences prefs;
private View hintView;
public HintManager(Context context, View hintView)
{
this.context = context;
this.hintView = hintView;
prefs = PreferenceManager.getDefaultSharedPreferences(context);
}
public void dismissHint()
{
hintView.animate().alpha(0f).setDuration(500).setListener(new AnimatorListenerAdapter()
{
@Override
public void onAnimationEnd(Animator animation)
{
hintView.setVisibility(View.GONE);
}
});
}
public void showHintIfAppropriate()
{
Integer lastHintNumber = prefs.getInt("last_hint_number", -1);
Long lastHintTimestamp = prefs.getLong("last_hint_timestamp", -1);
if (DateHelper.getStartOfToday() > lastHintTimestamp) showHint(lastHintNumber + 1);
}
private void showHint(int hintNumber)
{
String[] hints = context.getResources().getStringArray(R.array.hints);
if (hintNumber >= hints.length) return;
prefs.edit().putInt("last_hint_number", hintNumber).apply();
prefs.edit().putLong("last_hint_timestamp", DateHelper.getStartOfToday()).apply();
TextView tvContent = (TextView) hintView.findViewById(R.id.hintContent);
tvContent.setText(hints[hintNumber]);
hintView.setAlpha(0.0f);
hintView.setVisibility(View.VISIBLE);
hintView.animate().alpha(1f).setDuration(500);
}
}

@ -57,13 +57,20 @@ public class EditHabitFragment extends DialogFragment
private OnSavedListener onSavedListener;
private Habit originalHabit, modifiedHabit;
private TextView tvName, tvDescription, tvFreqNum, tvFreqDen, tvReminderTime, tvReminderDays;
private Habit originalHabit;
private Habit modifiedHabit;
private TextView tvName;
private TextView tvDescription;
private TextView tvFreqNum;
private TextView tvFreqDen;
private TextView tvReminderTime;
private TextView tvReminderDays;
private SharedPreferences prefs;
private boolean is24HourMode;
static EditHabitFragment editSingleHabitFragment(long id)
public static EditHabitFragment editSingleHabitFragment(long id)
{
EditHabitFragment frag = new EditHabitFragment();
Bundle args = new Bundle();
@ -73,7 +80,7 @@ public class EditHabitFragment extends DialogFragment
return frag;
}
static EditHabitFragment createHabitFragment()
public static EditHabitFragment createHabitFragment()
{
EditHabitFragment frag = new EditHabitFragment();
Bundle args = new Bundle();
@ -96,13 +103,13 @@ public class EditHabitFragment extends DialogFragment
Button buttonSave = (Button) view.findViewById(R.id.buttonSave);
Button buttonDiscard = (Button) view.findViewById(R.id.buttonDiscard);
ImageButton buttonPickColor = (ImageButton) view.findViewById(R.id.buttonPickColor);
buttonSave.setOnClickListener(this);
buttonDiscard.setOnClickListener(this);
tvReminderTime.setOnClickListener(this);
tvReminderDays.setOnClickListener(this);
ImageButton buttonPickColor = (ImageButton) view.findViewById(R.id.buttonPickColor);
buttonPickColor.setOnClickListener(this);
prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
@ -140,8 +147,6 @@ public class EditHabitFragment extends DialogFragment
changeColor(modifiedHabit.color);
updateReminder();
buttonPickColor.setOnClickListener(this);
return view;
}
@ -225,8 +230,6 @@ public class EditHabitFragment extends DialogFragment
private void onSaveButtonClick()
{
Command command = null;
modifiedHabit.name = tvName.getText().toString().trim();
modifiedHabit.description = tvDescription.getText().toString().trim();
modifiedHabit.freqNum = Integer.parseInt(tvFreqNum.getText().toString());
@ -239,6 +242,7 @@ public class EditHabitFragment extends DialogFragment
editor.putInt("pref_default_habit_freq_den", modifiedHabit.freqDen);
editor.apply();
Command command = null;
Habit savedHabit = null;
if (mode == EDIT_MODE)
@ -246,8 +250,10 @@ public class EditHabitFragment extends DialogFragment
command = new EditHabitCommand(originalHabit, modifiedHabit);
savedHabit = originalHabit;
}
if (mode == CREATE_MODE) command = new CreateHabitCommand(modifiedHabit);
else if (mode == CREATE_MODE)
{
command = new CreateHabitCommand(modifiedHabit);
}
if (onSavedListener != null) onSavedListener.onSaved(command, savedHabit);
@ -325,6 +331,7 @@ public class EditHabitFragment extends DialogFragment
int count = 0;
for(int i = 0; i < 7; i++)
if(selectedDays[i]) count++;
if(count == 0) Arrays.fill(selectedDays, true);
modifiedHabit.reminderDays = DateHelper.packWeekdayList(selectedDays);

@ -0,0 +1,97 @@
package org.isoron.uhabits.fragments;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.isoron.helpers.DateHelper;
import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.ListHabitsHelper;
import org.isoron.uhabits.loaders.HabitListLoader;
import org.isoron.uhabits.models.Habit;
import java.util.List;
class HabitListAdapter extends BaseAdapter
{
private LayoutInflater inflater;
private HabitListLoader loader;
private ListHabitsHelper helper;
private List selectedPositions;
private View.OnLongClickListener onCheckmarkLongClickListener;
private View.OnClickListener onCheckmarkClickListener;
public HabitListAdapter(Context context, HabitListLoader loader)
{
this.loader = loader;
inflater = LayoutInflater.from(context);
helper = new ListHabitsHelper(context, loader);
}
@Override
public int getCount()
{
return loader.habits.size();
}
@Override
public Object getItem(int position)
{
return loader.habitsList.get(position);
}
@Override
public long getItemId(int position)
{
return ((Habit) getItem(position)).getId();
}
@Override
public View getView(int position, View view, ViewGroup parent)
{
final Habit habit = loader.habitsList.get(position);
if (view == null || (Long) view.getTag(R.id.timestamp_key) != DateHelper.getStartOfToday())
{
view = inflater.inflate(R.layout.list_habits_item, null);
helper.initializeLabelAndIcon(view);
helper.inflateCheckmarkButtons(view, onCheckmarkLongClickListener,
onCheckmarkClickListener, inflater);
}
TextView tvStar = ((TextView) view.findViewById(R.id.tvStar));
TextView tvName = (TextView) view.findViewById(R.id.label);
LinearLayout llInner = (LinearLayout) view.findViewById(R.id.llInner);
LinearLayout llButtons = (LinearLayout) view.findViewById(R.id.llButtons);
llInner.setTag(R.string.habit_key, habit.getId());
helper.updateNameAndIcon(habit, tvStar, tvName);
helper.updateCheckmarkButtons(habit, llButtons);
boolean selected = selectedPositions.contains(position);
helper.updateHabitBackground(llInner, selected);
return view;
}
public void setSelectedPositions(List selectedPositions)
{
this.selectedPositions = selectedPositions;
}
public void setOnCheckmarkLongClickListener(View.OnLongClickListener listener)
{
this.onCheckmarkLongClickListener = listener;
}
public void setOnCheckmarkClickListener(View.OnClickListener listener)
{
this.onCheckmarkClickListener = listener;
}
}

@ -16,22 +16,11 @@
package org.isoron.uhabits.fragments;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Fragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.DisplayMetrics;
import android.view.ActionMode;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
@ -46,293 +35,100 @@ import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.android.colorpicker.ColorPickerDialog;
import com.android.colorpicker.ColorPickerSwatch;
import com.mobeta.android.dslv.DragSortController;
import com.mobeta.android.dslv.DragSortListView;
import com.mobeta.android.dslv.DragSortListView.DropListener;
import org.isoron.helpers.ColorHelper;
import org.isoron.helpers.Command;
import org.isoron.helpers.DateHelper;
import org.isoron.helpers.DialogHelper;
import org.isoron.helpers.DialogHelper.OnSavedListener;
import org.isoron.helpers.ReplayableActivity;
import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.ArchiveHabitsCommand;
import org.isoron.uhabits.commands.ChangeHabitColorCommand;
import org.isoron.uhabits.commands.DeleteHabitsCommand;
import org.isoron.uhabits.commands.ToggleRepetitionCommand;
import org.isoron.uhabits.commands.UnarchiveHabitsCommand;
import org.isoron.uhabits.dialogs.HabitSelectionCallback;
import org.isoron.uhabits.dialogs.HintManager;
import org.isoron.uhabits.helpers.ListHabitsHelper;
import org.isoron.uhabits.helpers.ReminderHelper;
import org.isoron.uhabits.io.CSVExporter;
import org.isoron.uhabits.loaders.HabitListLoader;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Score;
import java.io.File;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.LinkedList;
import java.util.List;
public class ListHabitsFragment extends Fragment
implements OnSavedListener, OnItemClickListener, OnLongClickListener, DropListener,
OnClickListener, HabitListLoader.Listener, AdapterView.OnItemLongClickListener
OnClickListener, HabitListLoader.Listener, AdapterView.OnItemLongClickListener,
HabitSelectionCallback.Listener
{
private class ListHabitsActionBarCallback implements ActionMode.Callback
{
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu)
{
getActivity().getMenuInflater().inflate(R.menu.list_habits_context, menu);
updateTitle(mode);
updateActions(menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu)
{
updateTitle(mode);
updateActions(menu);
return true;
}
private void updateActions(Menu menu)
{
boolean showEdit = (selectedPositions.size() == 1);
boolean showColor = true;
boolean showArchive = true;
boolean showUnarchive = true;
if(showEdit) showColor = false;
for(int i : selectedPositions)
{
Habit h = loader.habitsList.get(i);
if(h.isArchived())
{
showColor = false;
showArchive = false;
}
else showUnarchive = false;
}
MenuItem itemEdit = menu.findItem(R.id.action_edit_habit);
MenuItem itemColor = menu.findItem(R.id.action_color);
MenuItem itemArchive = menu.findItem(R.id.action_archive_habit);
MenuItem itemUnarchive = menu.findItem(R.id.action_unarchive_habit);
itemEdit.setVisible(showEdit);
itemColor.setVisible(showColor);
itemArchive.setVisible(showArchive);
itemUnarchive.setVisible(showUnarchive);
}
private void updateTitle(ActionMode mode)
{
mode.setTitle("" + selectedPositions.size());
}
@Override
public boolean onActionItemClicked(final ActionMode mode, MenuItem item)
{
final LinkedList<Habit> selectedHabits = new LinkedList<>();
for(int i : selectedPositions)
selectedHabits.add(loader.habitsList.get(i));
Habit firstHabit = selectedHabits.getFirst();
switch(item.getItemId())
{
case R.id.action_archive_habit:
executeCommand(new ArchiveHabitsCommand(selectedHabits), null);
mode.finish();
return true;
case R.id.action_unarchive_habit:
executeCommand(new UnarchiveHabitsCommand(selectedHabits), null);
mode.finish();
return true;
case R.id.action_edit_habit:
{
EditHabitFragment frag = EditHabitFragment.editSingleHabitFragment(firstHabit.getId());
frag.setOnSavedListener(ListHabitsFragment.this);
frag.show(getFragmentManager(), "dialog");
return true;
}
case R.id.action_color:
{
ColorPickerDialog picker = ColorPickerDialog.newInstance(
R.string.color_picker_default_title, ColorHelper.palette,
firstHabit.color, 4, ColorPickerDialog.SIZE_SMALL);
picker.setOnColorSelectedListener(new ColorPickerSwatch.OnColorSelectedListener()
{
public void onColorSelected(int color)
{
executeCommand(new ChangeHabitColorCommand(selectedHabits, color), null);
mode.finish();
}
});
picker.show(getFragmentManager(), "picker");
return true;
}
case R.id.action_delete:
{
new AlertDialog.Builder(activity)
.setTitle(R.string.delete_habits)
.setMessage(R.string.delete_habits_message)
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
executeCommand(new DeleteHabitsCommand(selectedHabits), null);
mode.finish();
}
}).setNegativeButton(android.R.string.no, null)
.show();
return true;
}
case R.id.action_export_csv:
{
onExportHabitsClick(selectedHabits);
return true;
}
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode)
{
actionMode = null;
selectedPositions.clear();
adapter.notifyDataSetChanged();
listView.setDragEnabled(true);
}
}
public static final int INACTIVE_COLOR = Color.rgb(200, 200, 200);
public static final int INACTIVE_CHECKMARK_COLOR = Color.rgb(230, 230, 230);
public static final int HINT_INTERVAL = 5;
public static final int HINT_INTERVAL_OFFSET = 2;
public interface OnHabitClickListener
{
void onHabitClicked(Habit habit);
}
ListHabitsAdapter adapter;
DragSortListView listView;
ReplayableActivity activity;
TextView tvNameHeader;
long lastLongClick = 0;
private int tvNameWidth;
private int buttonCount;
private View llEmpty;
private View llHint;
private OnHabitClickListener habitClickListener;
private boolean isShortToggleEnabled;
private HabitListLoader loader;
private boolean showArchived;
private SharedPreferences prefs;
private ActionMode actionMode;
private HabitListAdapter adapter;
private HabitListLoader loader;
private HintManager hintManager;
private ListHabitsHelper helper;
private List<Integer> selectedPositions;
private DragSortController dragSortController;
private OnHabitClickListener habitClickListener;
private ReplayableActivity activity;
private SharedPreferences prefs;
private DragSortListView listView;
private LinearLayout llButtonsHeader;
private ProgressBar progressBar;
private View llEmpty;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
DisplayMetrics dm = getResources().getDisplayMetrics();
int width = (int) (dm.widthPixels / dm.density);
buttonCount = Math.max(0, (int) ((width - 160) / 42.0));
tvNameWidth = (int) ((width - 30 - buttonCount * 42) * dm.density);
View view = inflater.inflate(R.layout.list_habits_fragment, container, false);
View llHint = view.findViewById(R.id.llHint);
TextView tvStarEmpty = (TextView) view.findViewById(R.id.tvStarEmpty);
listView = (DragSortListView) view.findViewById(R.id.listView);
llButtonsHeader = (LinearLayout) view.findViewById(R.id.llButtonsHeader);
llEmpty = view.findViewById(R.id.llEmpty);
progressBar = (ProgressBar) view.findViewById(R.id.progressBar);
selectedPositions = new LinkedList<>();
loader = new HabitListLoader();
helper = new ListHabitsHelper(activity, loader);
hintManager = new HintManager(activity, llHint);
loader.setListener(this);
loader.setCheckmarkCount(buttonCount);
loader.setCheckmarkCount(helper.getButtonCount());
loader.setProgressBar(progressBar);
View view = inflater.inflate(R.layout.list_habits_fragment, container, false);
tvNameHeader = (TextView) view.findViewById(R.id.tvNameHeader);
llHint.setOnClickListener(this);
tvStarEmpty.setTypeface(helper.getFontawesome());
progressBar = (ProgressBar) view.findViewById(R.id.progressBar);
loader.setProgressBar(progressBar);
adapter = new HabitListAdapter(getActivity(), loader);
adapter.setSelectedPositions(selectedPositions);
adapter.setOnCheckmarkClickListener(this);
adapter.setOnCheckmarkLongClickListener(this);
DragSortListView.DragListener dragListener = new HabitsDragListener();
DragSortController dragSortController = new HabitsDragSortController();
adapter = new ListHabitsAdapter(getActivity());
listView = (DragSortListView) view.findViewById(R.id.listView);
listView.setAdapter(adapter);
listView.setOnItemClickListener(this);
listView.setOnItemLongClickListener(this);
listView.setDropListener(this);
listView.setDragListener(new DragSortListView.DragListener()
{
@Override
public void drag(int from, int to)
{
}
@Override
public void startDrag(int position)
{
selectItem(position);
}
});
dragSortController = new DragSortController(listView) {
@Override
public View onCreateFloatView(int position)
{
return adapter.getView(position, null, null);
}
@Override
public void onDestroyFloatView(View floatView)
{
}
};
dragSortController.setRemoveEnabled(false);
listView.setDragListener(dragListener);
listView.setFloatViewManager(dragSortController);
listView.setDragEnabled(true);
listView.setLongClickable(true);
llHint = view.findViewById(R.id.llHint);
llHint.setOnClickListener(this);
Typeface fontawesome = Typeface.createFromAsset(getActivity().getAssets(),
"fontawesome-webfont.ttf");
((TextView) view.findViewById(R.id.tvStarEmpty)).setTypeface(fontawesome);
llEmpty = view.findViewById(R.id.llEmpty);
loader.updateAllHabits(true);
setHasOptionsMenu(true);
selectedPositions = new LinkedList<>();
return view;
}
@ -356,42 +152,19 @@ public class ListHabitsFragment extends Fragment
if (timestamp != null && timestamp != DateHelper.getStartOfToday())
loader.updateAllHabits(true);
updateEmptyMessage();
updateHeader();
showNextHint();
helper.updateEmptyMessage(llEmpty);
helper.updateHeader(llButtonsHeader);
hintManager.showHintIfAppropriate();
adapter.notifyDataSetChanged();
isShortToggleEnabled = prefs.getBoolean("pref_short_toggle", false);
}
private void updateHeader()
{
LayoutInflater inflater = activity.getLayoutInflater();
View view = getView();
if (view == null) return;
GregorianCalendar day = DateHelper.getStartOfTodayCalendar();
LinearLayout llButtonsHeader = (LinearLayout) view.findViewById(R.id.llButtonsHeader);
llButtonsHeader.removeAllViews();
for (int i = 0; i < buttonCount; i++)
{
View tvDay = inflater.inflate(R.layout.list_habits_header_check, null);
Button btCheck = (Button) tvDay.findViewById(R.id.tvCheck);
btCheck.setText(DateHelper.formatHeaderDate(day));
llButtonsHeader.addView(tvDay);
day.add(GregorianCalendar.DAY_OF_MONTH, -1);
}
}
@Override
public void onLoadFinished()
{
adapter.notifyDataSetChanged();
updateEmptyMessage();
helper.updateEmptyMessage(llEmpty);
}
@Override
@ -485,8 +258,13 @@ public class ListHabitsFragment extends Fragment
if(actionMode == null)
{
actionMode = getActivity().startActionMode(new ListHabitsActionBarCallback());
// listView.setDragEnabled(false);
HabitSelectionCallback callback = new HabitSelectionCallback(activity, loader);
callback.setSelectedPositions(selectedPositions);
callback.setProgressBar(progressBar);
callback.setOnSavedListener(this);
callback.setListener(this);
actionMode = getActivity().startActionMode(callback);
}
if(actionMode != null) actionMode.invalidate();
@ -506,12 +284,6 @@ public class ListHabitsFragment extends Fragment
if(actionMode != null) actionMode.finish();
}
private void updateEmptyMessage()
{
if (loader.getLastLoadTimestamp() == null) llEmpty.setVisibility(View.GONE);
else llEmpty.setVisibility(loader.habits.size() > 0 ? View.GONE : View.VISIBLE);
}
@Override
public boolean onLongClick(View v)
{
@ -538,15 +310,14 @@ public class ListHabitsFragment extends Fragment
private void toggleCheck(View v)
{
Long tag = (Long) v.getTag(R.string.habit_key);
Habit habit = loader.habits.get(tag);
int offset = (Integer) v.getTag(R.string.offset_key);
Integer offset = (Integer) v.getTag(R.string.offset_key);
long timestamp = DateHelper.getStartOfDay(
DateHelper.getLocalTime() - offset * DateHelper.millisecondsInOneDay);
if (v.getTag(R.string.toggle_key).equals(2)) updateCheckmark(habit.color, (TextView) v, 0);
else updateCheckmark(habit.color, (TextView) v, 2);
Habit habit = loader.habits.get(tag);
if(habit == null) return;
helper.toggleCheckmarkView(v, habit);
executeCommand(new ToggleRepetitionCommand(habit, timestamp), habit.getId());
}
@ -555,43 +326,6 @@ public class ListHabitsFragment extends Fragment
activity.executeCommand(c, refreshKey);
}
private void hideHint()
{
llHint.animate().alpha(0f).setDuration(500).setListener(new AnimatorListenerAdapter()
{
@Override
public void onAnimationEnd(Animator animation)
{
llHint.setVisibility(View.GONE);
}
});
}
private void showNextHint()
{
Integer lastHintNumber = prefs.getInt("last_hint_number", -1);
Long lastHintTimestamp = prefs.getLong("last_hint_timestamp", -1);
if(DateHelper.getStartOfToday() > lastHintTimestamp)
showHint(lastHintNumber + 1);
}
private void showHint(int hintNumber)
{
String[] hints = activity.getResources().getStringArray(R.array.hints);
if(hintNumber >= hints.length) return;
prefs.edit().putInt("last_hint_number", hintNumber).apply();
prefs.edit().putLong("last_hint_timestamp", DateHelper.getStartOfToday()).apply();
TextView tvContent = (TextView) llHint.findViewById(R.id.hintContent);
tvContent.setText(hints[hintNumber]);
llHint.setAlpha(0.0f);
llHint.setVisibility(View.VISIBLE);
llHint.animate().alpha(1f).setDuration(500);
}
@Override
public void drop(int from, int to)
{
@ -614,226 +348,61 @@ public class ListHabitsFragment extends Fragment
break;
case R.id.llHint:
hideHint();
hintManager.dismissHint();
break;
}
}
class ListHabitsAdapter extends BaseAdapter
{
private LayoutInflater inflater;
private Typeface fontawesome;
public ListHabitsAdapter(Context context)
{
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
fontawesome = Typeface.createFromAsset(context.getAssets(), "fontawesome-webfont.ttf");
}
@Override
public int getCount()
{
return loader.habits.size();
}
@Override
public Object getItem(int position)
{
return loader.habitsList.get(position);
}
@Override
public long getItemId(int position)
{
return ((Habit) getItem(position)).getId();
}
@Override
public View getView(int position, View view, ViewGroup parent)
{
final Habit habit = loader.habitsList.get(position);
if (view == null ||
(Long) view.getTag(R.id.timestamp_key) != DateHelper.getStartOfToday())
{
view = inflater.inflate(R.layout.list_habits_item, null);
((TextView) view.findViewById(R.id.tvStar)).setTypeface(fontawesome);
LinearLayout.LayoutParams params =
new LinearLayout.LayoutParams(tvNameWidth, LayoutParams.WRAP_CONTENT, 1);
view.findViewById(R.id.label).setLayoutParams(params);
inflateCheckmarkButtons(view);
view.setTag(R.id.timestamp_key, DateHelper.getStartOfToday());
}
TextView tvStar = ((TextView) view.findViewById(R.id.tvStar));
TextView tvName = (TextView) view.findViewById(R.id.label);
LinearLayout llInner = (LinearLayout) view.findViewById(R.id.llInner);
LinearLayout llButtons = (LinearLayout) view.findViewById(R.id.llButtons);
llInner.setTag(R.string.habit_key, habit.getId());
updateNameAndIcon(habit, tvStar, tvName);
updateCheckmarkButtons(habit, llButtons);
boolean selected = selectedPositions.contains(position);
if(selected)
llInner.setBackgroundResource(R.drawable.selected_box);
else
{
if (android.os.Build.VERSION.SDK_INT >= 21)
llInner.setBackgroundResource(R.drawable.ripple_white);
else
llInner.setBackgroundResource(R.drawable.card_background);
}
return view;
}
private void inflateCheckmarkButtons(View view)
{
for (int i = 0; i < buttonCount; i++)
public void onPostExecuteCommand(Long refreshKey)
{
View check = inflater.inflate(R.layout.list_habits_item_check, null);
TextView btCheck = (TextView) check.findViewById(R.id.tvCheck);
btCheck.setTypeface(fontawesome);
btCheck.setOnLongClickListener(ListHabitsFragment.this);
btCheck.setOnClickListener(ListHabitsFragment.this);
((LinearLayout) view.findViewById(R.id.llButtons)).addView(check);
}
}
if (refreshKey == null) loader.updateAllHabits(true);
else loader.updateHabit(refreshKey);
}
private void updateCheckmarkButtons(Habit habit, LinearLayout llButtons)
public void onActionModeDestroyed(ActionMode mode)
{
int activeColor = getActiveColor(habit);
int m = llButtons.getChildCount();
Long habitId = habit.getId();
int isChecked[] = loader.checkmarks.get(habitId);
for (int i = 0; i < m; i++)
{
TextView tvCheck = (TextView) llButtons.getChildAt(i);
tvCheck.setTag(R.string.habit_key, habitId);
tvCheck.setTag(R.string.offset_key, i);
if(isChecked.length > i)
updateCheckmark(activeColor, tvCheck, isChecked[i]);
}
actionMode = null;
selectedPositions.clear();
adapter.notifyDataSetChanged();
listView.setDragEnabled(true);
}
private void updateNameAndIcon(Habit habit, TextView tvStar, TextView tvName)
{
int activeColor = getActiveColor(habit);
tvName.setText(habit.name);
tvName.setTextColor(activeColor);
if (habit.isArchived())
public interface OnHabitClickListener
{
tvStar.setText(getString(R.string.fa_archive));
tvStar.setTextColor(activeColor);
void onHabitClicked(Habit habit);
}
else
{
int score = loader.scores.get(habit.getId());
if (score < Score.HALF_STAR_CUTOFF)
private class HabitsDragSortController extends DragSortController
{
tvStar.setText(getString(R.string.fa_star_o));
tvStar.setTextColor(INACTIVE_COLOR);
}
else if (score < Score.FULL_STAR_CUTOFF)
{
tvStar.setText(getString(R.string.fa_star_half_o));
tvStar.setTextColor(INACTIVE_COLOR);
}
else
public HabitsDragSortController()
{
tvStar.setText(getString(R.string.fa_star));
tvStar.setTextColor(activeColor);
}
}
super(ListHabitsFragment.this.listView);
setRemoveEnabled(false);
}
private int getActiveColor(Habit habit)
@Override
public View onCreateFloatView(int position)
{
int activeColor = habit.color;
if(habit.isArchived()) activeColor = INACTIVE_COLOR;
return activeColor;
return adapter.getView(position, null, null);
}
private void updateCheckmark(int activeColor, TextView tvCheck, int check)
{
switch (check)
@Override
public void onDestroyFloatView(View floatView)
{
case 2:
tvCheck.setText(R.string.fa_check);
tvCheck.setTextColor(activeColor);
tvCheck.setTag(R.string.toggle_key, 2);
break;
case 1:
tvCheck.setText(R.string.fa_check);
tvCheck.setTextColor(INACTIVE_CHECKMARK_COLOR);
tvCheck.setTag(R.string.toggle_key, 1);
break;
case 0:
tvCheck.setText(R.string.fa_times);
tvCheck.setTextColor(INACTIVE_CHECKMARK_COLOR);
tvCheck.setTag(R.string.toggle_key, 0);
break;
}
}
public void onPostExecuteCommand(Long refreshKey)
private class HabitsDragListener implements DragSortListView.DragListener
{
if (refreshKey == null) loader.updateAllHabits(true);
else loader.updateHabit(refreshKey);
}
private void onExportHabitsClick(final LinkedList<Habit> selectedHabits)
{
new AsyncTask<Void, Void, Void>()
{
String filename;
@Override
protected void onPreExecute()
{
progressBar.setIndeterminate(true);
progressBar.setVisibility(View.VISIBLE);
}
@Override
protected void onPostExecute(Void aVoid)
{
if(filename != null)
public void drag(int from, int to)
{
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setType("application/zip");
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(filename)));
startActivity(intent);
}
progressBar.setVisibility(View.GONE);
}
@Override
protected Void doInBackground(Void... params)
public void startDrag(int position)
{
CSVExporter exporter = new CSVExporter(activity, selectedHabits);
filename = exporter.writeArchive();
return null;
selectItem(position);
}
}.execute();
}
}

@ -0,0 +1,211 @@
package org.isoron.uhabits.helpers;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.isoron.helpers.DateHelper;
import org.isoron.uhabits.R;
import org.isoron.uhabits.loaders.HabitListLoader;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Score;
import java.util.GregorianCalendar;
public class ListHabitsHelper
{
public static final int INACTIVE_COLOR = Color.rgb(200, 200, 200);
public static final int INACTIVE_CHECKMARK_COLOR = Color.rgb(230, 230, 230);
private final Context context;
private final HabitListLoader loader;
private Typeface fontawesome;
public ListHabitsHelper(Context context, HabitListLoader loader)
{
this.context = context;
this.loader = loader;
fontawesome = Typeface.createFromAsset(context.getAssets(), "fontawesome-webfont.ttf");
}
public Typeface getFontawesome()
{
return fontawesome;
}
public int getButtonCount()
{
DisplayMetrics dm = context.getResources().getDisplayMetrics();
int width = (int) (dm.widthPixels / dm.density);
return Math.max(0, (int) ((width - 160) / 42.0));
}
public int getHabitNameWidth()
{
DisplayMetrics dm = context.getResources().getDisplayMetrics();
int width = (int) (dm.widthPixels / dm.density);
return (int) ((width - 30 - getButtonCount() * 42) * dm.density);
}
public void updateCheckmarkButtons(Habit habit, LinearLayout llButtons)
{
int activeColor = getActiveColor(habit);
int m = llButtons.getChildCount();
Long habitId = habit.getId();
int isChecked[] = loader.checkmarks.get(habitId);
for (int i = 0; i < m; i++)
{
TextView tvCheck = (TextView) llButtons.getChildAt(i);
tvCheck.setTag(R.string.habit_key, habitId);
tvCheck.setTag(R.string.offset_key, i);
if(isChecked.length > i)
updateCheckmark(activeColor, tvCheck, isChecked[i]);
}
}
public int getActiveColor(Habit habit)
{
int activeColor = habit.color;
if(habit.isArchived()) activeColor = INACTIVE_COLOR;
return activeColor;
}
public void initializeLabelAndIcon(View itemView)
{
TextView tvStar = (TextView) itemView.findViewById(R.id.tvStar);
tvStar.setTypeface(getFontawesome());
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(getHabitNameWidth(),
LinearLayout.LayoutParams.WRAP_CONTENT, 1);
itemView.findViewById(R.id.label).setLayoutParams(params);
}
public void updateNameAndIcon(Habit habit, TextView tvStar, TextView tvName)
{
int activeColor = getActiveColor(habit);
tvName.setText(habit.name);
tvName.setTextColor(activeColor);
if (habit.isArchived())
{
tvStar.setText(context.getString(R.string.fa_archive));
tvStar.setTextColor(activeColor);
}
else
{
int score = loader.scores.get(habit.getId());
if (score < Score.HALF_STAR_CUTOFF)
{
tvStar.setText(context.getString(R.string.fa_star_o));
tvStar.setTextColor(INACTIVE_COLOR);
}
else if (score < Score.FULL_STAR_CUTOFF)
{
tvStar.setText(context.getString(R.string.fa_star_half_o));
tvStar.setTextColor(INACTIVE_COLOR);
}
else
{
tvStar.setText(context.getString(R.string.fa_star));
tvStar.setTextColor(activeColor);
}
}
}
public void updateCheckmark(int activeColor, TextView tvCheck, int check)
{
switch (check)
{
case 2:
tvCheck.setText(R.string.fa_check);
tvCheck.setTextColor(activeColor);
tvCheck.setTag(R.string.toggle_key, 2);
break;
case 1:
tvCheck.setText(R.string.fa_check);
tvCheck.setTextColor(INACTIVE_CHECKMARK_COLOR);
tvCheck.setTag(R.string.toggle_key, 1);
break;
case 0:
tvCheck.setText(R.string.fa_times);
tvCheck.setTextColor(INACTIVE_CHECKMARK_COLOR);
tvCheck.setTag(R.string.toggle_key, 0);
break;
}
}
public void updateHabitBackground(View view, boolean isSelected)
{
if (isSelected)
view.setBackgroundResource(R.drawable.selected_box);
else
{
if (android.os.Build.VERSION.SDK_INT >= 21)
view.setBackgroundResource(R.drawable.ripple_white);
else view.setBackgroundResource(R.drawable.card_background);
}
}
public void inflateCheckmarkButtons(View view, View.OnLongClickListener onLongClickListener,
View.OnClickListener onClickListener, LayoutInflater inflater)
{
for (int i = 0; i < getButtonCount(); i++)
{
View check = inflater.inflate(R.layout.list_habits_item_check, null);
TextView btCheck = (TextView) check.findViewById(R.id.tvCheck);
btCheck.setTypeface(fontawesome);
btCheck.setOnLongClickListener(onLongClickListener);
btCheck.setOnClickListener(onClickListener);
((LinearLayout) view.findViewById(R.id.llButtons)).addView(check);
}
view.setTag(R.id.timestamp_key, DateHelper.getStartOfToday());
}
public void updateHeader(ViewGroup header)
{
LayoutInflater inflater = LayoutInflater.from(context);
GregorianCalendar day = DateHelper.getStartOfTodayCalendar();
header.removeAllViews();
for (int i = 0; i < getButtonCount(); i++)
{
View tvDay = inflater.inflate(R.layout.list_habits_header_check, null);
Button btCheck = (Button) tvDay.findViewById(R.id.tvCheck);
btCheck.setText(DateHelper.formatHeaderDate(day));
header.addView(tvDay);
day.add(GregorianCalendar.DAY_OF_MONTH, -1);
}
}
public void updateEmptyMessage(View view)
{
if (loader.getLastLoadTimestamp() == null) view.setVisibility(View.GONE);
else view.setVisibility(loader.habits.size() > 0 ? View.GONE : View.VISIBLE);
}
public void toggleCheckmarkView(View v, Habit habit)
{
if (v.getTag(R.string.toggle_key).equals(2))
updateCheckmark(habit.color, (TextView) v, 0);
else
updateCheckmark(habit.color, (TextView) v, 2);
}
}

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="color_swatch_large">64dip</dimen>
<dimen name="color_swatch_small">48dip</dimen>
<dimen name="color_swatch_margins_large">8dip</dimen>
<dimen name="color_swatch_margins_small">4dip</dimen>
</resources>

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<?xml version="1.0" encoding="utf-8" ?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<dimen name="color_swatch_large">64dip</dimen>
<dimen name="color_swatch_small">48dip</dimen>
<dimen name="color_swatch_margins_large">8dip</dimen>
<dimen name="color_swatch_margins_small">4dip</dimen>
<item name="circle_radius_multiplier" format="float" type="string" translatable="false">0.82</item>
<item name="circle_radius_multiplier_24HourMode" format="float" type="string" translatable="false">0.85</item>
<item name="selection_radius_multiplier" format="float" type="string" translatable="false">0.16</item>
@ -39,4 +44,22 @@
<dimen name="day_number_size">16sp</dimen>
<dimen name="year_label_height">64dp</dimen>
<dimen name="year_label_text_size">22dp</dimen>
<string name="color_swatch_description" translatable="false">Color <xliff:g id="color_index" example="14">%1$d</xliff:g></string>
<string name="color_swatch_description_selected" translatable="false">Color <xliff:g id="color_index" example="14">%1$d</xliff:g> selected</string>
<!-- Date and time picker -->
<string name="hour_picker_description" translatable="false">Hours circular slider</string>
<string name="minute_picker_description" translatable="false">Minutes circular slider</string>
<string name="day_picker_description" translatable="false">Month grid of days</string>
<string name="year_picker_description" translatable="false">Year list</string>
<string name="select_day" translatable="false">Select month and day</string>
<string name="select_year" translatable="false">Select year</string>
<string name="item_is_selected" translatable="false"><xliff:g id="item" example="2013">%1$s</xliff:g> selected</string>
<string name="deleted_key" translatable="false"><xliff:g id="key" example="4">%1$s</xliff:g> deleted</string>
<string name="time_placeholder" translatable="false">--</string>
<string name="time_separator" translatable="false">:</string>
<string name="radial_numbers_typeface" translatable="false">sans-serif</string>
<string name="sans_serif" translatable="false">sans-serif</string>
<string name="day_of_week_label_typeface" translatable="false">sans-serif</string>
</resources>

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<resources>
<string name="app_name">Loop Habit Tracker</string>
<string name="main_activity_title">Habits</string>
@ -21,28 +21,6 @@
<string name="toast_habit_archived">Habits archived.</string>
<string name="toast_habit_unarchived">Habits unarchived.</string>
<string name="color_swatch_description" translatable="false">Color <xliff:g example="14" id="color_index">%1$d</xliff:g></string>
<string name="color_swatch_description_selected" translatable="false">Color <xliff:g example="14" id="color_index">%1$d</xliff:g> selected</string>
<!-- Date and time picker -->
<string name="done_label">Done</string>
<string name="clear_label">Clear</string>
<string name="hour_picker_description" translatable="false">Hours circular slider</string>
<string name="minute_picker_description" translatable="false">Minutes circular slider</string>
<string name="select_hours">Select hours</string>
<string name="select_minutes">Select minutes</string>
<string name="day_picker_description" translatable="false">Month grid of days</string>
<string name="year_picker_description" translatable="false">Year list</string>
<string name="select_day">Select month and day</string>
<string name="select_year">Select year</string>
<string name="item_is_selected" translatable="false"><xliff:g example="2013" id="item">%1$s</xliff:g> selected</string>
<string name="deleted_key" translatable="false"><xliff:g example="4" id="key">%1$s</xliff:g> deleted</string>
<string name="time_placeholder" translatable="false">--</string>
<string name="time_separator" translatable="false">:</string>
<string name="radial_numbers_typeface" translatable="false">sans-serif</string>
<string name="sans_serif" translatable="false">sans-serif</string>
<string name="day_of_week_label_typeface" translatable="false">sans-serif</string>
<string name="title_activity_show_habit" translatable="false"/>
<string name="overview">Overview</string>
<string name="habit_strength">Habit strength</string>
@ -111,6 +89,11 @@
<string name="select_weekdays">Select days</string>
<string name="export_to_csv">Export data</string>
<string name="done_label">Done</string>
<string name="clear_label">Clear</string>
<string name="select_hours">Select hours</string>
<string name="select_minutes">Select minutes</string>
<string-array name="hints">
<item>@string/hint_drag</item>
<item>@string/hint_landscape</item>

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
Loading…
Cancel
Save