| package com.google.lizlooney.shoppinglist; |
| |
| import android.app.Activity; |
| import android.content.ActivityNotFoundException; |
| import android.content.Intent; |
| import android.graphics.Paint; |
| import android.os.Bundle; |
| import android.os.Environment; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.util.Log; |
| import android.view.Gravity; |
| import android.view.Menu; |
| import android.view.MenuItem; |
| import android.view.MenuItem.OnMenuItemClickListener; |
| import android.view.View; |
| import android.view.View.OnClickListener; |
| import android.view.View.OnLongClickListener; |
| import android.widget.AdapterView; |
| import android.widget.AdapterView.OnItemSelectedListener; |
| import android.widget.ArrayAdapter; |
| import android.widget.Button; |
| import android.widget.CheckBox; |
| import android.widget.CompoundButton; |
| import android.widget.CompoundButton.OnCheckedChangeListener; |
| import android.widget.EditText; |
| import android.widget.LinearLayout; |
| import android.widget.LinearLayout.LayoutParams; |
| import android.widget.Spinner; |
| import android.widget.TextView; |
| import java.io.BufferedReader; |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.FileReader; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Objects; |
| |
| /** |
| * Activity class for the shopping list. |
| * |
| * @author lizlooney@gmail.com (Liz Looney) |
| */ |
| public final class ShoppingList extends Activity { |
| public static final String LOG_TAG = "ShoppingList"; |
| |
| private static final boolean DEBUG = false; |
| |
| private static final int EDIT_ITEM_REQUEST_CODE = 1; |
| |
| private static final String IMPORT_FILE_NAME = "ShoppingList.in"; |
| private static final String EXPORT_FILE_NAME = "ShoppingList.out"; |
| |
| final static String STORE_FILTER_ALL = "<All Stores>"; |
| final static String STORE_FILTER_MISSING = "<Missing Store>"; |
| |
| private Storage storage = new Storage(); |
| /** |
| * All the items, not just the ones currently displayed. |
| */ |
| private final List<Item> allItems = new ArrayList<>(); |
| private final Object allItemsLock = new Object(); |
| /** |
| * All the categories that are used in items. |
| */ |
| private final Categories allCategories = new Categories(); |
| /** |
| * All the aisles that are used in items. |
| */ |
| private final Aisles allAisles = new Aisles(); |
| /** |
| * All the stores that are used in items. |
| */ |
| private final Stores allStores = new Stores(); |
| |
| /** |
| * The items being displayed. |
| */ |
| private final List<Item> displayedItems = new ArrayList<>(); |
| /** |
| * Comparator used to sort the items being displayed. |
| */ |
| private Comparator<Item> comparator; |
| |
| /** |
| * The current display mode. |
| */ |
| private DisplayMode displayMode; |
| /** |
| * The current store filter. |
| */ |
| private String storeFilter; |
| |
| private TextView displayModeTextView; |
| private Spinner storeFilterSpinner; |
| private boolean spinnersDropDownVerticalOffsetDone; |
| private ArrayAdapter<String> storeFilterAdapter; |
| private LinearLayout searchArea; |
| private EditText searchBox; |
| private TextView secondHeading; |
| private LinearLayout itemsContainer; |
| |
| private Item itemBeingEdited; |
| |
| /** Called when the activity is first created. */ |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| setContentView(R.layout.shopping_list); |
| |
| storage.init(this); |
| |
| displayModeTextView = findViewById(R.id.displayMode); |
| displayModeTextView.setOnClickListener(new OnClickListener() { |
| @Override |
| public void onClick(View view) { |
| toggleDisplayMode(); |
| } |
| }); |
| storeFilterSpinner = findViewById(R.id.storeFilter); |
| storeFilterSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { |
| @Override |
| public void onItemSelected(AdapterView<?> adapterView, View view, int pos, long id) { |
| String selectedStoreFilter = (String) adapterView.getItemAtPosition(pos); |
| setStoreFilter(selectedStoreFilter); |
| } |
| |
| @Override |
| public void onNothingSelected(AdapterView<?> parent) {} |
| }); |
| |
| searchArea = findViewById(R.id.searchArea); |
| searchBox = findViewById(R.id.searchBox); |
| searchBox.addTextChangedListener(new TextChangeAdapter() { |
| @Override |
| public void textChanged(String s) { |
| updateDisplay(); |
| } |
| }); |
| Button searchClearButton = findViewById(R.id.searchClear); |
| searchClearButton.setOnClickListener(new OnClickListener() { |
| @Override |
| public void onClick(View view) { |
| searchBox.setText(""); |
| } |
| }); |
| Button searchHideButton = findViewById(R.id.searchHide); |
| searchHideButton.setOnClickListener(new OnClickListener() { |
| @Override |
| public void onClick(View view) { |
| stopSearch(); |
| } |
| }); |
| |
| TextView firstHeading = findViewById(R.id.firstHeading); |
| firstHeading.setPaintFlags(firstHeading.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); |
| secondHeading = findViewById(R.id.secondHeading); |
| secondHeading.setPaintFlags(secondHeading.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); |
| |
| itemsContainer = findViewById(R.id.items); |
| |
| loadFromStorage(); |
| |
| storeFilterAdapter = new ArrayAdapter<>(this, R.layout.spinner_item_large_right); |
| storeFilterAdapter.setDropDownViewResource(R.layout.spinner_dropdown_item_large_right); |
| storeFilterSpinner.setAdapter(storeFilterAdapter); |
| |
| updateComparator(); |
| updateDisplay(); |
| } |
| |
| private void loadFromStorage() { |
| synchronized (allItemsLock) { |
| storage.loadItems(allItems); |
| if (DEBUG) { |
| Log.d(LOG_TAG, "Loaded " + allItems.size() + " items from storage"); |
| } |
| allStores.loadStores(allItems); |
| allAisles.loadAisles(allItems); |
| allCategories.loadCategories(allItems); |
| } |
| displayMode = storage.loadDisplayMode(); |
| storeFilter = storage.loadStoreFilter(); |
| if (storeFilter == null || storeFilter.isEmpty()) { |
| storeFilter = STORE_FILTER_ALL; |
| } |
| } |
| |
| private void setSpinnersDropDownVerticalOffset() { |
| if (spinnersDropDownVerticalOffsetDone) { |
| return; |
| } |
| if (storeFilterSpinner.getHeight() == 0) { |
| new Handler() |
| .post( |
| new Runnable() { |
| @Override |
| public void run() { |
| setSpinnersDropDownVerticalOffset(); |
| } |
| }); |
| return; |
| } |
| |
| storeFilterSpinner.setDropDownVerticalOffset(storeFilterSpinner.getHeight()); |
| spinnersDropDownVerticalOffsetDone = true; |
| } |
| |
| @Override |
| public boolean onSearchRequested() { |
| if (searchArea.getVisibility() != View.VISIBLE) { |
| startSearch(); |
| } else { |
| stopSearch(); |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean onPrepareOptionsMenu(Menu menu) { |
| super.onPrepareOptionsMenu(menu); |
| |
| menu.clear(); |
| |
| if (displayMode == DisplayMode.PLANNING) { |
| // Search |
| if (searchArea.getVisibility() != View.VISIBLE) { |
| menu.add(getString(R.string.ShowSearch)) |
| .setOnMenuItemClickListener( |
| new OnMenuItemClickListener() { |
| @Override |
| public boolean onMenuItemClick(MenuItem menuItem) { |
| startSearch(); |
| return true; |
| } |
| }); |
| } |
| } |
| |
| // Refresh |
| menu.add(getString(R.string.Refresh)) |
| .setOnMenuItemClickListener( |
| new OnMenuItemClickListener() { |
| @Override |
| public boolean onMenuItemClick(MenuItem menuItem) { |
| updateDisplay(); |
| return true; |
| } |
| }); |
| |
| // Add Item... |
| menu.add(getString(R.string.AddItem)) |
| .setOnMenuItemClickListener(new OnMenuItemClickListener() { |
| @Override |
| public boolean onMenuItemClick(MenuItem menuItem) { |
| editItem(null); |
| return true; |
| } |
| }); |
| |
| if (displayMode == DisplayMode.SHOPPING) { |
| // Clear Checked Items |
| menu.add(getString(R.string.ClearCheckedItems)) |
| .setOnMenuItemClickListener( |
| new OnMenuItemClickListener() { |
| @Override |
| public boolean onMenuItemClick(MenuItem menuItem) { |
| for (final Item item : displayedItems) { |
| if (item.getState() == ItemState.IN_SHOPPING_CART) { |
| if (item.getAutoDelete()) { |
| synchronized (allItemsLock) { |
| allItems.remove(item); |
| } |
| storage.deleteItem(item); |
| } else { |
| item.setState(ItemState.DONT_NEED); |
| storage.saveItem(item); |
| } |
| } |
| } |
| updateDisplay(); |
| return true; |
| } |
| }); |
| } |
| |
| if (displayMode == DisplayMode.PLANNING) { |
| // Import Items |
| File importFile = new File(Environment.getExternalStorageDirectory().getPath(), IMPORT_FILE_NAME); |
| if (importFile.exists() && importFile.isFile()) { |
| menu.add(getString(R.string.ImportItems)) |
| .setOnMenuItemClickListener( |
| new OnMenuItemClickListener() { |
| @Override |
| public boolean onMenuItemClick(MenuItem menuItem) { |
| importFromFile(); |
| return true; |
| } |
| }); |
| } |
| |
| // Export Items |
| menu.add(getString(R.string.ExportItems)) |
| .setOnMenuItemClickListener( |
| new OnMenuItemClickListener() { |
| @Override |
| public boolean onMenuItemClick(MenuItem menuItem) { |
| exportToFile(); |
| return true; |
| } |
| }); |
| } |
| |
| return true; |
| } |
| |
| private void toggleDisplayMode() { |
| switch (displayMode) { |
| default: |
| case PLANNING: |
| displayMode = DisplayMode.SHOPPING; |
| break; |
| case SHOPPING: |
| displayMode = DisplayMode.PLANNING; |
| break; |
| } |
| storage.saveDisplayMode(displayMode); |
| |
| updateComparator(); |
| updateDisplay(); |
| } |
| |
| private void setStoreFilter(String newStoreFilter) { |
| if (!Objects.equals(storeFilter, newStoreFilter)) { |
| storeFilter = newStoreFilter; |
| storage.saveStoreFilter(storeFilter); |
| |
| updateComparator(); |
| updateDisplay(); |
| } |
| } |
| |
| private void updateComparator() { |
| switch (displayMode) { |
| default: |
| case PLANNING: |
| comparator = ItemComparators.SORT_FOR_PLANNING; |
| break; |
| case SHOPPING: |
| comparator = ItemComparators.sortForShopping(storeFilter); |
| break; |
| } |
| } |
| |
| private void updateDisplay() { |
| synchronized (allItemsLock) { |
| if (itemsContainer.getWidth() == 0) { |
| new Handler().post(new Runnable() { |
| @Override |
| public void run() { |
| updateDisplay(); |
| } |
| }); |
| return; |
| } |
| long startTimeDebug = System.currentTimeMillis(); |
| |
| itemsContainer.removeAllViews(); |
| displayedItems.clear(); |
| Stores neededStores = new Stores(); |
| |
| String searchText; |
| if (searchArea.getVisibility() == View.VISIBLE) { |
| searchText = searchBox.getText().toString().trim().toLowerCase(Locale.getDefault()); |
| if (searchText.length() == 0) { |
| searchText = null; |
| } |
| } else { |
| searchText = null; |
| } |
| |
| displayModeTextView.setText(DisplayMode.toString(this, displayMode)); |
| |
| switch (displayMode) { |
| default: |
| case PLANNING: |
| secondHeading.setText(R.string.CategoryHeading); |
| for (Item item : allItems) { |
| if (itemShouldBeShown(item, searchText)) { |
| displayedItems.add(item); |
| } |
| } |
| break; |
| |
| case SHOPPING: |
| secondHeading.setText(R.string.AisleHeading); |
| for (Item item : allItems) { |
| if (item.getState() == ItemState.DONT_NEED) { |
| continue; |
| } |
| neededStores.add(item); |
| if (itemShouldBeShown(item, searchText)) { |
| displayedItems.add(item); |
| } |
| } |
| break; |
| } |
| |
| Collections.sort(displayedItems, comparator); |
| |
| for (final Item item : displayedItems) { |
| LinearLayout row = new LinearLayout(this); |
| Utils.setColors(row); |
| |
| // Create a checkbox for the item state. |
| final CheckBox stateCheckBox = new CheckBox(this); |
| Utils.setColors(stateCheckBox); |
| setStateCheckBox(stateCheckBox, item); |
| stateCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() { |
| @Override |
| public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { |
| if (stateCheckBox.isEnabled()) { |
| stateCheckBoxChanged(stateCheckBox, isChecked, item); |
| } |
| } |
| }); |
| row.addView(stateCheckBox, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0f)); |
| |
| // Create a TextView for the item description. |
| TextView description = new TextView(this); |
| Utils.setColors(description); |
| description.setText(item.getDescription()); |
| description.setOnLongClickListener(new OnLongClickListener() { |
| @Override |
| public boolean onLongClick(View view) { |
| editItem(item); |
| return true; |
| } |
| }); |
| row.addView(description, new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)); |
| |
| // Create another TextView for the category or aisle, depending on the display mode. |
| TextView second = new TextView(this); |
| Utils.setColors(second); |
| String text; |
| switch (displayMode) { |
| default: |
| case PLANNING: |
| text = item.getCategory(); |
| break; |
| case SHOPPING: |
| if (storeFilter.equals(STORE_FILTER_ALL) || |
| storeFilter.equals(STORE_FILTER_MISSING)) { |
| text = ""; |
| } else { |
| text = item.getAisle(storeFilter); |
| } |
| break; |
| } |
| second.setText(text); |
| second.setGravity(Gravity.RIGHT); |
| second.setPadding(0, 0, 10, 0); |
| row.addView( |
| second, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0f)); |
| |
| // Add the row to the itemsContainer. |
| itemsContainer.addView(row); |
| } |
| |
| if (displayedItems.size() == 0) { |
| TextView message = new TextView(this); |
| Utils.setColors(message); |
| message.setSingleLine(false); |
| message.setTextSize(30); |
| message.setGravity(Gravity.CENTER); |
| String text; |
| switch (displayMode) { |
| default: |
| case PLANNING: |
| text = getString(R.string.NoItems); |
| break; |
| case SHOPPING: |
| if (storeFilter.equals(STORE_FILTER_ALL)) { |
| text = getString(R.string.NoItemsNeeded); |
| } else if (storeFilter.equals(STORE_FILTER_MISSING)) { |
| text = getString(R.string.NoItemsNeeded); |
| } else { |
| text = getString(R.string.NoItemsNeededAtStore, storeFilter); |
| } |
| break; |
| } |
| message.setText(text); |
| itemsContainer.addView(message, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT, 0f)); |
| } |
| |
| itemsContainer.requestLayout(); |
| |
| Utils.updateSpinner( |
| storeFilterSpinner, |
| storeFilterAdapter, |
| (displayMode == DisplayMode.PLANNING) |
| ? allStores.getStoresForStoreFilter() |
| : neededStores.getStoresForStoreFilter(), |
| storeFilter); |
| setSpinnersDropDownVerticalOffset(); |
| |
| if (DEBUG) { |
| long elapsedTimeDebug = System.currentTimeMillis() - startTimeDebug; |
| Log.d(LOG_TAG, "storeFilter is \"" + storeFilter + "\""); |
| Log.d( |
| LOG_TAG, |
| "displaying " |
| + displayedItems.size() |
| + " (of " |
| + allItems.size() |
| + ") took " |
| + elapsedTimeDebug |
| + " ms"); |
| } |
| } |
| } |
| |
| private boolean itemShouldBeShown(Item item, String searchText) { |
| if (storeFilter.equals(STORE_FILTER_MISSING)) { |
| // Only show items that are missing a store. |
| if (!item.isMissingStore()) { |
| return false; |
| } |
| } |
| |
| // Eliminate items because of search text. |
| if (searchText != null && !item.getDescription().toLowerCase(Locale.getDefault()).contains(searchText)) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| private void startSearch() { |
| if (searchArea.getVisibility() != View.VISIBLE) { |
| searchArea.setVisibility(View.VISIBLE); |
| Utils.showSoftKeyboard(searchBox); |
| } |
| } |
| |
| private void stopSearch() { |
| searchBox.setText(""); |
| searchArea.setVisibility(View.GONE); |
| Utils.hideSoftKeyboard(searchBox); |
| } |
| |
| private void setStateCheckBox(CheckBox stateCheckBox, Item item) { |
| switch (displayMode) { |
| case PLANNING: |
| switch (item.getState()) { |
| case DONT_NEED: |
| stateCheckBox.setEnabled(true); |
| stateCheckBox.setChecked(false); |
| break; |
| case NEED: |
| stateCheckBox.setEnabled(true); |
| stateCheckBox.setChecked(true); |
| break; |
| case IN_SHOPPING_CART: |
| // If the item is already in the shopping cart, disable the checkbox. |
| stateCheckBox.setEnabled(false); |
| stateCheckBox.setChecked(true); |
| break; |
| } |
| break; |
| case SHOPPING: |
| switch (item.getState()) { |
| case DONT_NEED: |
| // Item should not even be displayed in SHOPPING mode. |
| stateCheckBox.setEnabled(false); |
| stateCheckBox.setChecked(false); |
| break; |
| case NEED: |
| stateCheckBox.setEnabled(true); |
| stateCheckBox.setChecked(false); |
| break; |
| case IN_SHOPPING_CART: |
| stateCheckBox.setEnabled(true); |
| stateCheckBox.setChecked(true); |
| break; |
| } |
| break; |
| } |
| } |
| |
| private void stateCheckBoxChanged(CheckBox stateCheckBox, boolean isChecked, Item item) { |
| switch (displayMode) { |
| case PLANNING: |
| item.setState(isChecked ? ItemState.NEED : ItemState.DONT_NEED); |
| break; |
| case SHOPPING: |
| item.setState(isChecked ? ItemState.IN_SHOPPING_CART : ItemState.NEED); |
| break; |
| } |
| storage.saveItem(item); |
| } |
| |
| private void editItem(Item item) { |
| Intent activityIntent = new Intent(); |
| activityIntent.setClass(this, EditItem.class); |
| activityIntent.putExtra(EditItem.ALL_CATEGORIES, allCategories.getCategoriesArray()); |
| activityIntent.putExtra(EditItem.ALL_STORES, allStores.getStoresArray()); |
| activityIntent.putExtra(EditItem.ALL_AISLES, allAisles.getAislesArray()); |
| |
| if (item != null) { |
| item.exportToIntent(activityIntent); |
| } else { |
| if (searchArea.getVisibility() == View.VISIBLE) { |
| String descriptionString = searchBox.getText().toString().trim(); |
| activityIntent.putExtra(EditItem.ITEM_DESCRIPTION, descriptionString); |
| } |
| } |
| |
| try { |
| itemBeingEdited = item; |
| startActivityForResult(activityIntent, EDIT_ITEM_REQUEST_CODE); |
| } catch (ActivityNotFoundException e) { |
| Log.e(LOG_TAG, "startActivityForResult threw ActivityNotFoundException", e); |
| itemBeingEdited = null; |
| } |
| } |
| |
| @Override |
| protected void onActivityResult(int requestCode, int resultCode, Intent data) { |
| if (requestCode == EDIT_ITEM_REQUEST_CODE) { |
| if (resultCode == Activity.RESULT_OK) { |
| if (data != null) { |
| Item item; |
| if (itemBeingEdited != null) { |
| // Edit an existing item. |
| item = itemBeingEdited; |
| itemBeingEdited = null; |
| } else { |
| // Add an item. |
| item = new Item(storage.getUnusedItemId()); |
| synchronized (allItemsLock) { |
| allItems.add(item); |
| } |
| } |
| item.importFromIntent(data); |
| allCategories.add(item); |
| allStores.add(item); |
| allAisles.add(item); |
| storage.saveItem(item); |
| } else { |
| // Delete the item. |
| if (itemBeingEdited != null) { |
| synchronized (allItemsLock) { |
| allItems.remove(itemBeingEdited); |
| } |
| storage.deleteItem(itemBeingEdited); |
| itemBeingEdited = null; |
| } |
| } |
| updateDisplay(); |
| } |
| } |
| } |
| |
| private void importFromFile() { |
| synchronized (allItemsLock) { |
| allItems.clear(); |
| } |
| allCategories.clear(); |
| allAisles.clear(); |
| allStores.clear(); |
| displayMode = DisplayMode.PLANNING; |
| storeFilter = STORE_FILTER_ALL; |
| storage.clear(); |
| |
| new Thread(new Runnable() { |
| @Override |
| public void run() { |
| long startTimeDebug = System.currentTimeMillis(); |
| int count = 0; |
| try { |
| BufferedReader reader = new BufferedReader(new FileReader(new File(Environment.getExternalStorageDirectory().getPath(), IMPORT_FILE_NAME))); |
| try { |
| while (true) { |
| String line = reader.readLine(); |
| if (line == null) { |
| break; |
| } |
| line = line.trim(); |
| if (line.length() > 0) { |
| Item item = new Item(storage.getUnusedItemId()); |
| item.setState(ItemState.DONT_NEED); |
| if (item.importFromString(line)) { |
| synchronized (allItemsLock) { |
| allItems.add(item); |
| } |
| allCategories.add(item); |
| allAisles.add(item); |
| allStores.add(item); |
| storage.saveItem(item); |
| count++; |
| } else { |
| Log.w(LOG_TAG, "Unable to import item from \"" + line + "\""); |
| storage.deleteItem(item); |
| } |
| } |
| } |
| } finally { |
| reader.close(); |
| } |
| } catch (IOException e) { |
| Log.e(LOG_TAG, "importFromFile caught", e); |
| } |
| if (DEBUG) { |
| long elapsedTimeDebug = System.currentTimeMillis() - startTimeDebug; |
| Log.d(LOG_TAG, "Importing " + count + " items took " + elapsedTimeDebug + " ms"); |
| Log.d(LOG_TAG, "Categories:"); |
| for (String category : allCategories.getCategoriesArray()) { |
| Log.d(LOG_TAG, " " + category); |
| } |
| Log.d(LOG_TAG, "Aisles:"); |
| for (String aisle : allAisles.getAislesArray()) { |
| Log.d(LOG_TAG, " " + aisle); |
| } |
| Log.d(LOG_TAG, "Stores:"); |
| for (String store : allStores.getStoresArray()) { |
| Log.d(LOG_TAG, " " + store); |
| } |
| } |
| new Handler(Looper.getMainLooper()).post(new Runnable() { |
| @Override |
| public void run() { |
| updateDisplay(); |
| } |
| }); |
| } |
| }).start(); |
| } |
| |
| private void exportToFile() { |
| synchronized (allItemsLock) { |
| try { |
| int count = 0; |
| BufferedWriter writer = new BufferedWriter(new FileWriter(new File(Environment.getExternalStorageDirectory().getPath(), EXPORT_FILE_NAME))); |
| try { |
| for (Item item : allItems) { |
| writer.write(item.exportToString()); |
| writer.write("\n"); |
| count++; |
| } |
| } finally { |
| writer.close(); |
| if (DEBUG) { |
| Log.d(LOG_TAG, "Exported " + count + " items"); |
| } |
| } |
| } catch (IOException e) { |
| Log.e(LOG_TAG, "exportToFile caught", e); |
| } |
| } |
| } |
| } |