diff --git a/README.md b/README.md index b2abd1c9..263b3c06 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,8 @@ So make sure you have a **current backup** before switching! * [Carlos Melero](https://github.com/carmebar) ([view contributions](https://github.com/flocke/andOTP/commits/master?author=carmebar)) * [SuperVirus](https://github.com/SuperVirus) ([view contributions](https://github.com/flocke/andOTP/commits/master?author=SuperVirus)) + * [RichyHBM](https://github.com/RichyHBM) ([view contributions](https://github.com/flocke/andOTP/commits/master?author=RichyHBM)) + #### Translators: diff --git a/app/src/androidTest/java/org/shadowice/flocke/andotp/ApplicationTest.java b/app/src/androidTest/java/org/shadowice/flocke/andotp/ApplicationTest.java index b730a6cf..a63777ab 100644 --- a/app/src/androidTest/java/org/shadowice/flocke/andotp/ApplicationTest.java +++ b/app/src/androidTest/java/org/shadowice/flocke/andotp/ApplicationTest.java @@ -105,12 +105,17 @@ public class ApplicationTest extends ApplicationTestCase { "\"period\":" + Integer.toString(period) + "," + "\"digits\":6," + "\"type\":\"TOTP\"," + - "\"algorithm\":\"SHA1\"}"; + "\"algorithm\":\"SHA1\"," + + "\"tags\":[\"test1\",\"test2\"]}"; Entry e = new Entry(new JSONObject(s)); assertTrue(Arrays.equals(secret, e.getSecret())); assertEquals(label, e.getLabel()); + String[] tags = new String[]{"test1", "test2"}; + assertEquals(tags.length, e.getTags().size()); + assertTrue(Arrays.equals(tags, e.getTags().toArray(new String[e.getTags().size()]))); + assertEquals(s, e.toJSON().toString()); } @@ -147,6 +152,15 @@ public class ApplicationTest extends ApplicationTestCase { assertEquals("ACME Co - ACME Co:john.doe@email.com", entry.getLabel()); assertEquals("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", new String(new Base32().encode(entry.getSecret()))); + + + entry = new Entry("otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&ALGORITHM=SHA1&digits=6&period=30&tags=test1&tags=test2"); + assertEquals("ACME Co - ACME Co:john.doe@email.com", entry.getLabel()); + + assertEquals("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", new String(new Base32().encode(entry.getSecret()))); + String[] tags = new String[]{"test1", "test2"}; + assertEquals(tags.length, entry.getTags().size()); + assertTrue(Arrays.equals(tags, entry.getTags().toArray(new String[entry.getTags().size()]))); } public void testSettingsHelper() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java b/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java index 617b349f..f2de988f 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Activities/MainActivity.java @@ -29,10 +29,13 @@ import android.app.KeyguardManager; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.content.res.Configuration; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.support.constraint.ConstraintLayout; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.SearchView; @@ -46,7 +49,10 @@ import android.view.WindowManager; import android.view.animation.LinearInterpolator; import android.widget.AdapterView; import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.CheckedTextView; import android.widget.EditText; +import android.widget.ListView; import android.widget.ProgressBar; import android.widget.Spinner; import android.widget.Toast; @@ -54,15 +60,22 @@ import android.widget.Toast; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; -import org.shadowice.flocke.andotp.Utilities.Settings; -import org.shadowice.flocke.andotp.View.EntriesCardAdapter; import org.shadowice.flocke.andotp.Database.Entry; +import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.Utilities.DatabaseHelper; +import org.shadowice.flocke.andotp.Utilities.Settings; +import org.shadowice.flocke.andotp.Utilities.TagDialogHelper; +import org.shadowice.flocke.andotp.Utilities.TokenCalculator; +import org.shadowice.flocke.andotp.View.EntriesCardAdapter; import org.shadowice.flocke.andotp.View.FloatingActionMenu; import org.shadowice.flocke.andotp.View.ItemTouchHelper.SimpleItemTouchHelperCallback; -import org.shadowice.flocke.andotp.R; -import org.shadowice.flocke.andotp.Utilities.TokenCalculator; +import org.shadowice.flocke.andotp.View.TagsAdapter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Locale; +import java.util.concurrent.Callable; import static org.shadowice.flocke.andotp.Utilities.Settings.SortMode; @@ -83,6 +96,10 @@ public class MainActivity extends BaseActivity private Handler handler; private Runnable handlerTask; + private ListView tagsDrawerListView; + private TagsAdapter tagsDrawerAdapter; + private ActionBarDrawerToggle tagsToggle; + // QR code scanning private void scanQRCode(){ new IntentIntegrator(MainActivity.this) @@ -102,6 +119,7 @@ public class MainActivity extends BaseActivity final EditText periodInput = inputView.findViewById(R.id.manual_period); final EditText digitsInput = inputView.findViewById(R.id.manual_digits); final Spinner algorithmInput = inputView.findViewById(R.id.manual_algorithm); + final Button tagsInput = inputView.findViewById(R.id.manual_tags); final ArrayAdapter algorithmAdapter = new ArrayAdapter<>(this, android.R.layout.simple_expandable_list_item_1, TokenCalculator.HashAlgorithm.values()); final ArrayAdapter typeAdapter = new ArrayAdapter<>(this, android.R.layout.simple_expandable_list_item_1, Entry.PublicTypes.toArray(new Entry.OTPType[Entry.PublicTypes.size()])); @@ -144,6 +162,36 @@ public class MainActivity extends BaseActivity } }); + List allTags = adapter.getTags(); + HashMap tagsHashMap = new HashMap<>(); + for(String tag: allTags) { + tagsHashMap.put(tag, false); + } + final TagsAdapter tagsAdapter = new TagsAdapter(this, tagsHashMap); + + final Callable tagsCallable = new Callable() { + @Override + public Object call() throws Exception { + List selectedTags = tagsAdapter.getActiveTags(); + StringBuilder stringBuilder = new StringBuilder(); + for(int j = 0; j < selectedTags.size(); j++) { + stringBuilder.append(selectedTags.get(j)); + if(j < selectedTags.size() - 1) { + stringBuilder.append(", "); + } + } + tagsInput.setText(stringBuilder.toString()); + return null; + } + }; + + tagsInput.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + TagDialogHelper.createTagsDialog(MainActivity.this, tagsAdapter, tagsCallable, tagsCallable); + } + }); + AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.dialog_title_manual_entry) .setView(inputView) @@ -159,10 +207,12 @@ public class MainActivity extends BaseActivity int period = Integer.parseInt(periodInput.getText().toString()); int digits = Integer.parseInt(digitsInput.getText().toString()); - Entry e = new Entry(type, secret, period, digits, label, algorithm); + Entry e = new Entry(type, secret, period, digits, label, algorithm, tagsAdapter.getActiveTags()); e.updateOTP(); adapter.addEntry(e); adapter.saveEntries(); + + refreshTags(); } } }) @@ -273,7 +323,14 @@ public class MainActivity extends BaseActivity llm.setOrientation(LinearLayoutManager.VERTICAL); recList.setLayoutManager(llm); - adapter = new EntriesCardAdapter(this); + HashMap tagsHashMap = new HashMap<>(); + for(Entry entry : DatabaseHelper.loadDatabase(this)) { + for(String tag : entry.getTags()) + tagsHashMap.put(tag, settings.getTagToggle(tag)); + } + tagsDrawerAdapter = new TagsAdapter(this, tagsHashMap); + + adapter = new EntriesCardAdapter(this, tagsDrawerAdapter); recList.setAdapter(adapter); recList.addOnScrollListener(new RecyclerView.OnScrollListener() { @@ -332,6 +389,14 @@ public class MainActivity extends BaseActivity handler.postDelayed(this, 1000); } }; + + setupDrawer(); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + tagsToggle.syncState(); } // Controls for the updater background task @@ -372,6 +437,12 @@ public class MainActivity extends BaseActivity } } + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + tagsToggle.onConfigurationChanged(newConfig); + } + // Activity results @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { @@ -385,13 +456,16 @@ public class MainActivity extends BaseActivity e.updateOTP(); adapter.addEntry(e); adapter.saveEntries(); + refreshTags(); } catch (Exception e) { Toast.makeText(this, R.string.toast_invalid_qr_code, Toast.LENGTH_LONG).show(); } } } else if (requestCode == INTENT_INTERNAL_BACKUP && resultCode == RESULT_OK) { - if (intent.getBooleanExtra("reload", false)) + if (intent.getBooleanExtra("reload", false)) { adapter.loadEntries(); + refreshTags(); + } } else if (requestCode == INTENT_INTERNAL_AUTHENTICATE) { if (resultCode != RESULT_OK) { Toast.makeText(getBaseContext(), R.string.toast_auth_failed, Toast.LENGTH_LONG).show(); @@ -495,8 +569,119 @@ public class MainActivity extends BaseActivity adapter.setSortMode(SortMode.LABEL); touchHelperCallback.setDragEnabled(false); } + } else if (tagsToggle.onOptionsItemSelected(item)) { + return true; } return super.onOptionsItemSelected(item); } + + private void setupDrawer() { + tagsDrawerListView = (ListView)findViewById(R.id.tags_list_in_drawer); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(true); + + final DrawerLayout tagsDrawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout); + + tagsToggle = new ActionBarDrawerToggle(this, tagsDrawerLayout, R.string.drawer_open, R.string.drawer_close) { + @Override + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + getSupportActionBar().setTitle(R.string.label_tags); + invalidateOptionsMenu(); + } + + @Override + public void onDrawerClosed(View view) { + super.onDrawerClosed(view); + getSupportActionBar().setTitle(R.string.app_name); + invalidateOptionsMenu(); + } + }; + + tagsToggle.setDrawerIndicatorEnabled(true); + tagsDrawerLayout.addDrawerListener(tagsToggle); + + final CheckedTextView noTagsButton = (CheckedTextView)findViewById(R.id.no_tags_entries); + final CheckedTextView allTagsButton = (CheckedTextView)findViewById(R.id.all_tags_in_drawer); + + allTagsButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + CheckedTextView checkedTextView = ((CheckedTextView)view); + checkedTextView.setChecked(!checkedTextView.isChecked()); + + settings.setAllTagsToggle(checkedTextView.isChecked()); + + for(int i = 0; i < tagsDrawerListView.getChildCount(); i++) { + CheckedTextView childCheckBox = (CheckedTextView)tagsDrawerListView.getChildAt(i); + childCheckBox.setChecked(checkedTextView.isChecked()); + tagsDrawerAdapter.setTagState(childCheckBox.getText().toString(), childCheckBox.isChecked()); + settings.setTagToggle(childCheckBox.getText().toString(), childCheckBox.isChecked()); + } + + if(checkedTextView.isChecked()) { + adapter.filterByTags(tagsDrawerAdapter.getActiveTags()); + } else { + adapter.filterByTags(new ArrayList()); + } + } + }); + allTagsButton.setChecked(settings.getAllTagsToggle()); + + noTagsButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + CheckedTextView checkedTextView = ((CheckedTextView)view); + checkedTextView.setChecked(!checkedTextView.isChecked()); + + settings.setNoTagsToggle(checkedTextView.isChecked()); + adapter.filterByTags(tagsDrawerAdapter.getActiveTags()); + } + }); + noTagsButton.setChecked(settings.getNoTagsToggle()); + + tagsDrawerListView.setAdapter(tagsDrawerAdapter); + tagsDrawerListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + CheckedTextView checkedTextView = ((CheckedTextView)view); + checkedTextView.setChecked(!checkedTextView.isChecked()); + + settings.setTagToggle(checkedTextView.getText().toString(), checkedTextView.isChecked()); + tagsDrawerAdapter.setTagState(checkedTextView.getText().toString(), checkedTextView.isChecked()); + + if (! checkedTextView.isChecked()) { + allTagsButton.setChecked(false); + settings.setAllTagsToggle(false); + } + + if (tagsDrawerAdapter.allTagsActive()) { + allTagsButton.setChecked(true); + settings.setAllTagsToggle(true); + } + + adapter.filterByTags(tagsDrawerAdapter.getActiveTags()); + } + }); + + adapter.filterByTags(tagsDrawerAdapter.getActiveTags()); + } + + void refreshTags() { + HashMap tagsHashMap = new HashMap<>(); + for(String tag: tagsDrawerAdapter.getTags()) { + tagsHashMap.put(tag, false); + } + for(String tag: tagsDrawerAdapter.getActiveTags()) { + tagsHashMap.put(tag, true); + } + for(String tag: adapter.getTags()) { + if(!tagsHashMap.containsKey(tag)) + tagsHashMap.put(tag, true); + } + tagsDrawerAdapter.setTags(tagsHashMap); + adapter.filterByTags(tagsDrawerAdapter.getActiveTags()); + } } \ No newline at end of file diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Database/Entry.java b/app/src/main/java/org/shadowice/flocke/andotp/Database/Entry.java index 7afe5445..b1cda863 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Database/Entry.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Database/Entry.java @@ -26,13 +26,16 @@ package org.shadowice.flocke.andotp.Database; import android.net.Uri; import org.apache.commons.codec.binary.Base32; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.shadowice.flocke.andotp.Utilities.TokenCalculator; import java.net.URL; +import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; +import java.util.List; import java.util.Objects; import java.util.Set; @@ -48,6 +51,7 @@ public class Entry { private static final String JSON_DIGITS = "digits"; private static final String JSON_TYPE = "type"; private static final String JSON_ALGORITHM = "algorithm"; + private static final String JSON_TAGS = "tags"; private OTPType type = OTPType.TOTP; private int period = TokenCalculator.TOTP_DEFAULT_PERIOD; @@ -57,16 +61,18 @@ public class Entry { private String label; private String currentOTP; private long last_update = 0; + public List tags = new ArrayList<>(); public Entry(){} - public Entry(OTPType type, String secret, int period, int digits, String label, TokenCalculator.HashAlgorithm algorithm) { + public Entry(OTPType type, String secret, int period, int digits, String label, TokenCalculator.HashAlgorithm algorithm, List tags) { this.type = type; this.secret = new Base32().decode(secret.toUpperCase()); this.period = period; this.digits = digits; this.label = label; this.algorithm = algorithm; + this.tags = tags; } public Entry(String contents) throws Exception { @@ -91,6 +97,7 @@ public class Entry { String period = uri.getQueryParameter("period"); String digits = uri.getQueryParameter("digits"); String algorithm = uri.getQueryParameter("algorithm"); + List tags = uri.getQueryParameters("tags"); if(issuer != null){ label = issuer +" - "+label; @@ -116,6 +123,12 @@ public class Entry { } else { this.algorithm = TokenCalculator.DEFAULT_ALGORITHM; } + + if(tags != null) { + this.tags = tags; + } else { + this.tags = new ArrayList<>(); + } } public Entry (JSONObject jsonObj) throws JSONException { @@ -140,6 +153,16 @@ public class Entry { } catch (JSONException e) { this.algorithm = TokenCalculator.DEFAULT_ALGORITHM; } + + this.tags = new ArrayList<>(); + try { + JSONArray tagsArray = jsonObj.getJSONArray(JSON_TAGS); + for(int i = 0; i < tagsArray.length(); i++) { + this.tags.add(tagsArray.getString(i)); + } + } catch (JSONException e) { + e.printStackTrace(); + } } public JSONObject toJSON() throws JSONException { @@ -151,6 +174,12 @@ public class Entry { jsonObj.put(JSON_TYPE, getType().toString()); jsonObj.put(JSON_ALGORITHM, algorithm.toString()); + JSONArray tagsArray = new JSONArray(); + for(String tag : tags){ + tagsArray.put(tag); + } + jsonObj.put(JSON_TAGS, tagsArray); + return jsonObj; } @@ -194,6 +223,10 @@ public class Entry { this.digits = digits; } + public List getTags() { return tags; } + + public void setTags(List tags) { this.tags = tags; } + public TokenCalculator.HashAlgorithm getAlgorithm() { return this.algorithm; } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java index b9a9a4e6..77e3d4bf 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Settings.java @@ -36,6 +36,7 @@ import java.io.File; import java.nio.charset.StandardCharsets; import java.security.KeyPair; import java.util.Collections; +import java.util.HashSet; import java.util.Locale; import java.util.Set; @@ -125,6 +126,10 @@ public class Settings { return settings.getLong(getResString(keyId), defaultValue); } + private Set getStringSet(int keyId, Set defaultValue) { + return new HashSet(settings.getStringSet(getResString(keyId), defaultValue)); + } + private void setBoolean(int keyId, boolean value) { settings.edit() .putBoolean(getResString(keyId), value) @@ -137,6 +142,12 @@ public class Settings { .apply(); } + private void setStringSet(int keyId, Set value) { + settings.edit() + .putStringSet(getResString(keyId), value) + .apply(); + } + private void remove(int keyId) { settings.edit() .remove(getResString(keyId)) @@ -288,4 +299,35 @@ public class Settings { public boolean getOpenPGPVerify() { return getBoolean(R.string.settings_key_openpgp_verify, false); } + + public boolean getAllTagsToggle() { + return getBoolean(R.string.settings_key_all_tags_toggle, true); + } + + public void setAllTagsToggle(Boolean value) { + setBoolean(R.string.settings_key_all_tags_toggle, value); + } + + public boolean getNoTagsToggle() { + return getBoolean(R.string.settings_key_no_tags_toggle, true); + } + + public void setNoTagsToggle(Boolean value) { + setBoolean(R.string.settings_key_no_tags_toggle, value); + } + + public boolean getTagToggle(String tag) { + //The tag toggle holds tags that are unchecked in order to default to checked. + Set toggledTags = getStringSet(R.string.settings_key_tags_toggles, new HashSet()); + return !toggledTags.contains(tag); + } + + public void setTagToggle(String tag, Boolean value) { + Set toggledTags = getStringSet(R.string.settings_key_tags_toggles, new HashSet()); + if(value) + toggledTags.remove(tag); + else + toggledTags.add(tag); + setStringSet(R.string.settings_key_tags_toggles, toggledTags); + } } diff --git a/app/src/main/java/org/shadowice/flocke/andotp/Utilities/TagDialogHelper.java b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/TagDialogHelper.java new file mode 100644 index 00000000..274633be --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/Utilities/TagDialogHelper.java @@ -0,0 +1,87 @@ +package org.shadowice.flocke.andotp.Utilities; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.view.View; +import android.widget.AdapterView; +import android.widget.CheckedTextView; +import android.widget.EditText; +import android.widget.ListView; + +import org.shadowice.flocke.andotp.R; +import org.shadowice.flocke.andotp.View.TagsAdapter; + +import java.util.HashMap; +import java.util.concurrent.Callable; + +public class TagDialogHelper { + public static void createTagsDialog(Context context, final TagsAdapter tagsAdapter, final Callable newTagCallable, final Callable selectedTagsCallable) { + final EditText input = new EditText(context); + + final AlertDialog.Builder newTagBuilder = new AlertDialog.Builder(context); + newTagBuilder.setTitle(R.string.button_new_tag) + .setView(input) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + String newTag = input.getText().toString(); + HashMap allTags = tagsAdapter.getTagsWithState(); + allTags.put(newTag, true); + tagsAdapter.setTags(allTags); + if(newTagCallable != null) { + try { + newTagCallable.call(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + }) + .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) {} + }); + + final ListView tagsSelectionView = new ListView(context); + tagsSelectionView.setAdapter(tagsAdapter); + tagsSelectionView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + CheckedTextView checkedTextView = ((CheckedTextView)view); + checkedTextView.setChecked(!checkedTextView.isChecked()); + + tagsAdapter.setTagState(checkedTextView.getText().toString(), checkedTextView.isChecked()); + } + }); + + final AlertDialog.Builder tagsSelectorBuilder = new AlertDialog.Builder(context); + tagsSelectorBuilder.setTitle(R.string.label_tags) + .setView(tagsSelectionView) + .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + dialogInterface.dismiss(); + } + }) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + if(selectedTagsCallable != null) { + try { + selectedTagsCallable.call(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + }) + .setNeutralButton(R.string.button_new_tag, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + newTagBuilder.create().show(); + } + }).create().show(); + + } +} diff --git a/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java b/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java index 7e3859a3..c01b89ad 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/View/EntriesCardAdapter.java @@ -44,6 +44,8 @@ import android.widget.Toast; import org.shadowice.flocke.andotp.Database.Entry; import org.shadowice.flocke.andotp.Utilities.DatabaseHelper; +import org.shadowice.flocke.andotp.Utilities.Settings; +import org.shadowice.flocke.andotp.Utilities.TagDialogHelper; import org.shadowice.flocke.andotp.View.ItemTouchHelper.ItemTouchHelperAdapter; import org.shadowice.flocke.andotp.R; @@ -52,6 +54,10 @@ import static org.shadowice.flocke.andotp.Utilities.Settings.SortMode; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.Callable; public class EntriesCardAdapter extends RecyclerView.Adapter implements ItemTouchHelperAdapter, Filterable { @@ -61,13 +67,17 @@ public class EntriesCardAdapter extends RecyclerView.Adapter private ArrayList entries; private ArrayList displayedEntries; private Callback callback; + private List tagsFilter = new ArrayList<>(); private SortMode sortMode = SortMode.UNSORTED; + private TagsAdapter tagsFilterAdapter; + private Settings settings; - public EntriesCardAdapter(Context context) { + public EntriesCardAdapter(Context context, TagsAdapter tagsFilterAdapter) { this.context = context; this.sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); - + this.tagsFilterAdapter = tagsFilterAdapter; + this.settings = new Settings(context); loadEntries(); } @@ -91,6 +101,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter public void entriesChanged() { displayedEntries = sortEntries(entries); + filterByTags(tagsFilter); notifyDataSetChanged(); } @@ -103,6 +114,29 @@ public class EntriesCardAdapter extends RecyclerView.Adapter entriesChanged(); } + public void filterByTags(List tags) { + tagsFilter = tags; + List matchingEntries = new ArrayList<>(); + + for(Entry e : entries) { + //Entries with no tags will always be shown + Boolean foundMatchingTag = e.getTags().isEmpty() && settings.getNoTagsToggle(); + + for(String tag : tags) { + if(e.getTags().contains(tag)) { + foundMatchingTag = true; + } + } + + if(foundMatchingTag) { + matchingEntries.add(e); + } + } + + displayedEntries = sortEntries(matchingEntries); + notifyDataSetChanged(); + } + public void updateTokens() { boolean change = false; @@ -119,7 +153,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter public void onBindViewHolder(EntryViewHolder entryViewHolder, int i) { Entry entry = displayedEntries.get(i); - entryViewHolder.updateValues(entry.getLabel(), entry.getCurrentOTP()); + entryViewHolder.updateValues(entry.getLabel(), entry.getCurrentOTP(), entry.getTags()); if (entry.hasNonDefaultPeriod()) { entryViewHolder.showCustomPeriod(entry.getPeriod()); @@ -227,6 +261,52 @@ public class EntriesCardAdapter extends RecyclerView.Adapter .show(); } + public void editEntryTags(final int pos) { + final int realPos = getRealIndex(pos); + final Entry entry = entries.get(realPos); + + HashMap tagsHashMap = new HashMap<>(); + for(String tag: entry.getTags()) { + tagsHashMap.put(tag, true); + } + for(String tag: getTags()) { + if(!tagsHashMap.containsKey(tag)) + tagsHashMap.put(tag, false); + } + final TagsAdapter tagsAdapter = new TagsAdapter(context, tagsHashMap); + + final Callable tagsCallable = new Callable() { + @Override + public Object call() throws Exception { + entries.get(realPos).setTags(tagsAdapter.getActiveTags()); + DatabaseHelper.saveDatabase(context, entries); + + List inUseTags = getTags(); + + HashMap tagsHashMap = new HashMap<>(); + for(String tag: tagsFilterAdapter.getTags()) { + if(inUseTags.contains(tag)) + tagsHashMap.put(tag, false); + } + for(String tag: tagsFilterAdapter.getActiveTags()) { + if(inUseTags.contains(tag)) + tagsHashMap.put(tag, true); + } + for(String tag: getTags()) { + if(inUseTags.contains(tag)) + if(!tagsHashMap.containsKey(tag)) + tagsHashMap.put(tag, true); + } + + tagsFilterAdapter.setTags(tagsHashMap); + filterByTags(tagsFilterAdapter.getActiveTags()); + return null; + } + }; + + TagDialogHelper.createTagsDialog(context, tagsAdapter, tagsCallable, tagsCallable); + } + public void removeItem(final int pos) { AlertDialog.Builder builder = new AlertDialog.Builder(context); @@ -265,6 +345,9 @@ public class EntriesCardAdapter extends RecyclerView.Adapter if (id == R.id.menu_popup_editLabel) { editEntryLabel(pos); return true; + } else if (id == R.id.menu_popup_editTags) { + editEntryTags(pos); + return true; } else if (id == R.id.menu_popup_remove) { removeItem(pos); return true; @@ -293,7 +376,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter return this.sortMode; } - private ArrayList sortEntries(ArrayList unsorted) { + private ArrayList sortEntries(List unsorted) { ArrayList sorted = new ArrayList<>(unsorted); if (sortMode == SortMode.LABEL) { @@ -314,6 +397,16 @@ public class EntriesCardAdapter extends RecyclerView.Adapter return filter; } + public List getTags() { + HashSet tags = new HashSet(); + + for(Entry entry : entries) { + tags.addAll(entry.getTags()); + } + + return new ArrayList(tags); + } + public class EntryFilter extends Filter { @Override protected FilterResults performFiltering(CharSequence constraint) { diff --git a/app/src/main/java/org/shadowice/flocke/andotp/View/EntryViewHolder.java b/app/src/main/java/org/shadowice/flocke/andotp/View/EntryViewHolder.java index e7b57b93..9f9f4370 100644 --- a/app/src/main/java/org/shadowice/flocke/andotp/View/EntryViewHolder.java +++ b/app/src/main/java/org/shadowice/flocke/andotp/View/EntryViewHolder.java @@ -38,6 +38,8 @@ import org.shadowice.flocke.andotp.Utilities.Tools; import org.shadowice.flocke.andotp.View.ItemTouchHelper.ItemTouchHelperViewHolder; import org.shadowice.flocke.andotp.R; +import java.util.List; + public class EntryViewHolder extends RecyclerView.ViewHolder implements ItemTouchHelperViewHolder { @@ -52,6 +54,7 @@ public class EntryViewHolder extends RecyclerView.ViewHolder private ImageView visibleImg; private TextView value; private TextView label; + private TextView tags; private TextView customPeriod; @@ -66,6 +69,7 @@ public class EntryViewHolder extends RecyclerView.ViewHolder visibleImg = v.findViewById(R.id.valueImg); coverLayout = v.findViewById(R.id.coverLayout); label = v.findViewById(R.id.textViewLabel); + tags = v.findViewById(R.id.textViewTags); customPeriodLayout = v.findViewById(R.id.customPeriodLayout); customPeriod = v.findViewById(R.id.customPeriod); @@ -99,9 +103,24 @@ public class EntryViewHolder extends RecyclerView.ViewHolder }); } - public void updateValues(String label, String token) { + public void updateValues(String label, String token, List tags) { this.label.setText(label); value.setText(token); + + StringBuilder stringBuilder = new StringBuilder(); + for(int i = 0; i < tags.size(); i++) { + stringBuilder.append(tags.get(i)); + if(i < tags.size() - 1) { + stringBuilder.append(", "); + } + } + this.tags.setText(stringBuilder.toString()); + + if (! tags.isEmpty()) { + this.tags.setVisibility(View.VISIBLE); + } else { + this.tags.setVisibility(View.GONE); + } } public void showCustomPeriod(int period) { @@ -115,6 +134,7 @@ public class EntryViewHolder extends RecyclerView.ViewHolder public void setLabelSize(int size) { label.setTextSize(TypedValue.COMPLEX_UNIT_PT, size); + tags.setTextSize(TypedValue.COMPLEX_UNIT_PT, size - 2); } public void setLabelScroll(boolean active) { diff --git a/app/src/main/java/org/shadowice/flocke/andotp/View/TagsAdapter.java b/app/src/main/java/org/shadowice/flocke/andotp/View/TagsAdapter.java new file mode 100644 index 00000000..5529bf84 --- /dev/null +++ b/app/src/main/java/org/shadowice/flocke/andotp/View/TagsAdapter.java @@ -0,0 +1,93 @@ +package org.shadowice.flocke.andotp.View; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckedTextView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class TagsAdapter extends ArrayAdapter { + private Context context; + private List tagsOrder; + private HashMap tagsState; + private static final int layoutResourceId = android.R.layout.simple_list_item_multiple_choice; + + public TagsAdapter(Context context, HashMap tags) { + super(context, layoutResourceId, new ArrayList<>(tags.keySet())); + this.context = context; + + this.tagsState = tags; + this.tagsOrder = new ArrayList<>(tagsState.keySet()); + Collections.sort(this.tagsOrder); + } + + @NonNull + @Override + public View getView(int i, View view, @NonNull ViewGroup viewGroup) { + CheckedTextView checkedTextView; + if (view == null) { + checkedTextView = (CheckedTextView)LayoutInflater.from(context).inflate(layoutResourceId, viewGroup, false); + } else{ + checkedTextView = (CheckedTextView) view; + } + checkedTextView.setText(tagsOrder.get(i)); + checkedTextView.setChecked(tagsState.get(tagsOrder.get(i))); + + return checkedTextView; + } + + public List getTags() { + return tagsOrder; + } + + public Boolean getTagState(String tag) { + if(tagsState.containsKey(tag)) + return tagsState.get(tag); + return false; + } + + public void setTagState(String tag, Boolean state) { + if(tagsState.containsKey(tag)) + tagsState.put(tag, state); + } + + public List getActiveTags() { + List tagsList = new ArrayList<>(); + for(String tag : tagsOrder) + { + if(tagsState.get(tag)) { + tagsList.add(tag); + } + } + return tagsList; + } + + public boolean allTagsActive() { + for (String key : tagsState.keySet()) + if (! tagsState.get(key)) + return false; + + return true; + } + + public HashMap getTagsWithState() { + return new HashMap(tagsState); + } + + public void setTags(HashMap tags) { + this.tagsState = tags; + this.tagsOrder = new ArrayList<>(tagsState.keySet()); + Collections.sort(this.tagsOrder); + + this.clear(); + this.addAll(getTags()); + notifyDataSetChanged(); + } +} diff --git a/app/src/main/res/layout/component_card.xml b/app/src/main/res/layout/component_card.xml index 839975c5..5ef14874 100644 --- a/app/src/main/res/layout/component_card.xml +++ b/app/src/main/res/layout/component_card.xml @@ -79,6 +79,14 @@ android:ellipsize="end" android:textSize="8pt" /> + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index cef69261..489c6886 100644 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -2,7 +2,6 @@ - + android:layout_height="match_parent"> - + + + + + + + diff --git a/app/src/main/res/layout/dialog_manual_entry.xml b/app/src/main/res/layout/dialog_manual_entry.xml index 0f257090..d22050c7 100644 --- a/app/src/main/res/layout/dialog_manual_entry.xml +++ b/app/src/main/res/layout/dialog_manual_entry.xml @@ -133,4 +133,25 @@ + + + + +