commit
d766f15cd1
17 changed files with 711 additions and 21 deletions
|
@ -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))
|
* [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))
|
* [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:
|
#### Translators:
|
||||||
|
|
||||||
|
|
|
@ -105,12 +105,17 @@ public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||||
"\"period\":" + Integer.toString(period) + "," +
|
"\"period\":" + Integer.toString(period) + "," +
|
||||||
"\"digits\":6," +
|
"\"digits\":6," +
|
||||||
"\"type\":\"TOTP\"," +
|
"\"type\":\"TOTP\"," +
|
||||||
"\"algorithm\":\"SHA1\"}";
|
"\"algorithm\":\"SHA1\"," +
|
||||||
|
"\"tags\":[\"test1\",\"test2\"]}";
|
||||||
|
|
||||||
Entry e = new Entry(new JSONObject(s));
|
Entry e = new Entry(new JSONObject(s));
|
||||||
assertTrue(Arrays.equals(secret, e.getSecret()));
|
assertTrue(Arrays.equals(secret, e.getSecret()));
|
||||||
assertEquals(label, e.getLabel());
|
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());
|
assertEquals(s, e.toJSON().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,6 +152,15 @@ public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||||
assertEquals("ACME Co - ACME Co:john.doe@email.com", entry.getLabel());
|
assertEquals("ACME Co - ACME Co:john.doe@email.com", entry.getLabel());
|
||||||
|
|
||||||
assertEquals("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", new String(new Base32().encode(entry.getSecret())));
|
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 {
|
public void testSettingsHelper() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
|
||||||
|
|
|
@ -29,10 +29,13 @@ import android.app.KeyguardManager;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.constraint.ConstraintLayout;
|
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.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.support.v7.widget.SearchView;
|
import android.support.v7.widget.SearchView;
|
||||||
|
@ -46,7 +49,10 @@ import android.view.WindowManager;
|
||||||
import android.view.animation.LinearInterpolator;
|
import android.view.animation.LinearInterpolator;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.CheckedTextView;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
import android.widget.ListView;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
import android.widget.Toast;
|
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.IntentIntegrator;
|
||||||
import com.google.zxing.integration.android.IntentResult;
|
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.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.FloatingActionMenu;
|
||||||
import org.shadowice.flocke.andotp.View.ItemTouchHelper.SimpleItemTouchHelperCallback;
|
import org.shadowice.flocke.andotp.View.ItemTouchHelper.SimpleItemTouchHelperCallback;
|
||||||
import org.shadowice.flocke.andotp.R;
|
import org.shadowice.flocke.andotp.View.TagsAdapter;
|
||||||
import org.shadowice.flocke.andotp.Utilities.TokenCalculator;
|
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
import static org.shadowice.flocke.andotp.Utilities.Settings.SortMode;
|
import static org.shadowice.flocke.andotp.Utilities.Settings.SortMode;
|
||||||
|
|
||||||
|
@ -83,6 +96,10 @@ public class MainActivity extends BaseActivity
|
||||||
private Handler handler;
|
private Handler handler;
|
||||||
private Runnable handlerTask;
|
private Runnable handlerTask;
|
||||||
|
|
||||||
|
private ListView tagsDrawerListView;
|
||||||
|
private TagsAdapter tagsDrawerAdapter;
|
||||||
|
private ActionBarDrawerToggle tagsToggle;
|
||||||
|
|
||||||
// QR code scanning
|
// QR code scanning
|
||||||
private void scanQRCode(){
|
private void scanQRCode(){
|
||||||
new IntentIntegrator(MainActivity.this)
|
new IntentIntegrator(MainActivity.this)
|
||||||
|
@ -102,6 +119,7 @@ public class MainActivity extends BaseActivity
|
||||||
final EditText periodInput = inputView.findViewById(R.id.manual_period);
|
final EditText periodInput = inputView.findViewById(R.id.manual_period);
|
||||||
final EditText digitsInput = inputView.findViewById(R.id.manual_digits);
|
final EditText digitsInput = inputView.findViewById(R.id.manual_digits);
|
||||||
final Spinner algorithmInput = inputView.findViewById(R.id.manual_algorithm);
|
final Spinner algorithmInput = inputView.findViewById(R.id.manual_algorithm);
|
||||||
|
final Button tagsInput = inputView.findViewById(R.id.manual_tags);
|
||||||
|
|
||||||
final ArrayAdapter<TokenCalculator.HashAlgorithm> algorithmAdapter = new ArrayAdapter<>(this, android.R.layout.simple_expandable_list_item_1, TokenCalculator.HashAlgorithm.values());
|
final ArrayAdapter<TokenCalculator.HashAlgorithm> algorithmAdapter = new ArrayAdapter<>(this, android.R.layout.simple_expandable_list_item_1, TokenCalculator.HashAlgorithm.values());
|
||||||
final ArrayAdapter<Entry.OTPType> typeAdapter = new ArrayAdapter<>(this, android.R.layout.simple_expandable_list_item_1, Entry.PublicTypes.toArray(new Entry.OTPType[Entry.PublicTypes.size()]));
|
final ArrayAdapter<Entry.OTPType> 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<String> allTags = adapter.getTags();
|
||||||
|
HashMap<String, Boolean> 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<String> 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);
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
builder.setTitle(R.string.dialog_title_manual_entry)
|
builder.setTitle(R.string.dialog_title_manual_entry)
|
||||||
.setView(inputView)
|
.setView(inputView)
|
||||||
|
@ -159,10 +207,12 @@ public class MainActivity extends BaseActivity
|
||||||
int period = Integer.parseInt(periodInput.getText().toString());
|
int period = Integer.parseInt(periodInput.getText().toString());
|
||||||
int digits = Integer.parseInt(digitsInput.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();
|
e.updateOTP();
|
||||||
adapter.addEntry(e);
|
adapter.addEntry(e);
|
||||||
adapter.saveEntries();
|
adapter.saveEntries();
|
||||||
|
|
||||||
|
refreshTags();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -273,7 +323,14 @@ public class MainActivity extends BaseActivity
|
||||||
llm.setOrientation(LinearLayoutManager.VERTICAL);
|
llm.setOrientation(LinearLayoutManager.VERTICAL);
|
||||||
recList.setLayoutManager(llm);
|
recList.setLayoutManager(llm);
|
||||||
|
|
||||||
adapter = new EntriesCardAdapter(this);
|
HashMap<String, Boolean> 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.setAdapter(adapter);
|
||||||
|
|
||||||
recList.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
recList.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
|
@ -332,6 +389,14 @@ public class MainActivity extends BaseActivity
|
||||||
handler.postDelayed(this, 1000);
|
handler.postDelayed(this, 1000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setupDrawer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostCreate(Bundle savedInstanceState) {
|
||||||
|
super.onPostCreate(savedInstanceState);
|
||||||
|
tagsToggle.syncState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Controls for the updater background task
|
// 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
|
// Activity results
|
||||||
@Override
|
@Override
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
||||||
|
@ -385,13 +456,16 @@ public class MainActivity extends BaseActivity
|
||||||
e.updateOTP();
|
e.updateOTP();
|
||||||
adapter.addEntry(e);
|
adapter.addEntry(e);
|
||||||
adapter.saveEntries();
|
adapter.saveEntries();
|
||||||
|
refreshTags();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Toast.makeText(this, R.string.toast_invalid_qr_code, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, R.string.toast_invalid_qr_code, Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (requestCode == INTENT_INTERNAL_BACKUP && resultCode == RESULT_OK) {
|
} else if (requestCode == INTENT_INTERNAL_BACKUP && resultCode == RESULT_OK) {
|
||||||
if (intent.getBooleanExtra("reload", false))
|
if (intent.getBooleanExtra("reload", false)) {
|
||||||
adapter.loadEntries();
|
adapter.loadEntries();
|
||||||
|
refreshTags();
|
||||||
|
}
|
||||||
} else if (requestCode == INTENT_INTERNAL_AUTHENTICATE) {
|
} else if (requestCode == INTENT_INTERNAL_AUTHENTICATE) {
|
||||||
if (resultCode != RESULT_OK) {
|
if (resultCode != RESULT_OK) {
|
||||||
Toast.makeText(getBaseContext(), R.string.toast_auth_failed, Toast.LENGTH_LONG).show();
|
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);
|
adapter.setSortMode(SortMode.LABEL);
|
||||||
touchHelperCallback.setDragEnabled(false);
|
touchHelperCallback.setDragEnabled(false);
|
||||||
}
|
}
|
||||||
|
} else if (tagsToggle.onOptionsItemSelected(item)) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item);
|
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<String>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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<String, Boolean> 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());
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -26,13 +26,16 @@ package org.shadowice.flocke.andotp.Database;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import org.apache.commons.codec.binary.Base32;
|
import org.apache.commons.codec.binary.Base32;
|
||||||
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.shadowice.flocke.andotp.Utilities.TokenCalculator;
|
import org.shadowice.flocke.andotp.Utilities.TokenCalculator;
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -48,6 +51,7 @@ public class Entry {
|
||||||
private static final String JSON_DIGITS = "digits";
|
private static final String JSON_DIGITS = "digits";
|
||||||
private static final String JSON_TYPE = "type";
|
private static final String JSON_TYPE = "type";
|
||||||
private static final String JSON_ALGORITHM = "algorithm";
|
private static final String JSON_ALGORITHM = "algorithm";
|
||||||
|
private static final String JSON_TAGS = "tags";
|
||||||
|
|
||||||
private OTPType type = OTPType.TOTP;
|
private OTPType type = OTPType.TOTP;
|
||||||
private int period = TokenCalculator.TOTP_DEFAULT_PERIOD;
|
private int period = TokenCalculator.TOTP_DEFAULT_PERIOD;
|
||||||
|
@ -57,16 +61,18 @@ public class Entry {
|
||||||
private String label;
|
private String label;
|
||||||
private String currentOTP;
|
private String currentOTP;
|
||||||
private long last_update = 0;
|
private long last_update = 0;
|
||||||
|
public List<String> tags = new ArrayList<>();
|
||||||
|
|
||||||
public Entry(){}
|
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<String> tags) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.secret = new Base32().decode(secret.toUpperCase());
|
this.secret = new Base32().decode(secret.toUpperCase());
|
||||||
this.period = period;
|
this.period = period;
|
||||||
this.digits = digits;
|
this.digits = digits;
|
||||||
this.label = label;
|
this.label = label;
|
||||||
this.algorithm = algorithm;
|
this.algorithm = algorithm;
|
||||||
|
this.tags = tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Entry(String contents) throws Exception {
|
public Entry(String contents) throws Exception {
|
||||||
|
@ -91,6 +97,7 @@ public class Entry {
|
||||||
String period = uri.getQueryParameter("period");
|
String period = uri.getQueryParameter("period");
|
||||||
String digits = uri.getQueryParameter("digits");
|
String digits = uri.getQueryParameter("digits");
|
||||||
String algorithm = uri.getQueryParameter("algorithm");
|
String algorithm = uri.getQueryParameter("algorithm");
|
||||||
|
List<String> tags = uri.getQueryParameters("tags");
|
||||||
|
|
||||||
if(issuer != null){
|
if(issuer != null){
|
||||||
label = issuer +" - "+label;
|
label = issuer +" - "+label;
|
||||||
|
@ -116,6 +123,12 @@ public class Entry {
|
||||||
} else {
|
} else {
|
||||||
this.algorithm = TokenCalculator.DEFAULT_ALGORITHM;
|
this.algorithm = TokenCalculator.DEFAULT_ALGORITHM;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(tags != null) {
|
||||||
|
this.tags = tags;
|
||||||
|
} else {
|
||||||
|
this.tags = new ArrayList<>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Entry (JSONObject jsonObj) throws JSONException {
|
public Entry (JSONObject jsonObj) throws JSONException {
|
||||||
|
@ -140,6 +153,16 @@ public class Entry {
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
this.algorithm = TokenCalculator.DEFAULT_ALGORITHM;
|
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 {
|
public JSONObject toJSON() throws JSONException {
|
||||||
|
@ -151,6 +174,12 @@ public class Entry {
|
||||||
jsonObj.put(JSON_TYPE, getType().toString());
|
jsonObj.put(JSON_TYPE, getType().toString());
|
||||||
jsonObj.put(JSON_ALGORITHM, algorithm.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;
|
return jsonObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,6 +223,10 @@ public class Entry {
|
||||||
this.digits = digits;
|
this.digits = digits;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getTags() { return tags; }
|
||||||
|
|
||||||
|
public void setTags(List<String> tags) { this.tags = tags; }
|
||||||
|
|
||||||
public TokenCalculator.HashAlgorithm getAlgorithm() {
|
public TokenCalculator.HashAlgorithm getAlgorithm() {
|
||||||
return this.algorithm;
|
return this.algorithm;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ import java.io.File;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -125,6 +126,10 @@ public class Settings {
|
||||||
return settings.getLong(getResString(keyId), defaultValue);
|
return settings.getLong(getResString(keyId), defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Set<String> getStringSet(int keyId, Set<String> defaultValue) {
|
||||||
|
return new HashSet<String>(settings.getStringSet(getResString(keyId), defaultValue));
|
||||||
|
}
|
||||||
|
|
||||||
private void setBoolean(int keyId, boolean value) {
|
private void setBoolean(int keyId, boolean value) {
|
||||||
settings.edit()
|
settings.edit()
|
||||||
.putBoolean(getResString(keyId), value)
|
.putBoolean(getResString(keyId), value)
|
||||||
|
@ -137,6 +142,12 @@ public class Settings {
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setStringSet(int keyId, Set<String> value) {
|
||||||
|
settings.edit()
|
||||||
|
.putStringSet(getResString(keyId), value)
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
private void remove(int keyId) {
|
private void remove(int keyId) {
|
||||||
settings.edit()
|
settings.edit()
|
||||||
.remove(getResString(keyId))
|
.remove(getResString(keyId))
|
||||||
|
@ -288,4 +299,35 @@ public class Settings {
|
||||||
public boolean getOpenPGPVerify() {
|
public boolean getOpenPGPVerify() {
|
||||||
return getBoolean(R.string.settings_key_openpgp_verify, false);
|
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<String> toggledTags = getStringSet(R.string.settings_key_tags_toggles, new HashSet<String>());
|
||||||
|
return !toggledTags.contains(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTagToggle(String tag, Boolean value) {
|
||||||
|
Set<String> toggledTags = getStringSet(R.string.settings_key_tags_toggles, new HashSet<String>());
|
||||||
|
if(value)
|
||||||
|
toggledTags.remove(tag);
|
||||||
|
else
|
||||||
|
toggledTags.add(tag);
|
||||||
|
setStringSet(R.string.settings_key_tags_toggles, toggledTags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<String, Boolean> 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();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,6 +44,8 @@ import android.widget.Toast;
|
||||||
|
|
||||||
import org.shadowice.flocke.andotp.Database.Entry;
|
import org.shadowice.flocke.andotp.Database.Entry;
|
||||||
import org.shadowice.flocke.andotp.Utilities.DatabaseHelper;
|
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.View.ItemTouchHelper.ItemTouchHelperAdapter;
|
||||||
import org.shadowice.flocke.andotp.R;
|
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.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
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<EntryViewHolder>
|
public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
implements ItemTouchHelperAdapter, Filterable {
|
implements ItemTouchHelperAdapter, Filterable {
|
||||||
|
@ -61,13 +67,17 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
private ArrayList<Entry> entries;
|
private ArrayList<Entry> entries;
|
||||||
private ArrayList<Entry> displayedEntries;
|
private ArrayList<Entry> displayedEntries;
|
||||||
private Callback callback;
|
private Callback callback;
|
||||||
|
private List<String> tagsFilter = new ArrayList<>();
|
||||||
|
|
||||||
private SortMode sortMode = SortMode.UNSORTED;
|
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.context = context;
|
||||||
this.sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
|
this.sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
this.tagsFilterAdapter = tagsFilterAdapter;
|
||||||
|
this.settings = new Settings(context);
|
||||||
loadEntries();
|
loadEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +101,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
|
|
||||||
public void entriesChanged() {
|
public void entriesChanged() {
|
||||||
displayedEntries = sortEntries(entries);
|
displayedEntries = sortEntries(entries);
|
||||||
|
filterByTags(tagsFilter);
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,6 +114,29 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
entriesChanged();
|
entriesChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void filterByTags(List<String> tags) {
|
||||||
|
tagsFilter = tags;
|
||||||
|
List<Entry> 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() {
|
public void updateTokens() {
|
||||||
boolean change = false;
|
boolean change = false;
|
||||||
|
|
||||||
|
@ -119,7 +153,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
public void onBindViewHolder(EntryViewHolder entryViewHolder, int i) {
|
public void onBindViewHolder(EntryViewHolder entryViewHolder, int i) {
|
||||||
Entry entry = displayedEntries.get(i);
|
Entry entry = displayedEntries.get(i);
|
||||||
|
|
||||||
entryViewHolder.updateValues(entry.getLabel(), entry.getCurrentOTP());
|
entryViewHolder.updateValues(entry.getLabel(), entry.getCurrentOTP(), entry.getTags());
|
||||||
|
|
||||||
if (entry.hasNonDefaultPeriod()) {
|
if (entry.hasNonDefaultPeriod()) {
|
||||||
entryViewHolder.showCustomPeriod(entry.getPeriod());
|
entryViewHolder.showCustomPeriod(entry.getPeriod());
|
||||||
|
@ -227,6 +261,52 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void editEntryTags(final int pos) {
|
||||||
|
final int realPos = getRealIndex(pos);
|
||||||
|
final Entry entry = entries.get(realPos);
|
||||||
|
|
||||||
|
HashMap<String, Boolean> 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<String> inUseTags = getTags();
|
||||||
|
|
||||||
|
HashMap<String, Boolean> 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) {
|
public void removeItem(final int pos) {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||||
|
|
||||||
|
@ -265,6 +345,9 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
if (id == R.id.menu_popup_editLabel) {
|
if (id == R.id.menu_popup_editLabel) {
|
||||||
editEntryLabel(pos);
|
editEntryLabel(pos);
|
||||||
return true;
|
return true;
|
||||||
|
} else if (id == R.id.menu_popup_editTags) {
|
||||||
|
editEntryTags(pos);
|
||||||
|
return true;
|
||||||
} else if (id == R.id.menu_popup_remove) {
|
} else if (id == R.id.menu_popup_remove) {
|
||||||
removeItem(pos);
|
removeItem(pos);
|
||||||
return true;
|
return true;
|
||||||
|
@ -293,7 +376,7 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
return this.sortMode;
|
return this.sortMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ArrayList<Entry> sortEntries(ArrayList<Entry> unsorted) {
|
private ArrayList<Entry> sortEntries(List<Entry> unsorted) {
|
||||||
ArrayList<Entry> sorted = new ArrayList<>(unsorted);
|
ArrayList<Entry> sorted = new ArrayList<>(unsorted);
|
||||||
|
|
||||||
if (sortMode == SortMode.LABEL) {
|
if (sortMode == SortMode.LABEL) {
|
||||||
|
@ -314,6 +397,16 @@ public class EntriesCardAdapter extends RecyclerView.Adapter<EntryViewHolder>
|
||||||
return filter;
|
return filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getTags() {
|
||||||
|
HashSet<String> tags = new HashSet<String>();
|
||||||
|
|
||||||
|
for(Entry entry : entries) {
|
||||||
|
tags.addAll(entry.getTags());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ArrayList<String>(tags);
|
||||||
|
}
|
||||||
|
|
||||||
public class EntryFilter extends Filter {
|
public class EntryFilter extends Filter {
|
||||||
@Override
|
@Override
|
||||||
protected FilterResults performFiltering(CharSequence constraint) {
|
protected FilterResults performFiltering(CharSequence constraint) {
|
||||||
|
|
|
@ -38,6 +38,8 @@ import org.shadowice.flocke.andotp.Utilities.Tools;
|
||||||
import org.shadowice.flocke.andotp.View.ItemTouchHelper.ItemTouchHelperViewHolder;
|
import org.shadowice.flocke.andotp.View.ItemTouchHelper.ItemTouchHelperViewHolder;
|
||||||
import org.shadowice.flocke.andotp.R;
|
import org.shadowice.flocke.andotp.R;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class EntryViewHolder extends RecyclerView.ViewHolder
|
public class EntryViewHolder extends RecyclerView.ViewHolder
|
||||||
implements ItemTouchHelperViewHolder {
|
implements ItemTouchHelperViewHolder {
|
||||||
|
|
||||||
|
@ -52,6 +54,7 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
|
||||||
private ImageView visibleImg;
|
private ImageView visibleImg;
|
||||||
private TextView value;
|
private TextView value;
|
||||||
private TextView label;
|
private TextView label;
|
||||||
|
private TextView tags;
|
||||||
private TextView customPeriod;
|
private TextView customPeriod;
|
||||||
|
|
||||||
|
|
||||||
|
@ -66,6 +69,7 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
|
||||||
visibleImg = v.findViewById(R.id.valueImg);
|
visibleImg = v.findViewById(R.id.valueImg);
|
||||||
coverLayout = v.findViewById(R.id.coverLayout);
|
coverLayout = v.findViewById(R.id.coverLayout);
|
||||||
label = v.findViewById(R.id.textViewLabel);
|
label = v.findViewById(R.id.textViewLabel);
|
||||||
|
tags = v.findViewById(R.id.textViewTags);
|
||||||
customPeriodLayout = v.findViewById(R.id.customPeriodLayout);
|
customPeriodLayout = v.findViewById(R.id.customPeriodLayout);
|
||||||
customPeriod = v.findViewById(R.id.customPeriod);
|
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<String> tags) {
|
||||||
this.label.setText(label);
|
this.label.setText(label);
|
||||||
value.setText(token);
|
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) {
|
public void showCustomPeriod(int period) {
|
||||||
|
@ -115,6 +134,7 @@ public class EntryViewHolder extends RecyclerView.ViewHolder
|
||||||
|
|
||||||
public void setLabelSize(int size) {
|
public void setLabelSize(int size) {
|
||||||
label.setTextSize(TypedValue.COMPLEX_UNIT_PT, size);
|
label.setTextSize(TypedValue.COMPLEX_UNIT_PT, size);
|
||||||
|
tags.setTextSize(TypedValue.COMPLEX_UNIT_PT, size - 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLabelScroll(boolean active) {
|
public void setLabelScroll(boolean active) {
|
||||||
|
|
|
@ -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<String> {
|
||||||
|
private Context context;
|
||||||
|
private List<String> tagsOrder;
|
||||||
|
private HashMap<String, Boolean> tagsState;
|
||||||
|
private static final int layoutResourceId = android.R.layout.simple_list_item_multiple_choice;
|
||||||
|
|
||||||
|
public TagsAdapter(Context context, HashMap<String, Boolean> 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<String> 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<String> getActiveTags() {
|
||||||
|
List<String> 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<String, Boolean> getTagsWithState() {
|
||||||
|
return new HashMap<String, Boolean>(tagsState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTags(HashMap<String, Boolean> tags) {
|
||||||
|
this.tagsState = tags;
|
||||||
|
this.tagsOrder = new ArrayList<>(tagsState.keySet());
|
||||||
|
Collections.sort(this.tagsOrder);
|
||||||
|
|
||||||
|
this.clear();
|
||||||
|
this.addAll(getTags());
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
|
@ -79,6 +79,14 @@
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:textSize="8pt" />
|
android:textSize="8pt" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textViewTags"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textSize="6pt" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|
66
app/src/main/res/layout/component_tags.xml
Normal file
66
app/src/main/res/layout/component_tags.xml
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/fab_main_layout"
|
||||||
|
android:background="?attr/windowBackground"
|
||||||
|
android:layout_width="@dimen/drawer_width"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_gravity="start" >
|
||||||
|
|
||||||
|
<CheckedTextView
|
||||||
|
android:id="@+id/all_tags_in_drawer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?android:attr/listPreferredItemHeightSmall"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItemSmall"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
|
||||||
|
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||||
|
android:text="@string/button_all_tags"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_gravity="center_horizontal">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="@color/divider" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<CheckedTextView
|
||||||
|
android:id="@+id/no_tags_entries"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?android:attr/listPreferredItemHeightSmall"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItemSmall"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
|
||||||
|
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||||
|
android:text="@string/button_no_tags"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_gravity="center_horizontal">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/strong_divider_size"
|
||||||
|
android:background="@color/divider" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/tags_list_in_drawer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:dividerHeight="1dp"
|
||||||
|
android:headerDividersEnabled="false"
|
||||||
|
android:footerDividersEnabled="true"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -2,7 +2,6 @@
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
|
||||||
android:id="@+id/main_content"
|
android:id="@+id/main_content"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
@ -10,12 +9,21 @@
|
||||||
tools:context="org.shadowice.flocke.andotp.Activities.MainActivity"
|
tools:context="org.shadowice.flocke.andotp.Activities.MainActivity"
|
||||||
tools:showIn="@layout/activity_main">
|
tools:showIn="@layout/activity_main">
|
||||||
|
|
||||||
<android.support.v7.widget.RecyclerView
|
<android.support.v4.widget.DrawerLayout
|
||||||
android:id="@+id/cardList"
|
android:id="@+id/drawer_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:layout_alignParentStart="true" />
|
|
||||||
|
|
||||||
<include layout="@layout/component_fam" />
|
<android.support.v7.widget.RecyclerView
|
||||||
|
android:id="@+id/cardList"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_alignParentStart="true" />
|
||||||
|
|
||||||
|
<include layout="@layout/component_fam" />
|
||||||
|
|
||||||
|
<include layout="@layout/component_tags" />
|
||||||
|
|
||||||
|
</android.support.v4.widget.DrawerLayout>
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
|
@ -133,4 +133,25 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal" >
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_weight="2"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/label_tags"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/manual_tags"
|
||||||
|
style="?android:attr/spinnerStyle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="7"
|
||||||
|
android:textAlignment="textEnd" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
|
@ -5,6 +5,10 @@
|
||||||
android:id="@+id/menu_popup_editLabel"
|
android:id="@+id/menu_popup_editLabel"
|
||||||
android:title="@string/menu_popup_edit_label" />
|
android:title="@string/menu_popup_edit_label" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_popup_editTags"
|
||||||
|
android:title="@string/menu_popup_edit_tags" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_popup_remove"
|
android:id="@+id/menu_popup_remove"
|
||||||
android:title="@string/menu_popup_remove" />
|
android:title="@string/menu_popup_remove" />
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
|
|
||||||
<dimen name="card_corner_radius">0dp</dimen>
|
<dimen name="card_corner_radius">0dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="drawer_width">200dp</dimen>
|
||||||
|
<dimen name="strong_divider_size">2dp</dimen>
|
||||||
<!-- FAB Menu -->
|
<!-- FAB Menu -->
|
||||||
<dimen name="fab_small_elevation">2dp</dimen>
|
<dimen name="fab_small_elevation">2dp</dimen>
|
||||||
<dimen name="fab_small_margin">8dp</dimen>
|
<dimen name="fab_small_margin">8dp</dimen>
|
||||||
|
|
|
@ -28,6 +28,9 @@
|
||||||
<string name="settings_key_security_backup_warning" translatable="false">pref_security_backup_warning_shown</string>
|
<string name="settings_key_security_backup_warning" translatable="false">pref_security_backup_warning_shown</string>
|
||||||
<string name="settings_key_sort_mode" translatable="false">pref_sort_mode</string>
|
<string name="settings_key_sort_mode" translatable="false">pref_sort_mode</string>
|
||||||
<string name="settings_key_special_features" translatable="false">pref_special_features</string>
|
<string name="settings_key_special_features" translatable="false">pref_special_features</string>
|
||||||
|
<string name="settings_key_all_tags_toggle" translatable="false">pref_all_tags_toggle</string>
|
||||||
|
<string name="settings_key_no_tags_toggle" translatable="false">pref_no_tags_toggle</string>
|
||||||
|
<string name="settings_key_tags_toggles" translatable="false">pref_tags_toggles</string>
|
||||||
|
|
||||||
<!-- Default values -->
|
<!-- Default values -->
|
||||||
<string name="settings_default_auth" translatable="false">none</string>
|
<string name="settings_default_auth" translatable="false">none</string>
|
||||||
|
|
|
@ -6,7 +6,10 @@
|
||||||
<string name="button_enter_details">Enter details</string>
|
<string name="button_enter_details">Enter details</string>
|
||||||
<string name="button_scan_qr">Scan QR-Code</string>
|
<string name="button_scan_qr">Scan QR-Code</string>
|
||||||
<string name="button_save">Save</string>
|
<string name="button_save">Save</string>
|
||||||
|
<string name="button_new_tag">New tag</string>
|
||||||
<string name="button_warned">You have been warned!</string>
|
<string name="button_warned">You have been warned!</string>
|
||||||
|
<string name="button_all_tags">All tags</string>
|
||||||
|
<string name="button_no_tags">No tags</string>
|
||||||
|
|
||||||
<!-- Custom formatting -->
|
<!-- Custom formatting -->
|
||||||
<string name="format_custom_period">%d s</string>
|
<string name="format_custom_period">%d s</string>
|
||||||
|
@ -24,6 +27,11 @@
|
||||||
<string name="label_digits">Digits</string>
|
<string name="label_digits">Digits</string>
|
||||||
<string name="label_label">Label</string>
|
<string name="label_label">Label</string>
|
||||||
<string name="label_algorithm">Algorithm</string>
|
<string name="label_algorithm">Algorithm</string>
|
||||||
|
<string name="label_tags">Tags</string>
|
||||||
|
|
||||||
|
<!-- Drawer -->
|
||||||
|
<string name="drawer_open">Show tags</string>
|
||||||
|
<string name="drawer_close">Hide tags</string>
|
||||||
|
|
||||||
<!-- Menu -->
|
<!-- Menu -->
|
||||||
<string name="menu_main_about">About</string>
|
<string name="menu_main_about">About</string>
|
||||||
|
@ -36,6 +44,7 @@
|
||||||
<string name="menu_sort_label">Label</string>
|
<string name="menu_sort_label">Label</string>
|
||||||
|
|
||||||
<string name="menu_popup_edit_label">Edit label</string>
|
<string name="menu_popup_edit_label">Edit label</string>
|
||||||
|
<string name="menu_popup_edit_tags">Edit tags</string>
|
||||||
<string name="menu_popup_remove">Remove</string>
|
<string name="menu_popup_remove">Remove</string>
|
||||||
|
|
||||||
<!-- Toast messages -->
|
<!-- Toast messages -->
|
||||||
|
|
Loading…
Reference in a new issue