├── .gitignore ├── COPYING ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── woefe │ │ └── shoppinglist │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ └── com │ │ └── woefe │ │ └── shoppinglist │ │ ├── activity │ │ ├── AboutActivity.java │ │ ├── BinderActivity.java │ │ ├── EditBar.java │ │ ├── InvalidFragment.java │ │ ├── MainActivity.java │ │ ├── RecyclerListAdapter.java │ │ ├── SettingsActivity.java │ │ ├── SettingsFragment.java │ │ └── ShoppingListFragment.java │ │ ├── dialog │ │ ├── ConfirmationDialog.java │ │ ├── DirectoryChooser.java │ │ └── TextInputDialog.java │ │ └── shoppinglist │ │ ├── DirectoryStatus.java │ │ ├── ListItem.java │ │ ├── ListsChangeListener.java │ │ ├── ShoppingList.java │ │ ├── ShoppingListException.java │ │ ├── ShoppingListMarshaller.java │ │ ├── ShoppingListService.java │ │ ├── ShoppingListUnmarshaller.java │ │ ├── ShoppingListsManager.java │ │ └── UnmarshallException.java │ └── res │ ├── drawable-anydpi │ └── ic_done_white_24dp.xml │ ├── drawable-hdpi │ ├── ic_add_black_24dp.png │ ├── ic_delete_forever_white_24.png │ ├── ic_delete_sweep_white_24dp.png │ ├── ic_done_white_24dp.png │ ├── ic_menu_white_24dp.png │ ├── ic_playlist_add_white_24dp.png │ ├── ic_sd_storage_black_24dp.png │ ├── ic_sort_white_24dp.png │ └── ic_swap_vert_black_24dp.png │ ├── drawable-mdpi │ ├── ic_add_black_24dp.png │ ├── ic_delete_forever_white_24.png │ ├── ic_delete_sweep_white_24dp.png │ ├── ic_done_white_24dp.png │ ├── ic_menu_white_24dp.png │ ├── ic_playlist_add_white_24dp.png │ ├── ic_sd_storage_black_24dp.png │ ├── ic_sort_white_24dp.png │ └── ic_swap_vert_black_24dp.png │ ├── drawable-xhdpi │ ├── ic_add_black_24dp.png │ ├── ic_delete_forever_white_24.png │ ├── ic_delete_sweep_white_24dp.png │ ├── ic_done_white_24dp.png │ ├── ic_menu_white_24dp.png │ ├── ic_playlist_add_white_24dp.png │ ├── ic_sd_storage_black_24dp.png │ ├── ic_sort_white_24dp.png │ └── ic_swap_vert_black_24dp.png │ ├── drawable-xxhdpi │ ├── ic_add_black_24dp.png │ ├── ic_delete_forever_white_24.png │ ├── ic_delete_sweep_white_24dp.png │ ├── ic_done_white_24dp.png │ ├── ic_menu_white_24dp.png │ ├── ic_playlist_add_white_24dp.png │ ├── ic_sd_storage_black_24dp.png │ ├── ic_sort_white_24dp.png │ └── ic_swap_vert_black_24dp.png │ ├── drawable-xxxhdpi │ ├── ic_add_black_24dp.png │ ├── ic_delete_forever_white_24.png │ ├── ic_delete_sweep_white_24dp.png │ ├── ic_done_black_36dp.png │ ├── ic_menu_white_24dp.png │ ├── ic_playlist_add_white_24dp.png │ ├── ic_sd_storage_black_24dp.png │ ├── ic_sort_white_24dp.png │ └── ic_swap_vert_black_24dp.png │ ├── drawable │ ├── button_add_selector.xml │ ├── ic_launcher_foreground.xml │ └── list_activated_background.xml │ ├── layout │ ├── activity_about.xml │ ├── activity_main.xml │ ├── dialog_directory_chooser.xml │ ├── dialog_text_input.xml │ ├── drawer_list_item.xml │ ├── fragment_invalid.xml │ ├── fragment_shoppinglist.xml │ ├── list_item.xml │ └── toolbar_main.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values-de │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-it │ └── strings.xml │ ├── values-nl │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values-v21 │ └── styles.xml │ ├── values-w820dp │ └── dimens.xml │ ├── values │ ├── colors.xml │ ├── dimens.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── preferences.xml ├── build.gradle ├── fastlane └── metadata │ └── android │ ├── de-DE │ ├── full_description.txt │ └── short_description.txt │ └── en-US │ ├── full_description.txt │ ├── images │ └── phoneScreenshots │ │ ├── 00_lists.png │ │ └── 01_list_content.png │ └── short_description.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Mobile Tools for Java (J2ME) 2 | .mtj.tmp/ 3 | 4 | # Package Files # 5 | *.jar 6 | *.war 7 | *.ear 8 | 9 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 10 | hs_err_pid* 11 | 12 | ## Android 13 | 14 | # Built application files 15 | *.apk 16 | *.ap_ 17 | 18 | # Files for the Dalvik VM 19 | *.dex 20 | 21 | # Java class files 22 | *.class 23 | 24 | # Generated files 25 | bin/ 26 | gen/ 27 | 28 | # Gradle files 29 | .gradle/ 30 | build/ 31 | */build/ 32 | 33 | # Local configuration file (sdk path, etc) 34 | local.properties 35 | 36 | # Proguard folder generated by Eclipse 37 | proguard/ 38 | 39 | # Log Files 40 | *.log 41 | 42 | # Android Studio Navigation editor temp files 43 | .navigation/ 44 | 45 | # Android Studio captures folder 46 | captures/ 47 | 48 | ## Android Studio 49 | *.iml 50 | 51 | # Directory-based project format: 52 | .idea/ 53 | 54 | # File-based project format: 55 | *.ipr 56 | *.iws 57 | 58 | # Plugin-specific files: 59 | 60 | # IntelliJ 61 | /out/ 62 | 63 | # mpeltonen/sbt-idea plugin 64 | .idea_modules/ 65 | 66 | # JIRA plugin 67 | atlassian-ide-plugin.xml 68 | 69 | # Crashlytics plugin (for Android Studio and IntelliJ) 70 | com_crashlytics_export_strings.xml 71 | crashlytics.properties 72 | crashlytics-build.properties 73 | fabric.properties 74 | 75 | # Ignore Gradle GUI config 76 | gradle-app.setting 77 | 78 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 79 | !gradle-wrapper.jar 80 | 81 | # Cache of project 82 | .gradletasknamecache 83 | 84 | ## C Build Files 85 | **/obj/local/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |


ShoppingList

2 | A simple shopping list for Android 3 | 4 |

5 | 6 | 7 |

8 | 9 | 10 | Get it on F-Droid 11 | 12 | 13 | ## ShoppingList text file 14 | ShoppingList saves your shopping lists as a plain text files. 15 | The syntax of a ShoppingList file is quite simple and easy to read and edit. 16 | 17 | ### Syntax 18 | * The very first line of the file is the name of the list in square brackets 19 | * Empty lines or lines with only white spaces are ignored 20 | * Every item of the list corresponds to a single line in the file 21 | * Checked items start with `//` 22 | * Specifying the amount of an item is optional 23 | * The amount of an item and its name are separated by the #-Sign 24 | 25 | ### Example 26 | ``` 27 | [ ShoppingList ] 28 | 29 | Milk 30 | Bananas # 31 | Juice #2 Liters 32 | // Eggs #12 33 | ``` 34 | 35 | ## Synchronization 36 | To synchronize the lists select a folder in the ShoppingList settings which gets synchronized by whichever sync-software you use. 37 | The ShoppingList app will automatically detect lists (and changes to the lists) that show up in the folder. 38 | Note that unfortunately most synchronisation solutions on Android cannot continuously watch and synchronize the folder. 39 | 40 | ### Nextcloud Example 41 | 1. Create a `shoppinglists` folder in your Nextcloud 42 | 2. Sync this folder to your Nextcloud files app. 43 | 3. Go to ShoppingList settings and find this folder on your Android file system (e.g. `/storage/emulated/0/Android/media/com.nextcloud.client/nextcloud/user@cloud.kom/shoppinglists`) 44 | 4. If you want to sync the lists, open the Nextcloud files app and synchronize the shopping list folder 45 | 46 | Better Integration with Nextcloud is [planned](https://github.com/woefe/ShoppingList/issues/17). 47 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | apply plugin: 'com.android.application' 21 | 22 | android { 23 | compileSdkVersion 28 24 | buildToolsVersion '28.0.3' 25 | 26 | defaultConfig { 27 | applicationId "com.woefe.shoppinglist" 28 | minSdkVersion 19 29 | targetSdkVersion 28 30 | versionCode 12 31 | versionName "0.11.0" 32 | } 33 | buildTypes { 34 | release { 35 | minifyEnabled false 36 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 37 | } 38 | } 39 | } 40 | 41 | dependencies { 42 | implementation fileTree(dir: 'libs', include: ['*.jar']) 43 | implementation 'com.android.support:appcompat-v7:28.0.0' 44 | implementation 'com.android.support:design:28.0.0' 45 | implementation 'com.android.support:preference-v7:28.0.0' 46 | implementation 'com.android.support:recyclerview-v7:28.0.0' 47 | } 48 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/popeye/Android/Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/woefe/shoppinglist/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist; 21 | 22 | import android.app.Application; 23 | import android.test.ApplicationTestCase; 24 | 25 | /** 26 | * Testing Fundamentals 27 | */ 28 | public class ApplicationTest extends ApplicationTestCase { 29 | public ApplicationTest() { 30 | super(Application.class); 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/activity/AboutActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.activity; 21 | 22 | import android.os.Bundle; 23 | import android.support.annotation.Nullable; 24 | import android.support.v7.app.AppCompatActivity; 25 | import android.text.Html; 26 | import android.text.method.LinkMovementMethod; 27 | import android.widget.TextView; 28 | 29 | import com.woefe.shoppinglist.BuildConfig; 30 | import com.woefe.shoppinglist.R; 31 | 32 | /** 33 | * @author Wolfgang Popp 34 | */ 35 | 36 | public class AboutActivity extends AppCompatActivity { 37 | 38 | @Override 39 | protected void onCreate(@Nullable Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | setContentView(R.layout.activity_about); 42 | TextView textView = findViewById(R.id.about_text); 43 | textView.setMovementMethod(LinkMovementMethod.getInstance()); 44 | textView.setText(Html.fromHtml(getString(R.string.about_title))); 45 | textView.append(Html.fromHtml(getString(R.string.about_version, BuildConfig.VERSION_NAME))); 46 | textView.append("\n"); 47 | textView.append("\n"); 48 | textView.append("\n"); 49 | textView.append(Html.fromHtml(getString(R.string.about_github))); 50 | textView.append("\n"); 51 | textView.append("\n"); 52 | textView.append("\n"); 53 | textView.append(Html.fromHtml(getString(R.string.about_license))); 54 | textView.append("\n"); 55 | textView.append("\n"); 56 | textView.append("\n"); 57 | textView.append(Html.fromHtml(getString(R.string.about_author))); 58 | textView.append("\n"); 59 | textView.append("\n"); 60 | textView.append("\n"); 61 | textView.append(Html.fromHtml(getString(R.string.about_contributors))); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/activity/BinderActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.activity; 21 | 22 | import android.content.ComponentName; 23 | import android.content.Context; 24 | import android.content.Intent; 25 | import android.content.ServiceConnection; 26 | import android.os.IBinder; 27 | import android.support.v7.app.AppCompatActivity; 28 | 29 | import com.woefe.shoppinglist.shoppinglist.ShoppingListService; 30 | 31 | /** 32 | * @author Wolfgang Popp 33 | */ 34 | public abstract class BinderActivity extends AppCompatActivity { 35 | 36 | private final ShoppingListServiceConnection serviceConnection = new ShoppingListServiceConnection(); 37 | private ShoppingListService.ShoppingListBinder binder = null; 38 | 39 | @Override 40 | protected void onStart() { 41 | super.onStart(); 42 | Intent intent = new Intent(this, ShoppingListService.class); 43 | bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); 44 | } 45 | 46 | protected void onStop() { 47 | unbindService(serviceConnection); 48 | super.onStop(); 49 | } 50 | 51 | public boolean isServiceConnected() { 52 | return binder != null; 53 | } 54 | 55 | protected ShoppingListService.ShoppingListBinder getBinder() { 56 | return binder; 57 | } 58 | 59 | protected abstract void onServiceConnected(ShoppingListService.ShoppingListBinder binder); 60 | 61 | protected abstract void onServiceDisconnected(ShoppingListService.ShoppingListBinder binder); 62 | 63 | private class ShoppingListServiceConnection implements ServiceConnection { 64 | 65 | @Override 66 | public void onServiceConnected(ComponentName name, IBinder iBinder) { 67 | binder = ((ShoppingListService.ShoppingListBinder) iBinder); 68 | BinderActivity.this.onServiceConnected(binder); 69 | } 70 | 71 | @Override 72 | public void onServiceDisconnected(ComponentName name) { 73 | BinderActivity.this.onServiceDisconnected(binder); 74 | binder = null; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/activity/EditBar.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.activity; 21 | 22 | import android.content.Context; 23 | import android.os.Bundle; 24 | import android.support.design.widget.FloatingActionButton; 25 | import android.support.v7.widget.RecyclerView; 26 | import android.text.Editable; 27 | import android.text.TextWatcher; 28 | import android.view.GestureDetector; 29 | import android.view.KeyEvent; 30 | import android.view.MotionEvent; 31 | import android.view.View; 32 | import android.view.ViewConfiguration; 33 | import android.view.inputmethod.InputMethodManager; 34 | import android.widget.EditText; 35 | import android.widget.ImageButton; 36 | import android.widget.RelativeLayout; 37 | import android.widget.TextView; 38 | import android.widget.Toast; 39 | 40 | import com.woefe.shoppinglist.R; 41 | import com.woefe.shoppinglist.shoppinglist.ShoppingList; 42 | 43 | import java.util.HashSet; 44 | import java.util.Set; 45 | 46 | public class EditBar implements ShoppingList.ShoppingListListener { 47 | private static final String KEY_SAVED_DESCRIPTION = "SAVED_DESCRIPTION"; 48 | private static final String KEY_SAVED_QUANTITY = "SAVED_QUANTITY"; 49 | private static final String KEY_SAVED_MODE = "SAVED_MODE"; 50 | private static final String KEY_SAVE_IS_VISIBLE = "SAVE_IS_VISIBLE"; 51 | private final Context ctx; 52 | private final RelativeLayout layout; 53 | private final EditText descriptionText; 54 | private final EditText quantityText; 55 | private final TextView duplicateWarnText; 56 | private Mode mode; 57 | private EditBarListener listener; 58 | private final FloatingActionButton fab; 59 | private int position; 60 | private final Set descriptionIndex = new HashSet<>(); 61 | private ShoppingList shoppingList; 62 | 63 | public EditBar(View boundView, final Context ctx) { 64 | this.ctx = ctx; 65 | this.layout = boundView.findViewById(R.id.layout_add_item); 66 | final ImageButton button = boundView.findViewById(R.id.button_add_new_item); 67 | this.descriptionText = boundView.findViewById(R.id.new_item_description); 68 | this.quantityText = boundView.findViewById(R.id.new_item_quantity); 69 | this.duplicateWarnText = boundView.findViewById(R.id.text_warn); 70 | this.mode = Mode.ADD; 71 | 72 | layout.setVisibility(View.GONE); 73 | 74 | quantityText.setOnEditorActionListener(new TextView.OnEditorActionListener() { 75 | @Override 76 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 77 | onConfirm(); 78 | return true; 79 | } 80 | }); 81 | 82 | button.setOnClickListener(new View.OnClickListener() { 83 | @Override 84 | public void onClick(View v) { 85 | onConfirm(); 86 | } 87 | }); 88 | 89 | setButtonEnabled(button, false); 90 | descriptionText.addTextChangedListener(new TextWatcher() { 91 | @Override 92 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 93 | 94 | } 95 | 96 | @Override 97 | public void onTextChanged(CharSequence s, int start, int before, int count) { 98 | String str = s.toString(); 99 | if (str.equals("")) { 100 | setButtonEnabled(button, false); 101 | } else { 102 | setButtonEnabled(button, true); 103 | } 104 | checkDuplicate(str); 105 | } 106 | 107 | @Override 108 | public void afterTextChanged(Editable s) { 109 | 110 | } 111 | }); 112 | 113 | fab = boundView.findViewById(R.id.fab_add); 114 | fab.setOnClickListener(new View.OnClickListener() { 115 | @Override 116 | public void onClick(View v) { 117 | fab.hide(); 118 | showAdd(); 119 | } 120 | }); 121 | } 122 | 123 | private void checkDuplicate(String str) { 124 | if (mode != Mode.ADD || !isVisible()) { 125 | return; 126 | } 127 | if (descriptionIndex.contains(str.toLowerCase())) { 128 | duplicateWarnText.setText(ctx.getString(R.string.duplicate_warning, str)); 129 | duplicateWarnText.setVisibility(View.VISIBLE); 130 | } else { 131 | duplicateWarnText.setVisibility(View.GONE); 132 | } 133 | } 134 | 135 | private void setButtonEnabled(ImageButton button, boolean enabled) { 136 | button.setEnabled(enabled); 137 | button.setClickable(enabled); 138 | button.setImageAlpha(enabled ? 255 : 100); 139 | } 140 | 141 | private void onConfirm() { 142 | String desc = descriptionText.getText().toString(); 143 | String qty = quantityText.getText().toString(); 144 | 145 | if (desc.equals("")) { 146 | Toast.makeText(ctx, R.string.error_description_empty, Toast.LENGTH_SHORT).show(); 147 | return; 148 | } 149 | 150 | if (mode == Mode.ADD) { 151 | listener.onNewItem(desc, qty); 152 | descriptionText.requestFocus(); 153 | } else if (mode == Mode.EDIT) { 154 | listener.onEditSave(position, desc, qty); 155 | } 156 | 157 | descriptionText.setText(""); 158 | quantityText.setText(""); 159 | } 160 | 161 | public void showEdit(int position, String description, String quantity) { 162 | this.position = position; 163 | prepare(Mode.EDIT, description, quantity); 164 | show(); 165 | } 166 | 167 | public void showAdd() { 168 | prepare(Mode.ADD, "", ""); 169 | show(); 170 | } 171 | 172 | private void prepare(Mode mode, String description, String quantity) { 173 | this.mode = mode; 174 | quantityText.setText(quantity); 175 | descriptionText.setText(""); 176 | descriptionText.append(description); 177 | } 178 | 179 | public void enableAutoHideFAB(RecyclerView view) { 180 | final GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() { 181 | private final int slop = ViewConfiguration.get(ctx).getScaledPagingTouchSlop(); 182 | private float start = -1; 183 | private float triggerPosition = -1; 184 | 185 | @Override 186 | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 187 | if (isNewEvent(e1)) { 188 | start = e1.getY(); 189 | } 190 | final float end = e2.getY(); 191 | 192 | if (end - start > slop) { 193 | showFAB(); 194 | start = end; 195 | } else if (end - start < -slop) { 196 | hideFAB(); 197 | start = end; 198 | } 199 | return super.onScroll(e1, e2, distanceX, distanceY); 200 | } 201 | 202 | private boolean isNewEvent(MotionEvent e1) { 203 | boolean isNewEvent = e1 != null && !(e1.getY() == triggerPosition); 204 | if (isNewEvent) { 205 | triggerPosition = e1.getY(); 206 | } 207 | return isNewEvent; 208 | } 209 | }; 210 | 211 | final GestureDetector detector = new GestureDetector(ctx, gestureListener); 212 | 213 | view.setOnTouchListener(new View.OnTouchListener() { 214 | @Override 215 | public boolean onTouch(View v, MotionEvent event) { 216 | detector.onTouchEvent(event); 217 | return false; 218 | } 219 | }); 220 | } 221 | 222 | private void showFAB() { 223 | if (!isVisible()) { 224 | fab.show(); 225 | } 226 | } 227 | 228 | private void hideFAB() { 229 | fab.hide(); 230 | } 231 | 232 | private void show() { 233 | layout.setVisibility(View.VISIBLE); 234 | descriptionText.requestFocus(); 235 | InputMethodManager imm = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE); 236 | if (imm != null) { 237 | imm.showSoftInput(descriptionText, InputMethodManager.SHOW_IMPLICIT); 238 | } 239 | } 240 | 241 | public void hide() { 242 | descriptionText.clearFocus(); 243 | quantityText.clearFocus(); 244 | layout.setVisibility(View.GONE); 245 | duplicateWarnText.setText(""); 246 | duplicateWarnText.setVisibility(View.GONE); 247 | InputMethodManager imm = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE); 248 | if (imm != null) { 249 | imm.hideSoftInputFromWindow(layout.getWindowToken(), 0); 250 | } 251 | fab.show(); 252 | fab.requestFocus(); 253 | } 254 | 255 | public boolean isVisible() { 256 | return layout.getVisibility() != View.GONE; 257 | } 258 | 259 | public void addEditBarListener(EditBarListener l) { 260 | listener = l; 261 | } 262 | 263 | public void removeEditBarListener(EditBarListener l) { 264 | if (l == listener) { 265 | listener = null; 266 | } 267 | } 268 | 269 | public void saveState(Bundle state) { 270 | state.putString(KEY_SAVED_DESCRIPTION, descriptionText.getText().toString()); 271 | state.putString(KEY_SAVED_QUANTITY, quantityText.getText().toString()); 272 | state.putBoolean(KEY_SAVE_IS_VISIBLE, isVisible()); 273 | state.putSerializable(KEY_SAVED_MODE, mode); 274 | } 275 | 276 | public void restoreState(Bundle state) { 277 | String description = state.getString(KEY_SAVED_DESCRIPTION); 278 | String quantity = state.getString(KEY_SAVED_QUANTITY); 279 | Mode mode = (Mode) state.getSerializable(KEY_SAVED_MODE); 280 | if (state.getBoolean(KEY_SAVE_IS_VISIBLE)) { 281 | prepare(mode, description, quantity); 282 | layout.setVisibility(View.VISIBLE); 283 | fab.hide(); 284 | } 285 | } 286 | 287 | public void connectShoppingList(ShoppingList shoppingList) { 288 | this.shoppingList = shoppingList; 289 | this.shoppingList.addListener(this); 290 | onShoppingListUpdate(shoppingList, null); 291 | } 292 | 293 | public void disconnectShoppingList() { 294 | if (shoppingList != null) { 295 | shoppingList.removeListener(this); 296 | shoppingList = null; 297 | } 298 | } 299 | 300 | @Override 301 | public void onShoppingListUpdate(ShoppingList list, ShoppingList.Event e) { 302 | if (mode == Mode.EDIT) { 303 | hide(); 304 | return; 305 | } 306 | descriptionIndex.clear(); 307 | descriptionIndex.addAll(list.createDescriptionIndex()); 308 | checkDuplicate(descriptionText.getText().toString()); 309 | } 310 | 311 | public interface EditBarListener { 312 | void onEditSave(int position, String description, String quantity); 313 | 314 | void onNewItem(String description, String quantity); 315 | } 316 | 317 | private enum Mode { 318 | EDIT, ADD 319 | } 320 | 321 | 322 | } 323 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/activity/InvalidFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.activity; 21 | 22 | import android.app.Fragment; 23 | import android.os.Bundle; 24 | import android.support.annotation.Nullable; 25 | import android.view.LayoutInflater; 26 | import android.view.View; 27 | import android.view.ViewGroup; 28 | 29 | import com.woefe.shoppinglist.R; 30 | 31 | public class InvalidFragment extends Fragment { 32 | @Nullable 33 | @Override 34 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { 35 | return inflater.inflate(R.layout.fragment_invalid, container, false); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/activity/RecyclerListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.woefe.shoppinglist.activity; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.drawable.ColorDrawable; 6 | import android.graphics.drawable.Drawable; 7 | import android.support.annotation.NonNull; 8 | import android.support.v4.content.ContextCompat; 9 | import android.support.v7.widget.RecyclerView; 10 | import android.support.v7.widget.helper.ItemTouchHelper; 11 | import android.view.LayoutInflater; 12 | import android.view.MotionEvent; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | import android.widget.ImageView; 16 | import android.widget.TextView; 17 | 18 | import com.woefe.shoppinglist.R; 19 | import com.woefe.shoppinglist.shoppinglist.ListItem; 20 | import com.woefe.shoppinglist.shoppinglist.ShoppingList; 21 | 22 | /** 23 | * @author Wolfgang Popp 24 | */ 25 | public class RecyclerListAdapter extends RecyclerView.Adapter { 26 | private final int colorChecked; 27 | private final int colorDefault; 28 | private final int colorBackground; 29 | private ShoppingList shoppingList; 30 | private ItemTouchHelper touchHelper; 31 | private ItemLongClickListener longClickListener; 32 | 33 | private final ShoppingList.ShoppingListListener listener = new ShoppingList.ShoppingListListener() { 34 | @Override 35 | public void onShoppingListUpdate(ShoppingList list, ShoppingList.Event e) { 36 | switch (e.getState()) { 37 | case ShoppingList.Event.ITEM_CHANGED: 38 | notifyItemChanged(e.getIndex()); 39 | break; 40 | case ShoppingList.Event.ITEM_INSERTED: 41 | notifyItemInserted(e.getIndex()); 42 | break; 43 | case ShoppingList.Event.ITEM_MOVED: 44 | notifyItemMoved(e.getOldIndex(), e.getNewIndex()); 45 | break; 46 | case ShoppingList.Event.ITEM_REMOVED: 47 | notifyItemRemoved(e.getIndex()); 48 | break; 49 | default: 50 | notifyDataSetChanged(); 51 | } 52 | } 53 | }; 54 | 55 | public RecyclerListAdapter(Context ctx) { 56 | colorChecked = ContextCompat.getColor(ctx, R.color.textColorChecked); 57 | colorDefault = ContextCompat.getColor(ctx, R.color.textColorDefault); 58 | colorBackground = ContextCompat.getColor(ctx, R.color.colorListItemBackground); 59 | touchHelper = new ItemTouchHelper(new RecyclerListCallback(ctx)); 60 | } 61 | 62 | public void connectShoppingList(ShoppingList shoppingList) { 63 | this.shoppingList = shoppingList; 64 | shoppingList.addListener(listener); 65 | notifyDataSetChanged(); 66 | } 67 | 68 | public void disconnectShoppingList() { 69 | if (shoppingList != null) { 70 | shoppingList.removeListener(listener); 71 | shoppingList = null; 72 | } 73 | } 74 | 75 | public void move(int fromPos, int toPos) { 76 | shoppingList.move(fromPos, toPos); 77 | } 78 | 79 | public void remove(int pos) { 80 | shoppingList.remove(pos); 81 | } 82 | 83 | public void registerRecyclerView(RecyclerView view) { 84 | touchHelper.attachToRecyclerView(view); 85 | } 86 | 87 | public void setOnItemLongClickListener(ItemLongClickListener listener) { 88 | this.longClickListener = listener; 89 | } 90 | 91 | @NonNull 92 | @Override 93 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 94 | View v = LayoutInflater.from(parent.getContext()) 95 | .inflate(R.layout.list_item, parent, false); 96 | return new ViewHolder(v); 97 | } 98 | 99 | @Override 100 | public void onBindViewHolder(@NonNull final ViewHolder holder, int position) { 101 | ListItem listItem = shoppingList.get(position); 102 | holder.description.setText(listItem.getDescription()); 103 | holder.quantity.setText(listItem.getQuantity()); 104 | 105 | if (listItem.isChecked()) { 106 | holder.description.setTextColor(colorChecked); 107 | holder.quantity.setTextColor(colorChecked); 108 | } else { 109 | holder.description.setTextColor(colorDefault); 110 | holder.quantity.setTextColor(colorDefault); 111 | } 112 | 113 | holder.itemView.setBackgroundColor(colorBackground); 114 | 115 | holder.view.setOnClickListener(new View.OnClickListener() { 116 | @Override 117 | public void onClick(View v) { 118 | shoppingList.toggleChecked(holder.getAdapterPosition()); 119 | } 120 | }); 121 | 122 | 123 | holder.view.setOnLongClickListener(new View.OnLongClickListener() { 124 | @Override 125 | public boolean onLongClick(View v) { 126 | return longClickListener != null 127 | && longClickListener.onLongClick(holder.getAdapterPosition()); 128 | } 129 | }); 130 | 131 | holder.dragHandler.setOnTouchListener(new View.OnTouchListener() { 132 | @Override 133 | public boolean onTouch(View v, MotionEvent event) { 134 | if (event.getAction() == MotionEvent.ACTION_DOWN) { 135 | touchHelper.startDrag(holder); 136 | return true; 137 | } 138 | return false; 139 | } 140 | }); 141 | 142 | } 143 | 144 | @Override 145 | public int getItemCount() { 146 | if (shoppingList != null) { 147 | return shoppingList.size(); 148 | } 149 | return 0; 150 | } 151 | 152 | public interface ItemLongClickListener { 153 | boolean onLongClick(int position); 154 | } 155 | 156 | public static class ViewHolder extends RecyclerView.ViewHolder { 157 | TextView description; 158 | TextView quantity; 159 | ImageView dragHandler; 160 | View view; 161 | 162 | public ViewHolder(View itemView) { 163 | super(itemView); 164 | view = itemView; 165 | description = itemView.findViewById(R.id.text_description); 166 | quantity = itemView.findViewById(R.id.text_quantity); 167 | dragHandler = itemView.findViewById(R.id.drag_n_drop_handler); 168 | } 169 | } 170 | 171 | public class RecyclerListCallback extends ItemTouchHelper.Callback { 172 | private ColorDrawable background; 173 | private Drawable deleteIcon; 174 | private int backgroundColor; 175 | 176 | public RecyclerListCallback(Context ctx) { 177 | this.background = new ColorDrawable(); 178 | this.deleteIcon = ContextCompat.getDrawable(ctx, R.drawable.ic_delete_forever_white_24); 179 | this.backgroundColor = ContextCompat.getColor(ctx, R.color.colorCritical); 180 | } 181 | 182 | @Override 183 | public boolean isItemViewSwipeEnabled() { 184 | return true; 185 | } 186 | 187 | @Override 188 | public boolean isLongPressDragEnabled() { 189 | return false; 190 | } 191 | 192 | @Override 193 | public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { 194 | final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; 195 | final int swipeFlags = ItemTouchHelper.START; 196 | return makeMovementFlags(dragFlags, swipeFlags); 197 | } 198 | 199 | @Override 200 | public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { 201 | if (viewHolder.getItemViewType() != target.getItemViewType()) { 202 | return false; 203 | } 204 | 205 | RecyclerListAdapter.this.move(viewHolder.getAdapterPosition(), target.getAdapterPosition()); 206 | return true; 207 | } 208 | 209 | @Override 210 | public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { 211 | RecyclerListAdapter.this.remove(viewHolder.getAdapterPosition()); 212 | } 213 | 214 | @Override 215 | public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { 216 | 217 | if (actionState != ItemTouchHelper.ACTION_STATE_SWIPE) { 218 | super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); 219 | return; 220 | } 221 | 222 | View itemView = viewHolder.itemView; 223 | 224 | int backgroundLeft = itemView.getRight() + (int) dX; 225 | background.setBounds(backgroundLeft, itemView.getTop(), itemView.getRight(), itemView.getBottom()); 226 | background.setColor(backgroundColor); 227 | background.draw(c); 228 | 229 | int itemHeight = itemView.getBottom() - itemView.getTop(); 230 | int intrinsicHeight = deleteIcon.getIntrinsicHeight(); 231 | int iconTop = itemView.getTop() + (itemHeight - intrinsicHeight) / 2; 232 | int iconMargin = (itemHeight - intrinsicHeight) / 2; 233 | int iconLeft = itemView.getRight() - iconMargin - deleteIcon.getIntrinsicWidth(); 234 | int iconRight = itemView.getRight() - iconMargin; 235 | int iconBottom = iconTop + intrinsicHeight; 236 | deleteIcon.setBounds(iconLeft, iconTop, iconRight, iconBottom); 237 | deleteIcon.draw(c); 238 | 239 | // Fade out the view as it is swiped out of the parent's bounds 240 | final float alpha = 1.0f - Math.abs(dX) / (float) itemView.getWidth(); 241 | itemView.setAlpha(alpha); 242 | itemView.setTranslationX(dX); 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/activity/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.activity; 21 | 22 | import android.os.Bundle; 23 | import android.support.v7.app.AppCompatActivity; 24 | 25 | /** 26 | * @author Wolfgang Popp. 27 | */ 28 | public class SettingsActivity extends AppCompatActivity { 29 | 30 | @Override 31 | protected void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | getSupportFragmentManager().beginTransaction() 34 | .replace(android.R.id.content, new SettingsFragment()) 35 | .commit(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/activity/SettingsFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.activity; 21 | 22 | import android.app.Activity; 23 | import android.content.Intent; 24 | import android.content.SharedPreferences; 25 | import android.os.Bundle; 26 | import android.support.annotation.Nullable; 27 | import android.support.v4.app.FragmentActivity; 28 | import android.support.v7.preference.EditTextPreference; 29 | import android.support.v7.preference.Preference; 30 | import android.support.v7.preference.PreferenceCategory; 31 | import android.support.v7.preference.PreferenceFragmentCompat; 32 | import android.support.v7.preference.PreferenceManager; 33 | 34 | import com.woefe.shoppinglist.R; 35 | import com.woefe.shoppinglist.dialog.DirectoryChooser; 36 | 37 | /** 38 | * @author Wolfgang Popp. 39 | */ 40 | public class SettingsFragment extends PreferenceFragmentCompat implements 41 | SharedPreferences.OnSharedPreferenceChangeListener { 42 | 43 | public static final String KEY_DIRECTORY_LOCATION = "FILE_LOCATION"; 44 | private static final int REQUEST_CODE_CHOOSE_DIR = 123; 45 | 46 | @Override 47 | public void onCreate(Bundle savedInstanceState) { 48 | super.onCreate(savedInstanceState); 49 | addPreferencesFromResource(R.xml.preferences); 50 | getSharedPreferences().registerOnSharedPreferenceChangeListener(this); 51 | 52 | // show the current value in the settings screen 53 | for (int i = 0; i < getPreferenceScreen().getPreferenceCount(); i++) { 54 | initSummary(getPreferenceScreen().getPreference(i)); 55 | } 56 | } 57 | 58 | @Override 59 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 60 | 61 | } 62 | 63 | @Override 64 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 65 | super.onActivityCreated(savedInstanceState); 66 | 67 | final Preference fileLocationPref = findPreference("FILE_LOCATION"); 68 | 69 | fileLocationPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { 70 | @Override 71 | public boolean onPreferenceClick(Preference preference) { 72 | Intent intent = new Intent(getContext(), DirectoryChooser.class); 73 | startActivityForResult(intent, REQUEST_CODE_CHOOSE_DIR); 74 | return true; 75 | } 76 | }); 77 | } 78 | 79 | @Override 80 | public void onDestroy() { 81 | super.onDestroy(); 82 | getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); 83 | } 84 | 85 | private void initSummary(Preference p) { 86 | if (p instanceof PreferenceCategory) { 87 | PreferenceCategory cat = (PreferenceCategory) p; 88 | for (int i = 0; i < cat.getPreferenceCount(); i++) { 89 | initSummary(cat.getPreference(i)); 90 | } 91 | } else { 92 | updatePreferences(p); 93 | } 94 | } 95 | 96 | private void updatePreferences(Preference p) { 97 | if (KEY_DIRECTORY_LOCATION.equals(p.getKey())) { 98 | String path = getSharedPreferences().getString(KEY_DIRECTORY_LOCATION, ""); 99 | p.setSummary(path); 100 | } 101 | if (p instanceof EditTextPreference) { 102 | EditTextPreference editTextPref = (EditTextPreference) p; 103 | p.setSummary(editTextPref.getText()); 104 | } 105 | } 106 | 107 | private SharedPreferences getSharedPreferences() { 108 | FragmentActivity activity = getActivity(); 109 | assert activity != null; 110 | return PreferenceManager.getDefaultSharedPreferences(activity); 111 | } 112 | 113 | @Override 114 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 115 | if (key.equals(KEY_DIRECTORY_LOCATION)) { 116 | Preference p = findPreference(key); 117 | updatePreferences(p); 118 | } 119 | } 120 | 121 | @Override 122 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 123 | super.onActivityResult(requestCode, resultCode, data); 124 | switch (requestCode) { 125 | case (REQUEST_CODE_CHOOSE_DIR): { 126 | if (resultCode == Activity.RESULT_OK) { 127 | String path = data.getStringExtra(DirectoryChooser.SELECTED_PATH); 128 | SharedPreferences.Editor editor = getSharedPreferences().edit(); 129 | editor.putString(KEY_DIRECTORY_LOCATION, path).apply(); 130 | } 131 | break; 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/activity/ShoppingListFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.activity; 21 | 22 | import android.app.Fragment; 23 | import android.os.Bundle; 24 | import android.support.annotation.Nullable; 25 | import android.support.v7.widget.DividerItemDecoration; 26 | import android.support.v7.widget.LinearLayoutManager; 27 | import android.support.v7.widget.RecyclerView; 28 | import android.view.LayoutInflater; 29 | import android.view.View; 30 | import android.view.ViewGroup; 31 | 32 | import com.woefe.shoppinglist.R; 33 | import com.woefe.shoppinglist.shoppinglist.ListItem; 34 | import com.woefe.shoppinglist.shoppinglist.ShoppingList; 35 | 36 | public class ShoppingListFragment extends Fragment implements EditBar.EditBarListener { 37 | 38 | private EditBar editBar; 39 | private RecyclerView recyclerView; 40 | private RecyclerView.LayoutManager layoutManager; 41 | private RecyclerListAdapter adapter; 42 | private View rootView; 43 | private ShoppingList shoppingList; 44 | 45 | public static ShoppingListFragment newInstance(ShoppingList shoppingList) { 46 | ShoppingListFragment fragment = new ShoppingListFragment(); 47 | fragment.setShoppingList(shoppingList); 48 | return fragment; 49 | } 50 | 51 | public void setShoppingList(ShoppingList shoppingList) { 52 | this.shoppingList = shoppingList; 53 | connectList(); 54 | } 55 | 56 | @Override 57 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { 58 | rootView = inflater.inflate(R.layout.fragment_shoppinglist, container, false); 59 | 60 | recyclerView = rootView.findViewById(R.id.shoppingListView); 61 | registerForContextMenu(recyclerView); 62 | 63 | recyclerView.setHasFixedSize(true); 64 | 65 | layoutManager = new LinearLayoutManager(getActivity()); 66 | recyclerView.setLayoutManager(layoutManager); 67 | 68 | DividerItemDecoration divider = new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL); 69 | recyclerView.addItemDecoration(divider); 70 | 71 | editBar = new EditBar(rootView, getActivity()); 72 | editBar.addEditBarListener(this); 73 | editBar.enableAutoHideFAB(recyclerView); 74 | 75 | if (savedInstanceState != null) { 76 | editBar.restoreState(savedInstanceState); 77 | } 78 | 79 | return rootView; 80 | } 81 | 82 | @Override 83 | public void onDestroyView() { 84 | editBar.removeEditBarListener(this); 85 | editBar.hide(); 86 | super.onDestroyView(); 87 | } 88 | 89 | private void connectList() { 90 | if (shoppingList != null && adapter != null) { 91 | adapter.connectShoppingList(shoppingList); 92 | } 93 | if (shoppingList != null && editBar != null) { 94 | editBar.connectShoppingList(shoppingList); 95 | } 96 | } 97 | 98 | @Override 99 | public void onStop() { 100 | adapter.disconnectShoppingList(); 101 | editBar.disconnectShoppingList(); 102 | super.onStop(); 103 | } 104 | 105 | @Override 106 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 107 | super.onActivityCreated(savedInstanceState); 108 | adapter = new RecyclerListAdapter(getActivity()); 109 | connectList(); 110 | adapter.registerRecyclerView(recyclerView); 111 | adapter.setOnItemLongClickListener(new RecyclerListAdapter.ItemLongClickListener() { 112 | @Override 113 | public boolean onLongClick(int position) { 114 | ListItem listItem = shoppingList.get(position); 115 | editBar.showEdit(position, listItem.getDescription(), listItem.getQuantity()); 116 | return true; 117 | } 118 | }); 119 | recyclerView.setAdapter(adapter); 120 | } 121 | 122 | public boolean onBackPressed() { 123 | if (editBar.isVisible()) { 124 | editBar.hide(); 125 | return true; 126 | } 127 | return false; 128 | } 129 | 130 | @Override 131 | public void onSaveInstanceState(Bundle outState) { 132 | super.onSaveInstanceState(outState); 133 | editBar.saveState(outState); 134 | } 135 | 136 | @Override 137 | public void onEditSave(int position, String description, String quantity) { 138 | shoppingList.editItem(position, description, quantity); 139 | editBar.hide(); 140 | recyclerView.smoothScrollToPosition(position); 141 | } 142 | 143 | @Override 144 | public void onNewItem(String description, String quantity) { 145 | shoppingList.add(description, quantity); 146 | recyclerView.smoothScrollToPosition(recyclerView.getAdapter().getItemCount() - 1); 147 | } 148 | 149 | public void removeAllCheckedItems() { 150 | shoppingList.removeAllCheckedItems(); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/dialog/ConfirmationDialog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.dialog; 21 | 22 | import android.app.AlertDialog; 23 | import android.app.Dialog; 24 | import android.content.Context; 25 | import android.content.DialogInterface; 26 | import android.os.Bundle; 27 | import android.support.annotation.NonNull; 28 | import android.support.v4.app.DialogFragment; 29 | import android.support.v7.app.AppCompatActivity; 30 | import android.text.Html; 31 | 32 | import com.woefe.shoppinglist.R; 33 | 34 | /** 35 | * @author Wolfgang Popp. 36 | */ 37 | public class ConfirmationDialog extends DialogFragment { 38 | private static final String TAG = ConfirmationDialog.class.getSimpleName(); 39 | private static final String KEY_MESSAGE = "MESSAGE"; 40 | private ConfirmationDialogListener listener; 41 | private String message; 42 | private int action; 43 | 44 | 45 | public interface ConfirmationDialogListener { 46 | void onPositiveButtonClicked(int action); 47 | 48 | void onNegativeButtonClicked(int action); 49 | } 50 | 51 | public static void show(AppCompatActivity activity, String message, int action) { 52 | ConfirmationDialog dialog = new ConfirmationDialog(); 53 | dialog.message = message; 54 | dialog.action = action; 55 | dialog.show(activity.getSupportFragmentManager(), TAG); 56 | } 57 | 58 | @Override 59 | public void onAttach(Context ctx) { 60 | super.onAttach(ctx); 61 | listener = (ConfirmationDialogListener) ctx; 62 | } 63 | 64 | @NonNull 65 | @Override 66 | public Dialog onCreateDialog(Bundle savedInstanceState) { 67 | super.onCreateDialog(savedInstanceState); 68 | 69 | if (savedInstanceState != null) { 70 | message = savedInstanceState.getString(KEY_MESSAGE); 71 | } 72 | 73 | AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 74 | builder.setMessage(Html.fromHtml(message)) 75 | .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { 76 | @Override 77 | public void onClick(DialogInterface dialog, int which) { 78 | listener.onPositiveButtonClicked(action); 79 | } 80 | }) 81 | .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { 82 | @Override 83 | public void onClick(DialogInterface dialog, int which) { 84 | listener.onNegativeButtonClicked(action); 85 | } 86 | }); 87 | 88 | return builder.create(); 89 | } 90 | 91 | @Override 92 | public void onSaveInstanceState(Bundle outState) { 93 | super.onSaveInstanceState(outState); 94 | outState.putString(KEY_MESSAGE, message); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/dialog/DirectoryChooser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.dialog; 21 | 22 | import android.Manifest; 23 | import android.app.AlertDialog; 24 | import android.content.DialogInterface; 25 | import android.content.Intent; 26 | import android.content.pm.PackageManager; 27 | import android.os.Bundle; 28 | import android.os.Environment; 29 | import android.support.annotation.Nullable; 30 | import android.support.design.widget.FloatingActionButton; 31 | import android.support.v4.app.ActivityCompat; 32 | import android.support.v4.content.ContextCompat; 33 | import android.support.v7.app.AppCompatActivity; 34 | import android.view.View; 35 | import android.widget.AdapterView; 36 | import android.widget.ArrayAdapter; 37 | import android.widget.Button; 38 | import android.widget.ImageButton; 39 | import android.widget.ListView; 40 | import android.widget.TextView; 41 | import android.widget.Toast; 42 | 43 | import com.woefe.shoppinglist.R; 44 | 45 | import java.io.File; 46 | import java.util.ArrayList; 47 | import java.util.Arrays; 48 | import java.util.Collections; 49 | import java.util.Comparator; 50 | import java.util.List; 51 | import java.util.ListIterator; 52 | 53 | /** 54 | * @author Wolfgang Popp 55 | */ 56 | 57 | public class DirectoryChooser extends AppCompatActivity implements TextInputDialog.TextInputDialogListener { 58 | public static final String SELECTED_PATH = "SELECTED_PATH"; 59 | private static final String KEY_CURRENT_DIR = "CURRENT_DIR"; 60 | private static final int ACTION_READ_INPUT = 1; 61 | private static final String PARENT_DIR = ".."; 62 | private static final int REQUEST_CODE_EXT_STORAGE = 2; 63 | 64 | private ArrayAdapter directoryViewAdapter; 65 | private File currentDirectory; 66 | private TextView title; 67 | 68 | @Override 69 | protected void onCreate(@Nullable Bundle savedInstanceState) { 70 | super.onCreate(savedInstanceState); 71 | setContentView(R.layout.dialog_directory_chooser); 72 | 73 | directoryViewAdapter = new ArrayAdapter<>(this, R.layout.drawer_list_item); 74 | ListView directoryView = findViewById(R.id.directoryListView); 75 | directoryView.setOnItemClickListener(new ListView.OnItemClickListener() { 76 | @Override 77 | public void onItemClick(AdapterView parent, View view, int position, long id) { 78 | String selectedDir = directoryViewAdapter.getItem(position); 79 | if (selectedDir != null) { 80 | if (selectedDir.equals(PARENT_DIR)) { 81 | changeDirectory(currentDirectory.getParentFile()); 82 | } else { 83 | changeDirectory(new File(currentDirectory, selectedDir)); 84 | } 85 | } 86 | } 87 | }); 88 | directoryView.setAdapter(directoryViewAdapter); 89 | 90 | 91 | Button okButton = findViewById(R.id.button_dialog_ok); 92 | okButton.setOnClickListener(new View.OnClickListener() { 93 | @Override 94 | public void onClick(View v) { 95 | accept(); 96 | } 97 | }); 98 | 99 | Button cancelButton = findViewById(R.id.button_dialog_cancel); 100 | cancelButton.setOnClickListener(new View.OnClickListener() { 101 | @Override 102 | public void onClick(View v) { 103 | cancel(); 104 | } 105 | }); 106 | 107 | ImageButton storageButton = findViewById(R.id.button_choose_storage); 108 | storageButton.setOnClickListener(new View.OnClickListener() { 109 | @Override 110 | public void onClick(View v) { 111 | chooseStorageLocation(); 112 | } 113 | }); 114 | 115 | FloatingActionButton fab = findViewById(R.id.fab_add); 116 | fab.setOnClickListener(new View.OnClickListener() { 117 | @Override 118 | public void onClick(View v) { 119 | createNewDir(); 120 | } 121 | }); 122 | 123 | title = findViewById(R.id.title); 124 | 125 | File directory; 126 | String savedDir; 127 | File[] storageLocations = listStorageLocations(); 128 | 129 | if (savedInstanceState != null 130 | && (savedDir = savedInstanceState.getString(KEY_CURRENT_DIR)) != null) { 131 | 132 | directory = new File(savedDir); 133 | } else if (storageLocations.length > 0) { 134 | directory = storageLocations[0]; 135 | } else { 136 | directory = new File(""); // Jeez you phone is broken?!?! 137 | } 138 | 139 | changeDirectory(directory); 140 | } 141 | 142 | @Override 143 | protected void onStart() { 144 | super.onStart(); 145 | 146 | int result = ContextCompat.checkSelfPermission(this, 147 | Manifest.permission.WRITE_EXTERNAL_STORAGE); 148 | 149 | if (result == PackageManager.PERMISSION_DENIED) { 150 | ActivityCompat.requestPermissions(this, 151 | new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 152 | REQUEST_CODE_EXT_STORAGE); 153 | } 154 | } 155 | 156 | @Override 157 | public void onInputComplete(String input, int action) { 158 | if (action == ACTION_READ_INPUT) { 159 | File newDir = new File(currentDirectory, input); 160 | boolean success = newDir.mkdir(); 161 | if (success) { 162 | changeDirectory(newDir); 163 | } else { 164 | Toast.makeText(this, getString(R.string.err_create_dir, input), Toast.LENGTH_LONG).show(); 165 | } 166 | } 167 | } 168 | 169 | @Override 170 | public void onSaveInstanceState(Bundle outState) { 171 | outState.putString(KEY_CURRENT_DIR, currentDirectory.getAbsolutePath()); 172 | super.onSaveInstanceState(outState); 173 | } 174 | 175 | private void accept() { 176 | if (!currentDirectory.canWrite()) { 177 | Toast.makeText(this, "Cannot write to directory", Toast.LENGTH_LONG).show(); 178 | return; 179 | } 180 | 181 | Intent intent = new Intent(); 182 | intent.putExtra(SELECTED_PATH, currentDirectory.getAbsolutePath()); 183 | setResult(RESULT_OK, intent); 184 | finish(); 185 | } 186 | 187 | private void cancel() { 188 | setResult(RESULT_CANCELED); 189 | finish(); 190 | } 191 | 192 | private void createNewDir() { 193 | NewDirectoryDialog.Builder builder = 194 | new TextInputDialog.Builder(this, NewDirectoryDialog.class); 195 | 196 | builder.setAction(ACTION_READ_INPUT) 197 | .setMessage(R.string.create_new_dir) 198 | .setHint(R.string.name_of_new_dir) 199 | .show(); 200 | } 201 | 202 | private void chooseStorageLocation() { 203 | AlertDialog.Builder builder = new AlertDialog.Builder(this); 204 | final File[] locations = listStorageLocations(); 205 | final String[] locationNames = new String[locations.length]; 206 | for (int i = 0; i < locations.length; i++) { 207 | locationNames[i] = locations[i].getAbsolutePath(); 208 | } 209 | 210 | builder.setTitle(R.string.select_storage_location) 211 | .setItems(locationNames, new DialogInterface.OnClickListener() { 212 | @Override 213 | public void onClick(DialogInterface dialog, int which) { 214 | changeDirectory(locations[which]); 215 | } 216 | }).create().show(); 217 | } 218 | 219 | private File[] listStorageLocations() { 220 | final List locations = new ArrayList<>(); 221 | 222 | locations.add(Environment.getExternalStorageDirectory()); 223 | locations.add(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)); 224 | locations.add(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)); 225 | locations.addAll(Arrays.asList(getExternalFilesDirs(Environment.DIRECTORY_DOCUMENTS))); 226 | 227 | ListIterator it = locations.listIterator(); 228 | while (it.hasNext()) { 229 | File directory = it.next(); 230 | if (directory == null 231 | || !Environment.MEDIA_MOUNTED.equals(Environment.getStorageState(directory)) 232 | || !directory.canExecute() 233 | || !directory.canRead()) { 234 | 235 | it.remove(); 236 | } 237 | } 238 | 239 | locations.add(getFilesDir()); 240 | 241 | return locations.toArray(new File[locations.size()]); 242 | } 243 | 244 | private void changeDirectory(File directory) { 245 | if (directory == null 246 | || !directory.canRead() 247 | || !directory.canExecute()) { 248 | 249 | Toast.makeText(this, R.string.warn_no_dir_access, Toast.LENGTH_LONG).show(); 250 | return; 251 | } 252 | 253 | List directories = new ArrayList<>(); 254 | for (File file : directory.listFiles()) { 255 | if (file.isDirectory()) { 256 | directories.add(file.getName()); 257 | } 258 | } 259 | 260 | Collections.sort(directories, new Comparator() { 261 | @Override 262 | public int compare(String o1, String o2) { 263 | return o1.compareToIgnoreCase(o2); 264 | } 265 | }); 266 | 267 | title.setText(directory.getAbsolutePath()); 268 | directoryViewAdapter.clear(); 269 | directoryViewAdapter.add(PARENT_DIR); 270 | directoryViewAdapter.addAll(directories); 271 | currentDirectory = directory; 272 | } 273 | 274 | public static class NewDirectoryDialog extends TextInputDialog { 275 | @Override 276 | public boolean onValidateInput(String input) { 277 | boolean isValid = !input.contains("/"); 278 | if (!isValid) { 279 | Toast.makeText(getContext(), R.string.err_illegal_char, Toast.LENGTH_LONG).show(); 280 | } 281 | return isValid; 282 | } 283 | } 284 | } 285 | 286 | 287 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/dialog/TextInputDialog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.dialog; 21 | 22 | import android.content.Context; 23 | import android.os.Bundle; 24 | import android.support.annotation.NonNull; 25 | import android.support.annotation.Nullable; 26 | import android.support.annotation.StringRes; 27 | import android.support.v4.app.DialogFragment; 28 | import android.support.v4.app.Fragment; 29 | import android.support.v4.app.FragmentActivity; 30 | import android.support.v4.app.FragmentManager; 31 | import android.util.Log; 32 | import android.view.KeyEvent; 33 | import android.view.LayoutInflater; 34 | import android.view.View; 35 | import android.view.ViewGroup; 36 | import android.widget.Button; 37 | import android.widget.EditText; 38 | import android.widget.TextView; 39 | 40 | import com.woefe.shoppinglist.R; 41 | 42 | public class TextInputDialog extends DialogFragment { 43 | private static final String TAG = DialogFragment.class.getSimpleName(); 44 | private static final String KEY_MESSAGE = "MESSAGE"; 45 | private static final String KEY_INPUT = "INPUT"; 46 | private static final String KEY_HINT = "INPUT"; 47 | private TextInputDialogListener listener; 48 | private String message; 49 | private String hint; 50 | private int action; 51 | private EditText inputField; 52 | 53 | 54 | public interface TextInputDialogListener { 55 | void onInputComplete(String input, int action); 56 | } 57 | 58 | @Override 59 | public void onAttach(Context ctx) { 60 | super.onAttach(ctx); 61 | Fragment owner = getParentFragment(); 62 | if (ctx instanceof TextInputDialogListener) { 63 | listener = (TextInputDialogListener) ctx; 64 | } else if (owner instanceof TextInputDialogListener) { 65 | listener = (TextInputDialogListener) owner; 66 | } else { 67 | Log.e(TAG, "Dialog not attached"); 68 | } 69 | } 70 | 71 | @Nullable 72 | @Override 73 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { 74 | super.onCreateView(inflater, container, savedInstanceState); 75 | 76 | String inputText = ""; 77 | if (savedInstanceState != null) { 78 | message = savedInstanceState.getString(KEY_MESSAGE); 79 | hint = savedInstanceState.getString(KEY_HINT); 80 | inputText = savedInstanceState.getString(KEY_INPUT); 81 | } 82 | 83 | View dialogRoot = inflater.inflate(R.layout.dialog_text_input, container, false); 84 | TextView label = dialogRoot.findViewById(R.id.dialog_label); 85 | Button cancelButton = dialogRoot.findViewById(R.id.button_dialog_cancel); 86 | Button okButton = dialogRoot.findViewById(R.id.button_dialog_ok); 87 | label.setText(message); 88 | 89 | inputField = dialogRoot.findViewById(R.id.dialog_text_field); 90 | inputField.setHint(hint); 91 | inputField.setText(inputText); 92 | inputField.requestFocus(); 93 | 94 | inputField.setOnEditorActionListener(new TextView.OnEditorActionListener() { 95 | @Override 96 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 97 | onInputComplete(); 98 | return true; 99 | } 100 | }); 101 | 102 | cancelButton.setOnClickListener(new View.OnClickListener() { 103 | @Override 104 | public void onClick(View v) { 105 | dismiss(); 106 | } 107 | }); 108 | 109 | okButton.setOnClickListener(new View.OnClickListener() { 110 | @Override 111 | public void onClick(View v) { 112 | onInputComplete(); 113 | } 114 | }); 115 | 116 | return dialogRoot; 117 | } 118 | 119 | private void onInputComplete() { 120 | String input = inputField.getText().toString(); 121 | if (onValidateInput(input)) { 122 | listener.onInputComplete(input, action); 123 | dismiss(); 124 | } 125 | } 126 | 127 | public boolean onValidateInput(String input) { 128 | return true; 129 | } 130 | 131 | @Override 132 | public void onSaveInstanceState(Bundle outState) { 133 | super.onSaveInstanceState(outState); 134 | outState.putString(KEY_MESSAGE, message); 135 | outState.putString(KEY_HINT, hint); 136 | outState.putString(KEY_INPUT, inputField.getText().toString()); 137 | } 138 | 139 | public static class Builder { 140 | private final FragmentActivity activity; 141 | private TextInputDialog dialog; 142 | private FragmentManager fragmentManager; 143 | 144 | public Builder(FragmentActivity activity, Class clazz) { 145 | this.activity = activity; 146 | try { 147 | this.dialog = clazz.newInstance(); 148 | } catch (java.lang.InstantiationException | IllegalAccessException e) { 149 | throw new IllegalStateException("Cannot start dialog" + clazz.getSimpleName()); 150 | } 151 | } 152 | 153 | public Builder setMessage(String message) { 154 | dialog.message = message; 155 | return this; 156 | } 157 | 158 | public Builder setMessage(@StringRes int messageID) { 159 | return setMessage(activity.getString(messageID)); 160 | } 161 | 162 | public Builder setHint(String hint) { 163 | dialog.hint = hint; 164 | return this; 165 | } 166 | 167 | public Builder setHint(@StringRes int hintID) { 168 | return setHint(activity.getString(hintID)); 169 | } 170 | 171 | public Builder setAction(int action) { 172 | dialog.action = action; 173 | return this; 174 | } 175 | 176 | public Builder setFragmentManager(FragmentManager manager) { 177 | fragmentManager = manager; 178 | return this; 179 | } 180 | 181 | public Builder setTargetFragment(Fragment fragment, int requestCode) { 182 | dialog.setTargetFragment(fragment, requestCode); 183 | return this; 184 | } 185 | 186 | public void show() { 187 | if (fragmentManager == null) { 188 | fragmentManager = activity.getSupportFragmentManager(); 189 | } 190 | dialog.show(fragmentManager, TAG); 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/shoppinglist/DirectoryStatus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.shoppinglist; 21 | 22 | import android.content.Context; 23 | import android.content.SharedPreferences; 24 | import android.preference.PreferenceManager; 25 | 26 | import com.woefe.shoppinglist.activity.SettingsFragment; 27 | 28 | import java.io.File; 29 | 30 | /** 31 | * @author Wolfgang Popp 32 | */ 33 | class DirectoryStatus { 34 | public enum Status {IS_OK, NOT_A_DIRECTORY, CANNOT_WRITE} 35 | 36 | private static final String DEFAULT_DIRECTORY = "ShoppingLists"; 37 | private Status reason; 38 | private String directory; 39 | 40 | public DirectoryStatus(Context ctx) { 41 | //ctx = ctx.getApplicationContext(); 42 | SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ctx); 43 | String directory = sharedPreferences.getString(SettingsFragment.KEY_DIRECTORY_LOCATION, "").trim(); 44 | String defaultDir = ctx.getFileStreamPath(DEFAULT_DIRECTORY).getAbsolutePath(); 45 | File file = new File(directory); 46 | 47 | if (directory.equals("")) { 48 | init(Status.IS_OK, defaultDir); 49 | } else if (!file.isDirectory()) { 50 | init(Status.NOT_A_DIRECTORY, defaultDir); 51 | } else if (!file.canWrite()) { 52 | init(Status.CANNOT_WRITE, defaultDir); 53 | } else { 54 | init(Status.IS_OK, directory); 55 | } 56 | } 57 | 58 | private void init(Status reason, String directory) { 59 | this.reason = reason; 60 | this.directory = directory; 61 | new File(directory).mkdirs(); 62 | } 63 | 64 | public boolean isFallback() { 65 | return reason != Status.IS_OK; 66 | } 67 | 68 | public String getDirectory() { 69 | return directory; 70 | } 71 | 72 | public Status getReason() { 73 | return reason; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/shoppinglist/ListItem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.shoppinglist; 21 | 22 | /** 23 | * @author Wolfgang Popp. 24 | */ 25 | public class ListItem { 26 | private boolean isChecked; 27 | private String description; 28 | private String quantity; 29 | 30 | public ListItem(boolean isChecked, String description, String quantity) { 31 | this.isChecked = isChecked; 32 | this.description = description; 33 | this.quantity = quantity; 34 | } 35 | 36 | public boolean isChecked() { 37 | return isChecked; 38 | } 39 | 40 | public void setChecked(boolean isChecked) { 41 | this.isChecked = isChecked; 42 | } 43 | 44 | public String getDescription() { 45 | return description; 46 | } 47 | 48 | public void setDescription(String description) { 49 | this.description = description; 50 | } 51 | 52 | public String getQuantity() { 53 | return quantity; 54 | } 55 | 56 | public void setQuantity(String quantity) { 57 | this.quantity = quantity; 58 | } 59 | 60 | static class ListItemWithID extends ListItem { 61 | private final int id; 62 | 63 | public ListItemWithID(int id, ListItem item) { 64 | super(item.isChecked, item.description, item.quantity); 65 | this.id = id; 66 | } 67 | 68 | public int getId() { 69 | return id; 70 | } 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/shoppinglist/ListsChangeListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.shoppinglist; 21 | 22 | /** 23 | * @author Wolfgang Popp 24 | */ 25 | public interface ListsChangeListener { 26 | void onListsChanged(); 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/shoppinglist/ShoppingList.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.shoppinglist; 21 | 22 | import android.os.Build; 23 | import android.support.annotation.NonNull; 24 | 25 | import java.util.ArrayList; 26 | import java.util.Arrays; 27 | import java.util.Collection; 28 | import java.util.Comparator; 29 | import java.util.HashSet; 30 | import java.util.Iterator; 31 | import java.util.LinkedList; 32 | import java.util.List; 33 | import java.util.ListIterator; 34 | import java.util.Set; 35 | import java.util.function.Predicate; 36 | import java.util.function.UnaryOperator; 37 | 38 | public class ShoppingList extends ArrayList { 39 | 40 | private String name; 41 | private static int currentID; 42 | private final List listeners = new LinkedList<>(); 43 | 44 | public ShoppingList(String name) { 45 | super(); 46 | this.name = name; 47 | } 48 | 49 | public ShoppingList(String name, Collection collection) { 50 | super(collection); 51 | this.name = name; 52 | } 53 | 54 | public String getName() { 55 | return name; 56 | } 57 | 58 | public void setName(String name) { 59 | this.name = name; 60 | notifyListChanged(Event.newOther()); 61 | } 62 | 63 | public int getId(int index) { 64 | return ((ListItem.ListItemWithID) get(index)).getId(); 65 | } 66 | 67 | @Override 68 | public boolean add(ListItem item) { 69 | boolean res = super.add(new ListItem.ListItemWithID(generateID(), item)); 70 | notifyListChanged(Event.newItemInserted(size() - 1)); 71 | return res; 72 | } 73 | 74 | public boolean add(String description, String quantity) { 75 | return add(new ListItem(false, description, quantity)); 76 | } 77 | 78 | @Override 79 | public void add(int index, ListItem element) { 80 | super.add(index, element); 81 | notifyListChanged(Event.newItemInserted(index)); 82 | } 83 | 84 | @Override 85 | public boolean addAll(Collection c) { 86 | boolean b = super.addAll(c); 87 | notifyListChanged(Event.newOther()); 88 | return b; 89 | } 90 | 91 | @Override 92 | public boolean addAll(int index, Collection c) { 93 | boolean b = super.addAll(index, c); 94 | notifyListChanged(Event.newOther()); 95 | return b; 96 | } 97 | 98 | @Override 99 | public ListItem set(int index, ListItem element) { 100 | ListItem old = super.set(index, element); 101 | notifyListChanged(Event.newItemChanged(index)); 102 | return old; 103 | } 104 | 105 | @Override 106 | public ListItem remove(int index) { 107 | ListItem res = super.remove(index); 108 | notifyListChanged(Event.newItemRemoved(index)); 109 | return res; 110 | } 111 | 112 | @Override 113 | public boolean remove(Object o) { 114 | boolean b = super.remove(o); 115 | if (b) { 116 | notifyListChanged(Event.newOther()); 117 | } 118 | return b; 119 | } 120 | 121 | @Override 122 | protected void removeRange(int fromIndex, int toIndex) { 123 | super.removeRange(fromIndex, toIndex); 124 | notifyListChanged(Event.newOther()); 125 | } 126 | 127 | @Override 128 | public boolean removeAll(Collection c) { 129 | boolean b = super.removeAll(c); 130 | if (b) { 131 | notifyListChanged(Event.newOther()); 132 | } 133 | return b; 134 | } 135 | 136 | @Override 137 | public boolean removeIf(Predicate filter) { 138 | boolean b = false; 139 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { 140 | b = super.removeIf(filter); 141 | } 142 | if (b) { 143 | notifyListChanged(Event.newOther()); 144 | } 145 | return b; 146 | } 147 | 148 | @Override 149 | public void replaceAll(UnaryOperator operator) { 150 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 151 | super.replaceAll(operator); 152 | notifyListChanged(Event.newOther()); 153 | } 154 | } 155 | 156 | @Override 157 | public boolean retainAll(Collection c) { 158 | boolean b = super.retainAll(c); 159 | if (b) { 160 | notifyListChanged(Event.newOther()); 161 | } 162 | return b; 163 | } 164 | 165 | @Override 166 | public void clear() { 167 | super.clear(); 168 | notifyListChanged(Event.newOther()); 169 | } 170 | 171 | @NonNull 172 | @Override 173 | public Iterator iterator() { 174 | return new Itr(super.iterator()); 175 | } 176 | 177 | @NonNull 178 | @Override 179 | public ListIterator listIterator(int index) { 180 | return new ListItr(super.listIterator(index)); 181 | } 182 | 183 | @NonNull 184 | @Override 185 | public ListIterator listIterator() { 186 | return new ListItr(super.listIterator()); 187 | } 188 | 189 | @Override 190 | public void sort(Comparator c) { 191 | ListItem[] items = toArray(new ListItem[size()]); 192 | Arrays.sort(items, c); 193 | super.clear(); 194 | super.addAll(Arrays.asList(items)); 195 | notifyListChanged(Event.newOther()); 196 | } 197 | 198 | public void setChecked(int index, boolean isChecked) { 199 | get(index).setChecked(isChecked); 200 | notifyListChanged(Event.newItemChanged(index)); 201 | } 202 | 203 | public void toggleChecked(int index) { 204 | setChecked(index, !get(index).isChecked()); 205 | } 206 | 207 | public void move(int oldIndex, int newIndex) { 208 | super.add(newIndex, super.remove(oldIndex)); 209 | notifyListChanged(Event.newItemMoved(oldIndex, newIndex)); 210 | } 211 | 212 | public void editItem(int index, String newDescription, String newQuantity) { 213 | ListItem listItem = get(index); 214 | listItem.setDescription(newDescription); 215 | listItem.setQuantity(newQuantity); 216 | notifyListChanged(Event.newItemChanged(index)); 217 | } 218 | 219 | 220 | public void removeAllCheckedItems() { 221 | Iterator it = iterator(); 222 | 223 | while (it.hasNext()) { 224 | ListItem item = it.next(); 225 | if (item.isChecked()) { 226 | it.remove(); 227 | } 228 | } 229 | notifyListChanged(Event.newOther()); 230 | } 231 | 232 | public Set createDescriptionIndex() { 233 | Set descriptionIndex = new HashSet<>(); 234 | for (ListItem listItem : this) { 235 | descriptionIndex.add(listItem.getDescription().toLowerCase()); 236 | } 237 | return descriptionIndex; 238 | } 239 | 240 | public void addListener(ShoppingListListener listener) { 241 | listeners.add(listener); 242 | } 243 | 244 | public void removeListener(ShoppingListListener listener) { 245 | listeners.remove(listener); 246 | } 247 | 248 | private synchronized int generateID() { 249 | return ++currentID; 250 | } 251 | 252 | private void notifyListChanged(ShoppingList.Event event) { 253 | for (ShoppingListListener listener : listeners) { 254 | listener.onShoppingListUpdate(this, event); 255 | } 256 | } 257 | 258 | public interface ShoppingListListener { 259 | void onShoppingListUpdate(ShoppingList list, ShoppingList.Event event); 260 | } 261 | 262 | public static class Event { 263 | public static final int ITEM_CHANGED = 0b1; 264 | public static final int ITEM_REMOVED = 0b10; 265 | public static final int ITEM_MOVED = 0b100; 266 | public static final int ITEM_INSERTED = 0b1000; 267 | public static final int OTHER = 0xffffffff; 268 | 269 | private final int state; 270 | private int index = -1; 271 | private int oldIndex = -1; 272 | private int newIndex = -1; 273 | 274 | public Event(int state) { 275 | this.state = state; 276 | } 277 | 278 | static Event newOther() { 279 | return new Event(OTHER); 280 | } 281 | 282 | static Event newItemMoved(int oldIndex, int newIndex) { 283 | Event e = new Event(ITEM_MOVED); 284 | e.oldIndex = oldIndex; 285 | e.newIndex = newIndex; 286 | return e; 287 | } 288 | 289 | static Event newItemChanged(int index) { 290 | Event e = new Event(ITEM_CHANGED); 291 | e.index = index; 292 | return e; 293 | } 294 | 295 | static Event newItemRemoved(int index) { 296 | Event e = new Event(ITEM_REMOVED); 297 | e.index = index; 298 | e.oldIndex = index; 299 | return e; 300 | } 301 | 302 | static Event newItemInserted(int index) { 303 | Event e = new Event(ITEM_INSERTED); 304 | e.index = index; 305 | e.newIndex = index; 306 | return e; 307 | } 308 | 309 | public int getState() { 310 | return state; 311 | } 312 | 313 | public int getIndex() { 314 | return index; 315 | } 316 | 317 | public int getOldIndex() { 318 | return oldIndex; 319 | } 320 | 321 | public int getNewIndex() { 322 | return newIndex; 323 | } 324 | 325 | } 326 | 327 | private class Itr implements Iterator { 328 | 329 | private Iterator iterator; 330 | 331 | private Itr(Iterator iterator) { 332 | this.iterator = iterator; 333 | } 334 | 335 | @Override 336 | public boolean hasNext() { 337 | return iterator.hasNext(); 338 | } 339 | 340 | @Override 341 | public ListItem next() { 342 | return iterator.next(); 343 | } 344 | 345 | @Override 346 | public void remove() { 347 | iterator.remove(); 348 | //TODO get index and use Event.newItemRemoved 349 | notifyListChanged(Event.newOther()); 350 | } 351 | } 352 | 353 | private class ListItr extends Itr implements ListIterator { 354 | 355 | private ListIterator iterator; 356 | 357 | private ListItr(ListIterator iterator) { 358 | super(iterator); 359 | this.iterator = iterator; 360 | } 361 | 362 | @Override 363 | public boolean hasPrevious() { 364 | return iterator.hasPrevious(); 365 | } 366 | 367 | @Override 368 | public ListItem previous() { 369 | return iterator.previous(); 370 | } 371 | 372 | @Override 373 | public int nextIndex() { 374 | return iterator.nextIndex(); 375 | } 376 | 377 | @Override 378 | public int previousIndex() { 379 | return iterator.previousIndex(); 380 | } 381 | 382 | @Override 383 | public void set(ListItem listItem) { 384 | iterator.set(listItem); 385 | //TODO get index and use Event.newItemChanged 386 | notifyListChanged(Event.newOther()); 387 | } 388 | 389 | @Override 390 | public void add(ListItem listItem) { 391 | iterator.add(listItem); 392 | //TODO get index and use Event.newItemInserted 393 | notifyListChanged(Event.newOther()); 394 | } 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/shoppinglist/ShoppingListException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.shoppinglist; 21 | 22 | public class ShoppingListException extends Exception { 23 | public ShoppingListException(String message) { 24 | super(message); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/shoppinglist/ShoppingListMarshaller.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.shoppinglist; 21 | 22 | import android.support.annotation.NonNull; 23 | 24 | import java.io.BufferedWriter; 25 | import java.io.IOException; 26 | import java.io.OutputStream; 27 | import java.io.OutputStreamWriter; 28 | 29 | public class ShoppingListMarshaller { 30 | public static void marshall(@NonNull OutputStream stream, @NonNull ShoppingList list) throws IOException { 31 | 32 | try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream))) { 33 | writer.write("[ "); 34 | writer.write(list.getName()); 35 | writer.write(" ]\n\n"); 36 | 37 | for (ListItem item : list) { 38 | String quantity = item.getQuantity(); 39 | String description = item.getDescription(); 40 | 41 | if (item.isChecked()) { 42 | writer.write("// "); 43 | } 44 | 45 | if (description != null) { 46 | writer.write(description); 47 | } 48 | 49 | if (quantity != null && !quantity.equals("")) { 50 | writer.write(" #"); 51 | writer.write(quantity); 52 | } 53 | 54 | writer.write("\n"); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/shoppinglist/ShoppingListService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.shoppinglist; 21 | 22 | import android.app.Service; 23 | import android.content.Intent; 24 | import android.content.SharedPreferences; 25 | import android.os.Binder; 26 | import android.os.IBinder; 27 | import android.preference.PreferenceManager; 28 | import android.support.annotation.Nullable; 29 | import android.util.Log; 30 | 31 | import java.util.Arrays; 32 | import java.util.Comparator; 33 | 34 | /** 35 | * @author Wolfgang Popp. 36 | */ 37 | public class ShoppingListService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener { 38 | private static final String TAG = ShoppingListService.class.getSimpleName(); 39 | 40 | 41 | private ShoppingListsManager manager = null; 42 | private final IBinder binder = new ShoppingListBinder(); 43 | private SharedPreferences sharedPreferences; 44 | private DirectoryStatus directoryStatus; 45 | 46 | private static final Comparator ignoreCaseComperator = new Comparator() { 47 | @Override 48 | public int compare(String o1, String o2) { 49 | return o1.compareToIgnoreCase(o2); 50 | } 51 | }; 52 | 53 | @Nullable 54 | @Override 55 | public IBinder onBind(Intent intent) { 56 | Log.v(TAG, "onBind() called: " + intent.toString()); 57 | sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 58 | sharedPreferences.registerOnSharedPreferenceChangeListener(this); 59 | 60 | manager = new ShoppingListsManager(); 61 | directoryStatus = new DirectoryStatus(this); 62 | manager.onStart(directoryStatus.getDirectory()); 63 | 64 | return binder; 65 | } 66 | 67 | @Override 68 | public boolean onUnbind(Intent intent) { 69 | Log.v(TAG, "onUnbind() called: " + intent.toString()); 70 | sharedPreferences.unregisterOnSharedPreferenceChangeListener(this); 71 | manager.onStop(); 72 | return false; 73 | } 74 | 75 | @Override 76 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 77 | manager.onStop(); 78 | directoryStatus = new DirectoryStatus(this); 79 | manager.onStart(directoryStatus.getDirectory()); 80 | } 81 | 82 | @Override 83 | public int onStartCommand(Intent intent, int flags, int startId) { 84 | Log.v(TAG, "onStartCommand() called"); 85 | return START_NOT_STICKY; 86 | } 87 | 88 | public class ShoppingListBinder extends Binder { 89 | 90 | public boolean usesFallbackDir() { 91 | return directoryStatus.isFallback(); 92 | } 93 | 94 | public void addList(String listName) throws ShoppingListException { 95 | manager.addList(listName); 96 | } 97 | 98 | public boolean removeList(String listName) { 99 | return manager.removeList(listName); 100 | } 101 | 102 | @Nullable 103 | public ShoppingList getList(String listName) { 104 | return manager.getList(listName); 105 | } 106 | 107 | public boolean hasList(String listName) { 108 | return manager.hasList(listName); 109 | } 110 | 111 | public String[] getListNames() { 112 | String[] names = manager.getListNames().toArray(new String[manager.size()]); 113 | Arrays.sort(names, ignoreCaseComperator); 114 | return names; 115 | } 116 | 117 | public int indexOf(String listName) { 118 | if (listName == null) { 119 | return -1; 120 | } 121 | return Arrays.binarySearch(getListNames(), listName, ignoreCaseComperator); 122 | } 123 | 124 | public int size() { 125 | return manager.size(); 126 | } 127 | 128 | public void addListChangeListener(ListsChangeListener listener) { 129 | manager.setListChangeListener(listener); 130 | } 131 | 132 | public void removeListChangeListener(ListsChangeListener listener) { 133 | manager.removeListChangeListenerListener(listener); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/shoppinglist/ShoppingListUnmarshaller.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.shoppinglist; 21 | 22 | import java.io.BufferedReader; 23 | import java.io.FileInputStream; 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | import java.io.InputStreamReader; 27 | import java.util.regex.Matcher; 28 | import java.util.regex.Pattern; 29 | 30 | public class ShoppingListUnmarshaller { 31 | private static final Pattern EMPTY_LINE = Pattern.compile("^\\s*$"); 32 | private static final Pattern HEADER = Pattern.compile("\\[(.*)]"); 33 | 34 | public static ShoppingList unmarshal(String filename) throws IOException, UnmarshallException { 35 | return unmarshal(new FileInputStream(filename)); 36 | } 37 | 38 | public static ShoppingList unmarshal(InputStream inputStream) throws IOException, UnmarshallException { 39 | ShoppingList shoppingList; 40 | String name = null; 41 | BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); 42 | String firstLine = reader.readLine(); 43 | 44 | if (firstLine != null) { 45 | Matcher matcher = HEADER.matcher(firstLine); 46 | if (matcher.matches()) { 47 | name = matcher.group(1).trim(); 48 | } 49 | } 50 | 51 | if (name == null) { 52 | throw new UnmarshallException("Could not find the name of the list"); 53 | } 54 | 55 | shoppingList = new ShoppingList(name); 56 | 57 | String line; 58 | while ((line = reader.readLine()) != null) { 59 | if (!EMPTY_LINE.matcher(line).matches()) { 60 | shoppingList.add(createListItem(line)); 61 | } 62 | } 63 | 64 | return shoppingList; 65 | } 66 | 67 | private static ListItem createListItem(String item) { 68 | boolean isChecked = item.startsWith("//"); 69 | int index; 70 | String quantity; 71 | String name; 72 | 73 | if (isChecked) { 74 | item = item.substring(2); 75 | } 76 | 77 | index = item.lastIndexOf("#"); 78 | 79 | if (index != -1) { 80 | quantity = item.substring(index + 1).trim(); 81 | name = item.substring(0, index).trim(); 82 | } else { 83 | quantity = ""; 84 | name = item.trim(); 85 | } 86 | 87 | return new ListItem(isChecked, name.trim(), quantity); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/shoppinglist/ShoppingListsManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.shoppinglist; 21 | 22 | import android.os.FileObserver; 23 | import android.os.SystemClock; 24 | import android.support.annotation.Nullable; 25 | import android.util.Log; 26 | 27 | import java.io.File; 28 | import java.io.FileOutputStream; 29 | import java.io.IOException; 30 | import java.io.OutputStream; 31 | import java.net.URLEncoder; 32 | import java.util.Collection; 33 | import java.util.HashMap; 34 | import java.util.LinkedList; 35 | import java.util.List; 36 | import java.util.Map; 37 | import java.util.Set; 38 | 39 | /** 40 | * @author Wolfgang Popp. 41 | */ 42 | class ShoppingListsManager { 43 | private static final String TAG = ShoppingListsManager.class.getSimpleName(); 44 | private static final String FILE_ENDING = ".lst"; 45 | 46 | private final Map trashcan = new HashMap<>(); 47 | private final MetadataContainer shoppingListsMetadata = new MetadataContainer(); 48 | private final List listeners = new LinkedList<>(); 49 | private FileObserver directoryObserver; 50 | private String directory; 51 | 52 | ShoppingListsManager() { 53 | } 54 | 55 | void setListChangeListener(ListsChangeListener listener) { 56 | this.listeners.add(listener); 57 | } 58 | 59 | void removeListChangeListenerListener(ListsChangeListener listener) { 60 | this.listeners.remove(listener); 61 | } 62 | 63 | void onStart(final String directory) { 64 | this.directory = directory; 65 | directoryObserver = new FileObserver(directory) { 66 | @Override 67 | public void onEvent(int event, @Nullable String path) { 68 | if (path == null) { 69 | return; 70 | } 71 | File file = new File(directory, path); 72 | switch (event) { 73 | case FileObserver.DELETE: 74 | shoppingListsMetadata.removeByFile(file.getPath()); 75 | break; 76 | case FileObserver.CREATE: 77 | // workaround: When CREATE is triggered, the file might still be empty. 78 | SystemClock.sleep(100); 79 | loadFromFile(file); 80 | break; 81 | } 82 | } 83 | }; 84 | 85 | Log.d(getClass().getSimpleName(), "Initializing from dir " + directory); 86 | maybeAddInitialList(); 87 | loadFromDirectory(directory); 88 | directoryObserver.startWatching(); 89 | } 90 | 91 | void onStop() { 92 | listeners.clear(); 93 | directoryObserver.stopWatching(); 94 | directoryObserver = null; 95 | 96 | for (ShoppingListMetadata metadata : trashcan.values()) { 97 | metadata.observer.stopWatching(); 98 | } 99 | for (ShoppingListMetadata metadata : shoppingListsMetadata.values()) { 100 | metadata.observer.stopWatching(); 101 | } 102 | 103 | try { 104 | writeAllUnsavedChanges(); 105 | } catch (IOException e) { 106 | Log.v(getClass().getSimpleName(), "Writing of changes failed", e); 107 | } 108 | 109 | shoppingListsMetadata.clear(); 110 | trashcan.clear(); 111 | } 112 | 113 | private void maybeAddInitialList() { 114 | boolean foundFile = false; 115 | 116 | for (File file : new File(directory).listFiles()) { 117 | foundFile = foundFile || file.isFile(); 118 | } 119 | 120 | if (!foundFile) { 121 | try { 122 | addList("Shopping List"); 123 | } catch (ShoppingListException e) { 124 | Log.e(getClass().getSimpleName(), "Failed to add initial list", e); 125 | } 126 | } 127 | } 128 | 129 | private void loadFromFile(File file) { 130 | try { 131 | final ShoppingList list = ShoppingListUnmarshaller.unmarshal(file.getPath()); 132 | addShoppingList(list, file.getPath()); 133 | Log.v(TAG, "Successfully loaded file: " + file); 134 | } catch (IOException | UnmarshallException e) { 135 | Log.v(getClass().getSimpleName(), "Ignoring file " + file); 136 | } 137 | } 138 | 139 | private void loadFromDirectory(String directory) { 140 | File d = new File(directory); 141 | for (File file : d.listFiles()) { 142 | if (file.isFile()) { 143 | loadFromFile(file); 144 | } 145 | } 146 | } 147 | 148 | private ShoppingListMetadata addShoppingList(ShoppingList list, String filename) { 149 | final ShoppingListMetadata metadata = new ShoppingListMetadata(list, filename); 150 | list.addListener(new ShoppingList.ShoppingListListener() { 151 | @Override 152 | public void onShoppingListUpdate(ShoppingList list, ShoppingList.Event e) { 153 | metadata.isDirty = true; 154 | } 155 | }); 156 | setupObserver(metadata); 157 | shoppingListsMetadata.add(metadata); 158 | return metadata; 159 | } 160 | 161 | private void setupObserver(final ShoppingListMetadata metadata) { 162 | FileObserver fileObserver = new FileObserver(metadata.filename) { 163 | @Override 164 | public void onEvent(int event, String path) { 165 | switch (event) { 166 | case FileObserver.CLOSE_WRITE: 167 | try { 168 | ShoppingList list = ShoppingListUnmarshaller.unmarshal(metadata.filename); 169 | metadata.shoppingList.clear(); 170 | metadata.shoppingList.addAll(list); 171 | metadata.isDirty = false; 172 | 173 | String oldName = metadata.shoppingList.getName(); 174 | rename(oldName, list.getName()); 175 | } catch (IOException | UnmarshallException e) { 176 | Log.e(TAG, "FileObserver could not read file.", e); 177 | } 178 | break; 179 | } 180 | } 181 | }; 182 | fileObserver.startWatching(); 183 | metadata.observer = fileObserver; 184 | } 185 | 186 | private void writeAllUnsavedChanges() throws IOException { 187 | // first empty trashcan and then write lists. This makes sure that a list that has been 188 | // removed and was later re-added is not actually deleted. 189 | for (ShoppingListMetadata metadata : trashcan.values()) { 190 | new File(metadata.filename).delete(); 191 | } 192 | 193 | for (ShoppingListMetadata metadata : shoppingListsMetadata.values()) { 194 | if (metadata.isDirty) { 195 | OutputStream os = new FileOutputStream(metadata.filename); 196 | ShoppingListMarshaller.marshall(os, metadata.shoppingList); 197 | Log.d(getClass().getSimpleName(), "Wrote file " + metadata.filename); 198 | } 199 | } 200 | } 201 | 202 | void addList(String name) throws ShoppingListException { 203 | if (hasList(name)) { 204 | throw new ShoppingListException("List already exists"); 205 | } 206 | 207 | String filename = new File(this.directory, URLEncoder.encode(name) + FILE_ENDING).getPath(); 208 | ShoppingListMetadata metadata = addShoppingList(new ShoppingList(name), filename); 209 | metadata.isDirty = true; 210 | } 211 | 212 | boolean removeList(String name) { 213 | if (hasList(name)) { 214 | ShoppingListMetadata toRemove = shoppingListsMetadata.removeByName(name); 215 | trashcan.put(toRemove.shoppingList.getName(), toRemove); 216 | return true; 217 | } 218 | return false; 219 | } 220 | 221 | @Nullable 222 | ShoppingList getList(String name) { 223 | ShoppingListMetadata metadata = shoppingListsMetadata.getByName(name); 224 | if (metadata != null) { 225 | return metadata.shoppingList; 226 | } 227 | return null; 228 | } 229 | 230 | Set getListNames() { 231 | return shoppingListsMetadata.getListNames(); 232 | } 233 | 234 | int size() { 235 | return shoppingListsMetadata.size(); 236 | } 237 | 238 | boolean hasList(String name) { 239 | return shoppingListsMetadata.hasName(name); 240 | } 241 | 242 | void rename(String oldName, String newName) { 243 | if (!oldName.equals(newName)) { 244 | ShoppingListMetadata metadata = shoppingListsMetadata.removeByName(oldName); 245 | metadata.shoppingList.setName(newName); 246 | shoppingListsMetadata.add(metadata); 247 | } 248 | } 249 | 250 | private class ShoppingListMetadata { 251 | private final ShoppingList shoppingList; 252 | private final String filename; 253 | private boolean isDirty; 254 | private FileObserver observer; 255 | 256 | private ShoppingListMetadata(ShoppingList shoppingList, String filename) { 257 | this.shoppingList = shoppingList; 258 | this.filename = filename; 259 | this.isDirty = false; 260 | } 261 | } 262 | 263 | private class MetadataContainer { 264 | private Map byName = new HashMap<>(); 265 | private Map filenameResolver = new HashMap<>(); 266 | 267 | private void add(ShoppingListMetadata metadata) { 268 | String name = metadata.shoppingList.getName(); 269 | byName.put(name, metadata); 270 | filenameResolver.put(metadata.filename, name); 271 | notifyListeners(); 272 | } 273 | 274 | private void clear() { 275 | filenameResolver.clear(); 276 | byName.clear(); 277 | notifyListeners(); 278 | } 279 | 280 | private ShoppingListMetadata removeByName(String name) { 281 | ShoppingListMetadata toRemove = byName.remove(name); 282 | filenameResolver.remove(toRemove.filename); 283 | notifyListeners(); 284 | return toRemove; 285 | } 286 | 287 | private ShoppingListMetadata removeByFile(String filename) { 288 | ShoppingListMetadata toRemove = byName.remove(filenameResolver.remove(filename)); 289 | notifyListeners(); 290 | return toRemove; 291 | } 292 | 293 | @Nullable 294 | private ShoppingListMetadata getByName(String name) { 295 | return byName.get(name); 296 | } 297 | 298 | private ShoppingListMetadata getByFile(String filename) { 299 | return getByName(filenameResolver.get(filename)); 300 | } 301 | 302 | private boolean hasName(String name) { 303 | return byName.containsKey(name); 304 | } 305 | 306 | private Collection values() { 307 | return byName.values(); 308 | } 309 | 310 | private Set getListNames() { 311 | return byName.keySet(); 312 | } 313 | 314 | private int size() { 315 | return byName.size(); 316 | } 317 | 318 | private void notifyListeners() { 319 | for (ListsChangeListener listener : listeners) { 320 | listener.onListsChanged(); 321 | } 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /app/src/main/java/com/woefe/shoppinglist/shoppinglist/UnmarshallException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * ShoppingList - A simple shopping list for Android 3 | * 4 | * Copyright (C) 2018. Wolfgang Popp 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package com.woefe.shoppinglist.shoppinglist; 21 | 22 | /** 23 | * @author Wolfgang Popp 24 | */ 25 | 26 | class UnmarshallException extends Exception { 27 | public UnmarshallException(String s) { 28 | super(s); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi/ic_done_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_add_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-hdpi/ic_add_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_delete_forever_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-hdpi/ic_delete_forever_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_delete_sweep_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-hdpi/ic_delete_sweep_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_done_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-hdpi/ic_done_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_menu_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-hdpi/ic_menu_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_playlist_add_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-hdpi/ic_playlist_add_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_sd_storage_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-hdpi/ic_sd_storage_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_sort_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-hdpi/ic_sort_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_swap_vert_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-hdpi/ic_swap_vert_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_add_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-mdpi/ic_add_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_delete_forever_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-mdpi/ic_delete_forever_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_delete_sweep_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-mdpi/ic_delete_sweep_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_done_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-mdpi/ic_done_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_menu_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-mdpi/ic_menu_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_playlist_add_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-mdpi/ic_playlist_add_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_sd_storage_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-mdpi/ic_sd_storage_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_sort_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-mdpi/ic_sort_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_swap_vert_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-mdpi/ic_swap_vert_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_add_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xhdpi/ic_add_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_delete_forever_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xhdpi/ic_delete_forever_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_delete_sweep_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xhdpi/ic_delete_sweep_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_done_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xhdpi/ic_done_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_menu_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xhdpi/ic_menu_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_playlist_add_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xhdpi/ic_playlist_add_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_sd_storage_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xhdpi/ic_sd_storage_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_sort_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xhdpi/ic_sort_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_swap_vert_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xhdpi/ic_swap_vert_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_add_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxhdpi/ic_add_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_delete_forever_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxhdpi/ic_delete_forever_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_delete_sweep_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxhdpi/ic_delete_sweep_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_done_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxhdpi/ic_done_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_menu_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxhdpi/ic_menu_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_playlist_add_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxhdpi/ic_playlist_add_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_sd_storage_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxhdpi/ic_sd_storage_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_sort_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxhdpi/ic_sort_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_swap_vert_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxhdpi/ic_swap_vert_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_add_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxxhdpi/ic_add_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_delete_forever_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxxhdpi/ic_delete_forever_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_delete_sweep_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxxhdpi/ic_delete_sweep_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_done_black_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxxhdpi/ic_done_black_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_menu_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxxhdpi/ic_menu_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_playlist_add_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxxhdpi/ic_playlist_add_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_sd_storage_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxxhdpi/ic_sd_storage_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_sort_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxxhdpi/ic_sort_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_swap_vert_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woefe/ShoppingList/c4da457bb1d78aa14be5b5d56404f81b0b244861/app/src/main/res/drawable-xxxhdpi/ic_swap_vert_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_add_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/list_activated_background.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_about.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 24 | 25 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 24 | 25 | 26 | 28 | 32 | 33 | 34 | 35 | 39 | 40 | 41 | 47 | 48 | 54 | 55 | 67 | 68 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_directory_chooser.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 25 | 26 | 32 | 33 | 40 | 41 | 45 | 46 | 52 | 53 | 64 | 65 | 72 | 73 |