Introduce Topics/Classification and Swipe to move

This commit is contained in:
ligi 2015-12-21 02:21:42 +01:00
parent 3b8ecbc14e
commit 03ea344a1c
39 changed files with 1396 additions and 183 deletions

1
.gitignore vendored
View file

@ -4,7 +4,6 @@ build
#assets
bin
gen
proguard-project.txt
project.properties
gradle.properties
local.properties

View file

@ -6,7 +6,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:2.0.0-beta4'
classpath 'com.android.tools.build:gradle:2.0.0-beta5'
classpath 'de.felixschulze.gradle:gradle-spoon-plugin:2.6.1'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.12.0'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
@ -82,6 +82,10 @@ android {
// hack for instrumentation testing :-(
exclude 'LICENSE.txt'
exclude 'META-INF/maven/com.google.guava/guava/pom.properties'
exclude 'META-INF/maven/com.google.guava/guava/pom.xml'
}
lintOptions {
@ -119,6 +123,8 @@ dependencies {
exclude module: 'recyclerview-v7'
}
androidTestCompile('com.android.support.test.espresso:espresso-web:2.2.1')
androidTestCompile 'com.squareup.spoon:spoon-client:1.3.2'
androidTestCompile 'com.squareup.assertj:assertj-android:1.1.1'
@ -145,6 +151,7 @@ dependencies {
compile 'com.squareup.okhttp:okhttp:2.7.4'
compile 'com.larswerkman:HoloColorPicker:1.5'
compile 'com.google.code.findbugs:jsr305:3.0.1'
compile 'com.squareup.moshi:moshi:1.1.0'
compile 'net.steamcrafted:load-toast:1.0.10'

View file

@ -0,0 +1,110 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
#http://stackoverflow.com/questions/19274974/android-badparcelableexception-only-with-signed-apk
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
public *;
}
# optimize
-optimizationpasses 2
-optimizations !code/simplification/arithmetic
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
# AppCompat
-dontwarn android.support.v7.**
-keep class android.support.v7.** { *; }
-keep interface android.support.v7.** { *; }
# Keep line numbers to alleviate debugging stack traces
-renamesourcefileattribute SourceFile
-keepattributes SourceFile,LineNumberTable
### for api client
-keepattributes Signature,RuntimeVisibleAnnotations,AnnotationDefault
-keepclassmembers class * {
@com.google.api.client.util.Key <fields>;
}
# Needed by Guava
# See https://groups.google.com/forum/#!topic/guava-discuss/YCZzeCiIVoI
-dontwarn sun.misc.Unsafe
-dontwarn com.google.common.collect.MinMaxPriorityQueue
# Needed by google-http-client-android when linking against an older platform version
-dontwarn com.google.api.client.extensions.android.**
# Needed by google-api-client-android when linking against an older platform version
-dontwarn com.google.api.client.googleapis.extensions.android.**
#### for butterknife
-dontwarn butterknife.internal.**
-keep class **$$ViewBinder { *; }
-keepnames class * { @butterknife.Bind *;}
#### for support 22
-dontwarn android.support.**
#### for guava
-dontwarn javax.annotation.**
-dontwarn javax.inject.**
-dontwarn sun.misc.Unsafe
-dontwarn com.google.common.collect.MinMaxPriorityQueue
-keep,allowoptimization class com.google.inject.** { *; }
-keep,allowoptimization class javax.inject.** { *; }
-keep,allowoptimization class javax.annotation.** { *; }
-keep,allowoptimization class com.google.inject.Binder
-keepclasseswithmembers public class * {
public static void main(java.lang.String[]);
}
-keepclassmembers,allowoptimization class com.google.common.* {
void finalizeReferent();
void startFinalizer(java.lang.Class,java.lang.Object);
}
-keepclassmembers class * {
@com.google.common.eventbus.Subscribe *;
}
-dontwarn java.nio.file.Files
-dontwarn java.nio.file.Path
-dontwarn java.nio.file.OpenOption
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-keep public class com.google.android.gms.**
-dontwarn com.google.android.gms.**

View file

@ -16,4 +16,6 @@ public interface TestComponent extends AppComponent {
void inject(ThePastLocationsStore thePastLocationsStore);
void inject(TheBarCodeEditFragment theBarCodeEditFragment);
void inject(ThePassListSwiping thePassListSwiping);
}

View file

@ -13,6 +13,7 @@ import org.ligi.passandroid.model.comparator.PassSortOrder;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import javax.inject.Singleton;
@ -29,10 +30,11 @@ public class TestModule {
public TestModule() {
passList = new ArrayList<>();
final PassImpl object = new PassImpl();
object.setDescription("description");
object.setBarCode(new BarCode(BarcodeFormat.AZTEC, "messageprobe"));
passList.add(object);
final PassImpl pass = new PassImpl();
pass.setId(UUID.randomUUID().toString());
pass.setDescription("description");
pass.setBarCode(new BarCode(BarcodeFormat.AZTEC, "messageprobe"));
passList.add(pass);
}
public TestModule(List<FiledPass> passList) {

View file

@ -14,6 +14,7 @@ import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static org.ligi.passandroid.steps.HelpSteps.checkThatHelpIsThere;
public class TheEmptyPassList extends BaseIntegration<PassListActivity> {
@ -38,7 +39,8 @@ public class TheEmptyPassList extends BaseIntegration<PassListActivity> {
@MediumTest
public void testHelpGoesToHelp() {
onView(withId(R.id.menu_help)).perform(click());
onView(withId(R.id.help_tv)).check(matches(isDisplayed()));
checkThatHelpIsThere();
}
}

View file

@ -8,13 +8,11 @@ import org.ligi.passandroid.ui.HelpActivity;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isClickable;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.Matchers.allOf;
import static org.ligi.passandroid.steps.HelpSteps.checkThatHelpIsThere;
public class TheHelpActivity extends BaseIntegration<HelpActivity> {
@ -31,7 +29,9 @@ public class TheHelpActivity extends BaseIntegration<HelpActivity> {
@SmallTest
public void testHelpIsThere() {
onView(withId(R.id.help_tv)).check(matches(isDisplayed()));
checkThatHelpIsThere();
Spoon.screenshot(getActivity(), "help");
}

View file

@ -14,6 +14,7 @@ import static android.support.test.espresso.contrib.DrawerActions.open;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.CoreMatchers.not;
import static org.ligi.passandroid.steps.HelpSteps.checkThatHelpIsThere;
@TargetApi(14)
public class ThePassListActivity extends BaseIntegration<PassListActivity> {
@ -34,14 +35,15 @@ public class ThePassListActivity extends BaseIntegration<PassListActivity> {
@MediumTest
public void testListIsThere() {
onView(withId(R.id.content_list)).check(matches(isDisplayed()));
onView(withId(R.id.pass_recyclerview)).check(matches(isDisplayed()));
Spoon.screenshot(getActivity(), "list");
}
@MediumTest
public void testHelpMenuBringsUsToHelp() {
onView(withId(R.id.menu_help)).perform(click());
onView(withId(R.id.help_tv)).check(matches(isDisplayed()));
checkThatHelpIsThere();
}
@MediumTest

View file

@ -0,0 +1,113 @@
package org.ligi.passandroid;
import org.ligi.passandroid.model.PassStore;
import org.ligi.passandroid.ui.PassListActivity;
import javax.inject.Inject;
public class ThePassListSwiping extends BaseIntegration<PassListActivity> {
public static final String CUSTOM_PROBE = "custom";
@Inject
PassStore passStore;
public ThePassListSwiping() {
super(PassListActivity.class);
}
@Override
public void setUp() throws Exception {
super.setUp();
final TestComponent testComponent = DaggerTestComponent.builder().build();
testComponent.inject(this);
App.setComponent(testComponent);
getActivity();
}
/*
TODO figure out why this is flaky on some devices :-(
@MediumTest
public void testDialogOpensWhenSwipeRight() {
onView(withId(R.id.pass_recyclerview)).perform(click());
onView(withId(R.id.pass_recyclerview)).perform(RecyclerViewActions.actionOnItemAtPosition(0,swipeRight()));
onView(withText("description")).perform(swipeRight());
RecyclerViewActions.actionOnItemAtPosition(0,swipeRight());
onView(withText(R.string.move_to_new_topic)).check(matches(isDisplayed()));
}
@MediumTest
public void testWeCanMoveToTrash() {
onView(withId(R.id.pass_recyclerview)).perform(click());
onView(withText("description")).perform(swipeRight());
onView(withId(R.id.suggestion_button_trash)).perform(click());
onView(withText(R.string.topic_trash)).check(matches(isDisplayed()));
assertThat(passStore.getClassifier().getTopics()).containsExactly(getActivity().getString(R.string.topic_trash));
}
@MediumTest
public void testWeCanMoveToArchive() {
onView(withId(R.id.pass_recyclerview)).perform(click());
onView(withText("description")).perform(swipeRight());
onView(withId(R.id.suggestion_button_archive)).perform(click());
onView(withText(R.string.topic_archive)).check(matches(isDisplayed()));
assertThat(passStore.getClassifier().getTopics()).containsExactly(getActivity().getString(R.string.topic_archive));
}
@MediumTest
public void testWeCanMoveToCustom() {
onView(withId(R.id.pass_recyclerview)).perform(click());
onView(withText("description")).perform(swipeLeft());
onView(withId(R.id.new_topic_edit)).perform(typeText(CUSTOM_PROBE));
onView(withText(android.R.string.ok)).perform(click());
assertThat(passStore.getClassifier().getTopics()).containsExactly(CUSTOM_PROBE);
}
@MediumTest
public void testDialogOpensWhenSwipeLeft() {
onView(withId(R.id.pass_recyclerview)).perform(click());
onView(withId(R.id.pass_recyclerview)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
onView(withId(R.id.pass_recyclerview)).perform(RecyclerViewActions.actionOnItemAtPosition(0, swipeLeft()));
onView(withText(R.string.move_to_new_topic)).check(matches(isDisplayed()));
}
public static ViewAction swipeRight() {
return actionWithAssertions(new GeneralSwipeAction(Swipe.SLOW,
translate(GeneralLocation.CENTER_LEFT, -0.1f, 0),
GeneralLocation.CENTER_RIGHT, Press.FINGER));
}
static CoordinatesProvider translate(final CoordinatesProvider coords,
final float dx, final float dy) {
return new CoordinatesProvider() {
@Override
public float[] calculateCoordinates(View view) {
float xy[] = coords.calculateCoordinates(view);
xy[0] += dx * view.getWidth();
xy[1] += dy * view.getHeight();
return xy;
}
};
}
*/
}

View file

@ -2,15 +2,18 @@ package org.ligi.passandroid.injections;
import org.ligi.passandroid.model.FiledPass;
import org.ligi.passandroid.model.Pass;
import org.ligi.passandroid.model.PassClassifier;
import org.ligi.passandroid.model.PassStore;
import org.ligi.passandroid.model.comparator.PassSortOrder;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
public class FixedPassListPassStore implements PassStore {
private final List<FiledPass> passes;
private Pass actPass;
private PassClassifier passClassifier;
public FixedPassListPassStore(List<FiledPass> passes) {
this.passes = passes;
@ -46,10 +49,6 @@ public class FixedPassListPassStore implements PassStore {
return null;
}
@Override
public void sort(PassSortOrder order) {
}
@Override
public Pass getCurrentPass() {
return actPass;
@ -60,6 +59,14 @@ public class FixedPassListPassStore implements PassStore {
actPass = pass;
}
@Override
public PassClassifier getClassifier() {
if (passClassifier == null) {
passClassifier = new PassClassifier(new HashMap<String, Collection<String>>());
}
return passClassifier;
}
@Override
public boolean deletePassWithId(String id) {
return false;

View file

@ -0,0 +1,13 @@
package org.ligi.passandroid.steps;
import android.support.test.espresso.web.assertion.WebViewAssertions;
import static android.support.test.espresso.web.matcher.DomMatchers.containingTextInBody;
import static android.support.test.espresso.web.sugar.Web.onWebView;
public class HelpSteps {
public static void checkThatHelpIsThere() {
onWebView().check(WebViewAssertions.webContent(containingTextInBody("Example passes")));
}
}

View file

@ -23,16 +23,20 @@
android:theme="@style/AppTheme">
<service android:name=".ui.SearchPassesIntentService" />
<activity
android:name=".ui.PassListActivity"
android:label="@string/app_name">
android:label="@string/app_name"
android:theme="@style/AppBaseThemeNoActionbar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ui.HelpActivity" />
<activity
android:name=".ui.HelpActivity"
android:theme="@style/AppBaseThemeNoActionbar" />
<activity
android:name=".ui.PassEditActivity"
android:screenOrientation="sensorPortrait" />
@ -743,6 +747,7 @@
<activity
android:name=".ui.PassViewActivity"
android:theme="@style/AppBaseThemeNoActionbar"
android:label="@string/app_name"
android:parentActivityName=".ui.PassListActivity">
<meta-data

View file

@ -8,6 +8,7 @@ import org.ligi.passandroid.ui.PassAndroidActivity;
import org.ligi.passandroid.ui.PassEditActivity;
import org.ligi.passandroid.ui.PassImportActivity;
import org.ligi.passandroid.ui.PassListActivity;
import org.ligi.passandroid.ui.PassListFragment;
import org.ligi.passandroid.ui.PassMenuOptions;
import org.ligi.passandroid.ui.PassViewActivityBase;
import org.ligi.passandroid.ui.SearchPassesIntentService;
@ -53,6 +54,8 @@ public interface AppComponent {
void inject(PassAndroidActivity passAndroidActivity);
void inject(PassListFragment passListFragment);
PassStore passStore();
Tracker tracker();

View file

@ -7,6 +7,7 @@ import com.squareup.otto.Bus;
import com.squareup.otto.ThreadEnforcer;
import org.ligi.passandroid.model.AndroidFileSystemPassStore;
import org.ligi.passandroid.model.AndroidSettings;
import org.ligi.passandroid.model.PassStore;
import org.ligi.passandroid.model.Settings;
@ -33,7 +34,7 @@ public class AppModule {
@Singleton
@Provides
Settings provideSettings() {
return new Settings(app);
return new AndroidSettings(app);
}
@Singleton

View file

@ -0,0 +1,25 @@
package org.ligi.passandroid.helper;
import android.app.Activity;
import android.support.design.widget.Snackbar;
import android.view.View;
import org.ligi.passandroid.R;
import org.ligi.passandroid.model.Pass;
import org.ligi.passandroid.model.PassClassifier;
public class MoveHelper {
public static void moveWithUndoSnackbar(final PassClassifier passClassifier, final Pass pass, String topic, final Activity activity) {
final String oldTopic = passClassifier.getTopic(pass);
Snackbar.make(activity.getWindow().getDecorView().findViewById(R.id.fam), "Pass moved to " + topic, Snackbar.LENGTH_LONG)
.setAction("undo", new View.OnClickListener() {
@Override
public void onClick(View v) {
passClassifier.moveToTopic(pass, oldTopic);
}
})
.show();
passClassifier.moveToTopic(pass, topic);
}
}

View file

@ -4,7 +4,6 @@ import android.content.Context;
import org.ligi.axt.AXT;
import org.ligi.passandroid.helper.DirectoryFileFilter;
import org.ligi.passandroid.model.comparator.PassSortOrder;
import org.ligi.passandroid.reader.AppleStylePassReader;
import org.ligi.passandroid.reader.PassReader;
import org.ligi.tracedroid.logging.Log;
@ -12,7 +11,6 @@ import org.ligi.tracedroid.logging.Log;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class AndroidFileSystemPassStore implements PassStore {
@ -22,12 +20,15 @@ public class AndroidFileSystemPassStore implements PassStore {
private List<FiledPass> passList = new ArrayList<>();
private Pass actPass;
private final PassClassifier passClassifier;
public AndroidFileSystemPassStore(final Context context, final Settings settings) {
this.context = context;
path = settings.getPassesDir();
refreshPassesList();
final File classificationFile = new File(settings.getStateDir(), "classification_state.json");
passClassifier = new FileBackedPassClassifier(classificationFile);
}
@Override
@ -124,11 +125,6 @@ public class AndroidFileSystemPassStore implements PassStore {
return getCachedPassOrLoad(id);
}
@Override
public void sort(final PassSortOrder order) {
Collections.sort(passList, order.toComparator());
}
@Override
public Pass getCurrentPass() {
return actPass;
@ -139,9 +135,18 @@ public class AndroidFileSystemPassStore implements PassStore {
actPass = pass;
}
@Override
public PassClassifier getClassifier() {
return passClassifier;
}
@Override
public boolean deletePassWithId(final String id) {
return AXT.at(new File(getPathForID(id))).deleteRecursive();
final boolean result = AXT.at(new File(getPathForID(id))).deleteRecursive();
if (result) {
refreshPassesList();
}
return result;
}
public String getPathForID(final String id) {

View file

@ -0,0 +1,61 @@
package org.ligi.passandroid.model;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;
import android.preference.PreferenceManager;
import org.ligi.passandroid.model.comparator.PassSortOrder;
import java.io.File;
public class AndroidSettings implements Settings {
public static final String ORDER_KEY = "order";
public final Context context;
final SharedPreferences sharedPreferences;
public AndroidSettings(Context context) {
this.context = context;
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
}
@Override
public void setSortOrder(PassSortOrder order) {
sharedPreferences.edit().putInt(ORDER_KEY, order.getInt()).apply();
}
@Override
public PassSortOrder getSortOrder() {
int id = sharedPreferences.getInt(ORDER_KEY, 0);
for (PassSortOrder order : PassSortOrder.values()) {
if (order.getInt() == id) {
return order;
}
}
return PassSortOrder.DATE;
}
@Override
public boolean doTraceDroidEmailSend() {
// will be overridden in test-module
return true;
}
@Override
public String getPassesDir() {
return context.getFilesDir().getAbsolutePath() + "/passes";
}
@Override
public File getStateDir() {
return new File(context.getFilesDir(), "state");
}
@Override
public String getShareDir() {
return Environment.getExternalStorageDirectory() + "/tmp/passbook_share_tmp/";
}
}

View file

@ -0,0 +1,82 @@
package org.ligi.passandroid.model;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import okio.BufferedSink;
import okio.Okio;
public class FileBackedPassClassifier extends PassClassifier {
private final JsonAdapter<Map> adapter;
private final File backed_file;
public FileBackedPassClassifier(final File backed_file) {
super(getBase(backed_file));
this.backed_file = backed_file;
adapter = getAdapter();
}
private static JsonAdapter<Map> getAdapter() {
final Moshi build = new Moshi.Builder().build();
return build.adapter(Map.class);
}
@SuppressWarnings("unchecked")
private static Map<String, Collection<String>> getBase(final File backed_file) {
if (backed_file.exists()) {
try {
return (Map<String, Collection<String>>) getAdapter().fromJson(Okio.buffer(Okio.source(backed_file)));
} catch (IOException e) {
e.printStackTrace();
}
}
return new HashMap<>();
}
@SuppressWarnings("ResultOfMethodCallIgnored")
private BufferedSink getBufferedSinkFromMaybeCreatedFile() {
try {
if (!backed_file.exists()) {
final File parentFile = backed_file.getParentFile();
if (!parentFile.exists()) {
parentFile.mkdirs();
}
backed_file.createNewFile();
}
return Okio.buffer(Okio.sink(backed_file));
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
@Override
public void processDataChange() {
super.processDataChange();
// write
if (adapter != null) {
final BufferedSink buffer = getBufferedSinkFromMaybeCreatedFile();
if (buffer != null) {
try {
adapter.toJson(buffer, pass_id_list_by_topic);
buffer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

View file

@ -0,0 +1,118 @@
package org.ligi.passandroid.model;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArraySet;
public class PassClassifier {
public interface OnClassificationChangeListener {
void OnClassificationChange();
}
public Set<OnClassificationChangeListener> onClassificationChangeListeners = new CopyOnWriteArraySet<>();
public final static String DEFAULT_TOPIC = "active";
protected final Map<String, Collection<String>> pass_id_list_by_topic;
private final Map<String, String> topic_by_id = new HashMap<>();
public PassClassifier(Map<String, Collection<String>> pass_id_list_by_topic) {
this.pass_id_list_by_topic = pass_id_list_by_topic;
processDataChange();
}
public void processDataChange() {
calculateReverseMapping();
removeEmpty();
makeSureDefaultTopicExists();
}
private void calculateReverseMapping() {
topic_by_id.clear();
for (Map.Entry<String, Collection<String>> stringListEntry : pass_id_list_by_topic.entrySet()) {
final String topic = stringListEntry.getKey();
for (String id : stringListEntry.getValue()) {
topic_by_id.put(id, topic);
}
}
}
private void makeSureDefaultTopicExists() {
if (pass_id_list_by_topic.isEmpty()) {
pass_id_list_by_topic.put(DEFAULT_TOPIC, new TreeSet<String>());
}
}
private void removeEmpty() {
final Set<String> toRemove = new HashSet<>();
for (Map.Entry<String, Collection<String>> stringListEntry : pass_id_list_by_topic.entrySet()) {
if (stringListEntry.getValue().isEmpty()) {
toRemove.add(stringListEntry.getKey());
}
}
for (String s : toRemove) {
pass_id_list_by_topic.remove(s);
}
}
public void moveToTopic(final Pass pass, final String newTopic) {
if (topic_by_id.containsKey(pass.getId())) {
final String oldTopic = topic_by_id.get(pass.getId());
final Collection<String> idsForOldTopic = pass_id_list_by_topic.get(oldTopic);
idsForOldTopic.remove(pass.getId());
if (idsForOldTopic.isEmpty()) {
pass_id_list_by_topic.remove(oldTopic);
}
}
upsertPassToTopic(pass, newTopic);
processDataChange();
notifyDataChange();
}
public void notifyDataChange() {
for (OnClassificationChangeListener onClassificationChangeListener : onClassificationChangeListeners) {
onClassificationChangeListener.OnClassificationChange();
}
}
private void upsertPassToTopic(Pass pass, String newTopic) {
if (!pass_id_list_by_topic.containsKey(newTopic)) {
pass_id_list_by_topic.put(newTopic, new TreeSet<String>());
}
final Collection<String> strings = pass_id_list_by_topic.get(newTopic);
if (!strings.contains(pass.getId())) {
strings.add(pass.getId());
processDataChange();
}
}
public String[] getTopics() {
final Set<String> strings = pass_id_list_by_topic.keySet();
return pass_id_list_by_topic.keySet().toArray(new String[strings.size()]);
}
public String getTopic(Pass pass) {
if(topic_by_id.containsKey(pass.getId())) {
return topic_by_id.get(pass.getId());
}
upsertPassToTopic(pass,DEFAULT_TOPIC);
return DEFAULT_TOPIC;
}
}

View file

@ -2,33 +2,28 @@ package org.ligi.passandroid.model;
import android.support.annotation.Nullable;
import org.ligi.passandroid.model.comparator.PassSortOrder;
import java.util.List;
public interface PassStore {
boolean deletePassWithId(String id);
String getPathForID(String id);
List<FiledPass> getPassList();
void preCachePassesList();
void deleteCacheForId(String id);
void refreshPassesList();
Pass getPassbookForId(String id);
boolean deletePassWithId(String id);
void sort(PassSortOrder order);
String getPathForID(String id);
void preCachePassesList();
List<FiledPass> getPassList();
@Nullable
Pass getCurrentPass();
void setCurrentPass(@Nullable Pass pass);
PassClassifier getClassifier();
}

View file

@ -0,0 +1,43 @@
package org.ligi.passandroid.model;
import org.ligi.passandroid.model.comparator.PassSortOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class PassStoreProjection {
private List<FiledPass> passList = new ArrayList<>();
private final PassStore passStore;
private final String topic;
private final PassSortOrder passSortOrder;
public PassStoreProjection(final PassStore passStore, final String topic, PassSortOrder order) {
this.passStore = passStore;
this.topic = topic;
this.passSortOrder = order;
refresh();
}
public List<FiledPass> getPassList() {
return passList;
}
public void refresh() {
ArrayList<FiledPass> newPassList = new ArrayList<>();
for (FiledPass filedPass : passStore.getPassList()) {
if (passStore.getClassifier().getTopic(filedPass).equals(topic)) {
newPassList.add(filedPass);
}
}
Collections.sort(newPassList, passSortOrder.toComparator());
passList = newPassList;
}
}

View file

@ -1,48 +1,21 @@
package org.ligi.passandroid.model;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;
import android.preference.PreferenceManager;
import org.ligi.passandroid.model.comparator.PassSortOrder;
public class Settings {
public static final String ORDER_KEY = "order";
public final Context context;
import java.io.File;
private final SharedPreferences sharedPreferences;
public interface Settings {
public Settings(Context context) {
this.context = context;
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
}
void setSortOrder(PassSortOrder order);
public void setSortOrder(PassSortOrder order) {
sharedPreferences.edit().putInt(ORDER_KEY, order.getInt()).apply();
}
PassSortOrder getSortOrder();
public PassSortOrder getSortOrder() {
int id = sharedPreferences.getInt(ORDER_KEY, 0);
for (PassSortOrder order : PassSortOrder.values()) {
if (order.getInt() == id) {
return order;
}
}
return PassSortOrder.DATE;
}
boolean doTraceDroidEmailSend();
public boolean doTraceDroidEmailSend() {
// will be overridden in test-module
return true;
}
String getPassesDir();
public String getPassesDir() {
return context.getFilesDir().getAbsolutePath() + "/passes";
}
File getStateDir();
public String getShareDir() {
return Environment.getExternalStorageDirectory() + "/tmp/passbook_share_tmp/";
}
String getShareDir();
}

View file

@ -2,19 +2,23 @@ package org.ligi.passandroid.ui;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
import android.widget.TextView;
import android.webkit.WebSettings;
import android.webkit.WebView;
import org.ligi.passandroid.R;
import butterknife.Bind;
import butterknife.ButterKnife;
import org.ligi.passandroid.R;
public class HelpActivity extends AppCompatActivity {
@Bind(R.id.help_tv)
TextView helpTextView;
@Bind(R.id.help_webview)
WebView helpWebView;
@Bind(R.id.toolbar)
Toolbar toolbar;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -22,11 +26,13 @@ public class HelpActivity extends AppCompatActivity {
setContentView(R.layout.activity_help);
ButterKnife.bind(this);
helpTextView.setText(Html.fromHtml(getString(R.string.help_content)));
WebSettings webSettings = helpWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setStandardFontFamily("Sans-Serif");
Linkify.addLinks(helpTextView, Linkify.ALL);
helpWebView.loadData(getString(R.string.help_content),"text/html","utf-8");
helpTextView.setMovementMethod(LinkMovementMethod.getInstance());
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}

View file

@ -0,0 +1,84 @@
package org.ligi.passandroid.ui;
import android.app.Activity;
import android.content.DialogInterface;
import android.support.v7.app.AlertDialog;
import android.view.View;
import android.widget.EditText;
import org.ligi.passandroid.R;
import org.ligi.passandroid.helper.MoveHelper;
import org.ligi.passandroid.model.Pass;
import org.ligi.passandroid.model.PassStore;
import butterknife.Bind;
import butterknife.ButterKnife;
import butterknife.OnClick;
class MoveToNewTopicUI {
private final Activity context;
private final PassStore passStore;
private final Pass pass;
@Bind(R.id.new_topic_edit)
EditText newTopicEditText;
private AlertDialog dialog;
@OnClick(R.id.suggestion_button_trash)
void onTrashClick() {
move(context.getString(R.string.topic_trash));
}
@OnClick(R.id.suggestion_button_archive)
void onArchiveClick() {
move(context.getString(R.string.topic_archive));
}
private void move(String topic) {
MoveHelper.moveWithUndoSnackbar(passStore.getClassifier(),pass,topic,context);
dialog.dismiss();
}
MoveToNewTopicUI(Activity context, PassStore passStore, Pass pass) {
this.context = context;
this.passStore = passStore;
this.pass = pass;
}
public void showTopicMove() {
dialog = new AlertDialog.Builder(context)
.setTitle(context.getString(R.string.move_to_new_topic))
.setView(R.layout.dialog_move_to_new_topic)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// empty but needed
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
passStore.getClassifier().notifyDataChange();
}
})
.show();
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (newTopicEditText.getText().toString().isEmpty()) {
newTopicEditText.setError("cannot be empty");
newTopicEditText.requestFocus();
} else {
move(newTopicEditText.getText().toString());
}
}
});
ButterKnife.bind(this, dialog);
}
}

View file

@ -0,0 +1,66 @@
package org.ligi.passandroid.ui;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.Snackbar;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;
import net.i2p.android.ext.floatingactionbutton.FloatingActionsMenu;
import org.ligi.passandroid.R;
public class MyShyFABBehavior extends CoordinatorLayout.Behavior<FloatingActionsMenu> {
public MyShyFABBehavior() {
}
public MyShyFABBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, FloatingActionsMenu child, View dependency) {
return dependency instanceof Snackbar.SnackbarLayout || dependency instanceof AppBarLayout;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionsMenu child, View dependency) {
if (dependency instanceof Snackbar.SnackbarLayout) {
updateFabTranslationForSnackbar(child, dependency);
}
if (dependency instanceof AppBarLayout) {
final CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
final int fabBottomMargin = lp.bottomMargin;
final int distanceToScroll;
if (child.isExpanded()) {
distanceToScroll = child.getHeight() + fabBottomMargin;
} else {
distanceToScroll = (int) (child.getContext().getResources().getDimension(R.dimen.fab_size_normal) + 2 * fabBottomMargin);
}
final float ratio = ViewCompat.getY(dependency) / getToolbarHeight(dependency.getContext());
ViewCompat.setTranslationY(child, -distanceToScroll * ratio);
}
return false;
}
private void updateFabTranslationForSnackbar(FloatingActionsMenu child, View dependency) {
final float translationY = ViewCompat.getTranslationY(dependency) - dependency.getHeight();
final float translationYClipped = Math.min(0, translationY);
ViewCompat.setTranslationY(child, translationYClipped);
}
private int getToolbarHeight(Context context) {
final TypedArray styledAttributes = context.getTheme().obtainStyledAttributes(
new int[]{R.attr.actionBarSize});
int toolbarHeight = (int) styledAttributes.getDimension(0, 0);
styledAttributes.recycle();
return toolbarHeight;
}
}

View file

@ -1,5 +1,6 @@
package org.ligi.passandroid.ui;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
@ -15,6 +16,7 @@ import org.ligi.passandroid.R;
import org.ligi.passandroid.model.FiledPass;
import org.ligi.passandroid.model.Pass;
import org.ligi.passandroid.model.PassStore;
import org.ligi.passandroid.model.PassStoreProjection;
import java.util.List;
@ -25,10 +27,13 @@ public class PassAdapter extends RecyclerView.Adapter<PassViewHolder> {
@Inject
PassStore passStore;
protected final PassListActivity passListActivity;
protected final AppCompatActivity passListActivity;
private final PassStoreProjection passStoreProjection;
ActionMode actionMode;
public PassAdapter(PassListActivity passListActivity) {
public PassAdapter(AppCompatActivity passListActivity, PassStoreProjection passStoreProjection) {
this.passStoreProjection = passStoreProjection;
App.component().inject(this);
this.passListActivity = passListActivity;
}
@ -42,8 +47,8 @@ public class PassAdapter extends RecyclerView.Adapter<PassViewHolder> {
}
@Override
public void onBindViewHolder(PassViewHolder viewHolder, final int longClickedCardPosition) {
final Pass pass = passStore.getPassList().get(longClickedCardPosition);
public void onBindViewHolder(final PassViewHolder viewHolder, int longClickedCardPosition) {
final Pass pass = passStoreProjection.getPassList().get(longClickedCardPosition);
viewHolder.apply(pass, passListActivity);
@ -62,7 +67,7 @@ public class PassAdapter extends RecyclerView.Adapter<PassViewHolder> {
@Override
public boolean onLongClick(View v) {
final Pass pass = getList().get(longClickedCardPosition);
final Pass pass = getList().get(viewHolder.getAdapterPosition());
if (actionMode != null) {
final boolean clickedOnDifferentItem = actionMode.getTag() == null || !actionMode.getTag().equals(pass);
@ -88,7 +93,7 @@ public class PassAdapter extends RecyclerView.Adapter<PassViewHolder> {
@Override
public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
if (new PassMenuOptions(passListActivity, getList().get(longClickedCardPosition)).process(menuItem)) {
if (new PassMenuOptions(passListActivity, getList().get(viewHolder.getAdapterPosition())).process(menuItem)) {
actionMode.finish();
return true;
}
@ -103,7 +108,7 @@ public class PassAdapter extends RecyclerView.Adapter<PassViewHolder> {
}
});
actionMode.setTag(getList().get(longClickedCardPosition));
actionMode.setTag(getList().get(viewHolder.getAdapterPosition()));
root.setCardElevation(v.getContext().getResources().getDimension(R.dimen.card_longclick_elevation));
@ -119,8 +124,8 @@ public class PassAdapter extends RecyclerView.Adapter<PassViewHolder> {
return position;
}
public List<FiledPass> getList() {
return passStore.getPassList();
private List<FiledPass> getList() {
return passStoreProjection.getPassList();
}
@Override

View file

@ -8,10 +8,14 @@ import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
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.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@ -31,6 +35,7 @@ import org.ligi.passandroid.events.SortOrderChangeEvent;
import org.ligi.passandroid.events.TypeFocusEvent;
import org.ligi.passandroid.helper.PassUtil;
import org.ligi.passandroid.model.FiledPass;
import org.ligi.passandroid.model.PassClassifier;
import org.ligi.snackengage.SnackEngage;
import org.ligi.snackengage.snacks.DefaultRateSnack;
import org.ligi.tracedroid.TraceDroid;
@ -40,15 +45,20 @@ import butterknife.Bind;
import butterknife.ButterKnife;
import butterknife.OnClick;
public class PassListActivity extends PassAndroidActivity {
public class PassListActivity extends PassAndroidActivity implements PassClassifier.OnClassificationChangeListener {
private static final int OPEN_FILE_READ_REQUEST_CODE = 1000;
private PassAdapter passAdapter;
private ActionBarDrawerToggle drawerToggle;
@Bind(R.id.content_list)
RecyclerView recyclerView;
@Bind(R.id.toolbar)
Toolbar toolbar;
@Bind(R.id.tab_layout)
TabLayout tabLayout;
@Bind(R.id.view_pager)
ViewPager viewPager;
@Bind(R.id.drawer_layout)
DrawerLayout drawer;
@ -59,11 +69,14 @@ public class PassListActivity extends PassAndroidActivity {
@Bind(R.id.fam)
FloatingActionsMenu floatingActionsMenu;
private PassTopicFragmentPagerAdapter adapter;
@OnClick(R.id.fab_action_create_pass)
void onFABClick() {
final FiledPass pass = PassUtil.createEmptyPass();
passStore.setCurrentPass(pass);
passStore.getClassifier().moveToTopic(pass,adapter.getPageTitle(tabLayout.getSelectedTabPosition()).toString());
pass.save(passStore);
AXT.at(this).startCommonIntent().activityFromClass(PassEditActivity.class);
floatingActionsMenu.collapse();
@ -86,7 +99,6 @@ public class PassListActivity extends PassAndroidActivity {
@Bind(R.id.fab_action_open_file)
FloatingActionButton openFileFAB;
public final static int VERSION_STARTING_TO_SUPPORT_STORAGE_FRAMEWORK = 19;
@OnClick(R.id.fab_action_open_file)
@ -120,7 +132,6 @@ public class PassListActivity extends PassAndroidActivity {
@Subscribe
public void typeFocus(TypeFocusEvent typeFocusEvent) {
scrollToType(typeFocusEvent.type);
drawer.closeDrawers();
}
@ -131,12 +142,9 @@ public class PassListActivity extends PassAndroidActivity {
public void run() {
passStore.refreshPassesList();
passStore.sort(settings.getSortOrder());
AXT.at(emptyView).setVisibility(passStore.getPassList().isEmpty());
passAdapter.notifyDataSetChanged();
}
});
@ -155,11 +163,9 @@ public class PassListActivity extends PassAndroidActivity {
ButterKnife.bind(this);
AXT.at(openFileFAB).setVisibility(Build.VERSION.SDK_INT >= VERSION_STARTING_TO_SUPPORT_STORAGE_FRAMEWORK);
setSupportActionBar(toolbar);
final LinearLayoutManager llm = new LinearLayoutManager(this);
llm.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(llm);
AXT.at(openFileFAB).setVisibility(Build.VERSION.SDK_INT >= VERSION_STARTING_TO_SUPPORT_STORAGE_FRAMEWORK);
// don't want too many windows in worst case - so check for errors first
if (TraceDroid.getStackTraceFiles().length > 0) {
@ -170,7 +176,7 @@ public class PassListActivity extends PassAndroidActivity {
} else { // if no error - check if there is a new version of the app
tracker.trackEvent("ui_event", "processFile", "updatenotice", null);
SnackEngage.from(this).withSnack(new DefaultRateSnack()).build().engageWhenAppropriate();
SnackEngage.from(floatingActionsMenu).withSnack(new DefaultRateSnack()).build().engageWhenAppropriate();
}
drawerToggle = new ActionBarDrawerToggle(this, drawer, R.string.drawer_open, R.string.drawer_close) {
@ -185,19 +191,13 @@ public class PassListActivity extends PassAndroidActivity {
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
passAdapter = new PassAdapter(this);
recyclerView.setAdapter(passAdapter);
adapter = new PassTopicFragmentPagerAdapter(passStore.getClassifier(), getSupportFragmentManager());
viewPager.setAdapter(adapter);
tabLayout.setupWithViewPager(viewPager);
}
private void scrollToType(String type) {
for (int i = 0; i < passAdapter.getItemCount(); i++) {
if (passStore.getPassList().get(i).getTypeNotNull().equals(type)) {
recyclerView.scrollToPosition(i);
return; // we are done
}
}
}
@Override
@ -222,6 +222,9 @@ public class PassListActivity extends PassAndroidActivity {
bus.register(this);
refreshPasses();
adapter.notifyDataSetChanged();
passStore.getClassifier().onClassificationChangeListeners.add(this);
}
@Override
@ -245,8 +248,77 @@ public class PassListActivity extends PassAndroidActivity {
@Override
protected void onPause() {
passStore.getClassifier().onClassificationChangeListeners.remove(this);
bus.unregister(this);
super.onPause();
}
@Override
public void OnClassificationChange() {
refreshPasses();
adapter.notifyDataSetChanged();
setupWithViewPagerIfNeeded();
}
private void setupWithViewPagerIfNeeded() {
if (!areTabLayoutAndViewPagerInSync()) {
tabLayout.setupWithViewPager(viewPager);
}
}
private boolean areTabLayoutAndViewPagerInSync() {
if (adapter.getCount() == tabLayout.getTabCount()) {
for (int i=0;i<adapter.getCount();i++) {
if (!adapter.getPageTitle(i).equals(tabLayout.getTabAt(i).getText())) {
return false;
}
}
return true;
}
return false;
}
private static class PassTopicFragmentPagerAdapter extends FragmentStatePagerAdapter {
private String[] topic_array;
private final PassClassifier passClassifier;
public PassTopicFragmentPagerAdapter(PassClassifier passClassifier, FragmentManager fragmentManager) {
super(fragmentManager);
this.passClassifier = passClassifier;
notifyDataSetChanged();
}
@Override
public void notifyDataSetChanged() {
topic_array = passClassifier.getTopics();
super.notifyDataSetChanged();
}
@Override
public Fragment getItem(int position) {
return PassListFragment.newInstance(topic_array[position]);
}
@Override
public int getItemPosition(Object object) {
return POSITION_NONE; // TODO - return POSITION_UNCHANGED in some cases
}
@Override
public int getCount() {
return topic_array.length;
}
@Override
public CharSequence getPageTitle(int position) {
return topic_array[position];
}
}
}

View file

@ -0,0 +1,181 @@
package org.ligi.passandroid.ui;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.squareup.otto.Bus;
import com.squareup.otto.Subscribe;
import org.ligi.passandroid.App;
import org.ligi.passandroid.R;
import org.ligi.passandroid.events.TypeFocusEvent;
import org.ligi.passandroid.helper.MoveHelper;
import org.ligi.passandroid.model.FiledPass;
import org.ligi.passandroid.model.PassStore;
import org.ligi.passandroid.model.PassStoreProjection;
import org.ligi.passandroid.model.Settings;
import javax.inject.Inject;
import static android.support.v7.widget.helper.ItemTouchHelper.LEFT;
import static android.support.v7.widget.helper.ItemTouchHelper.RIGHT;
import static android.support.v7.widget.helper.ItemTouchHelper.SimpleCallback;
import static org.ligi.passandroid.model.PassClassifier.OnClassificationChangeListener;
public class PassListFragment extends Fragment implements OnClassificationChangeListener {
private static final String BUNDLE_KEY_TOPIC = "topic";
private PassStoreProjection passStoreProjection;
private PassAdapter adapter;
private RecyclerView recyclerView;
public static PassListFragment newInstance(final String topic) {
PassListFragment myFragment = new PassListFragment();
Bundle args = new Bundle();
args.putString(BUNDLE_KEY_TOPIC, topic);
myFragment.setArguments(args);
return myFragment;
}
@Inject
PassStore passStore;
@Inject
Settings settings;
@Inject
Bus bus;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
final View inflate = inflater.inflate(R.layout.pass_recycler, container, false);
recyclerView = (RecyclerView) inflate.findViewById(R.id.pass_recyclerview);
App.component().inject(this);
passStoreProjection = new PassStoreProjection(passStore, getArguments().getString(BUNDLE_KEY_TOPIC), settings.getSortOrder());
adapter = new PassAdapter((AppCompatActivity) getActivity(), passStoreProjection);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
SimpleCallback simpleItemTouchCallback = new SimpleCallback(0, LEFT | RIGHT) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
final FiledPass pass = passStoreProjection.getPassList().get(viewHolder.getAdapterPosition());
final String nextTopic = calculateNextTopic(swipeDir, pass);
if (nextTopic != null) {
MoveHelper.moveWithUndoSnackbar(passStore.getClassifier(),pass,nextTopic,getActivity());
} else {
new MoveToNewTopicUI(getActivity(),passStore, pass).showTopicMove();
}
}
@Nullable
private String calculateNextTopic(final int swipeDir, final FiledPass pass) {
final String[] topics = passStore.getClassifier().getTopics();
switch (swipeDir) {
case LEFT:
return getNextTopicLeft(pass, topics);
case RIGHT:
return getNextTopicRight(pass, topics);
}
return null;
}
@Nullable
private String getNextTopicRight(FiledPass pass, String[] topics) {
boolean nextIsCandidate = false;
for (String topic : topics) {
if (nextIsCandidate) {
return topic;
}
if (passStore.getClassifier().getTopic(pass).equals(topic)) {
nextIsCandidate = true;
}
}
return null;
}
@Nullable
private String getNextTopicLeft(FiledPass pass, String[] topics) {
String prev = null;
for (String topic : topics) {
if (passStore.getClassifier().getTopic(pass).equals(topic)) {
return prev;
}
prev = topic;
}
return null;
}
};
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(simpleItemTouchCallback);
itemTouchHelper.attachToRecyclerView(recyclerView);
passStore.getClassifier().onClassificationChangeListeners.add(this);
bus.register(this);
return inflate;
}
@Override
public void onDestroyView() {
super.onDestroyView();
bus.unregister(this);
passStore.getClassifier().onClassificationChangeListeners.remove(this);
}
@Override
public void OnClassificationChange() {
passStoreProjection.refresh();
adapter.notifyDataSetChanged();
}
@Subscribe
public void typeFocus(TypeFocusEvent typeFocusEvent) {
scrollToType(typeFocusEvent.type);
}
private void scrollToType(String type) {
for (int i = 0; i < adapter.getItemCount(); i++) {
if (passStoreProjection.getPassList().get(i).getTypeNotNull().equals(type)) {
recyclerView.scrollToPosition(i);
return; // we are done
}
}
}
}

View file

@ -9,6 +9,7 @@ import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.NavUtils;
import android.support.v4.app.TaskStackBuilder;
import android.support.v7.widget.Toolbar;
import android.text.Html;
import android.text.util.Linkify;
import android.view.Menu;
@ -77,6 +78,9 @@ public class PassViewActivity extends PassViewActivityBase {
@Bind(R.id.barcode_alt_text)
TextView barcodeAlternativeText;
@Bind(R.id.toolbar)
Toolbar toolbar;
@OnClick(R.id.zoomIn)
void zoomIn() {
setBarCodeSize(currentBarcodeWidth + getFingerSize());
@ -125,28 +129,6 @@ public class PassViewActivity extends PassViewActivityBase {
int currentBarcodeWidth;
@Override
protected void onResume() {
super.onResume();
if (optionalPass == null) {
return;
}
AXT.at(this).disableRotation();
final View contentView = getLayoutInflater().inflate(R.layout.activity_pass_view, null);
setContentView(contentView);
final ViewGroup extraViewContainer = (ViewGroup) contentView.findViewById(R.id.passExtrasContainer);
final View passExtrasView = getLayoutInflater().inflate(R.layout.pass_view_extra_data, extraViewContainer, false);
extraViewContainer.addView(passExtrasView);
ButterKnife.bind(this);
refresh();
}
@Override
protected void refresh() {
super.refresh();
@ -226,14 +208,34 @@ public class PassViewActivity extends PassViewActivityBase {
AXT.at(this).disableRotation();
setContentView(R.layout.activity_pass_view);
}
final ViewGroup extraViewContainer = (ViewGroup) findViewById(R.id.passExtrasContainer);
getLayoutInflater().inflate(R.layout.pass_view_extra_data, extraViewContainer);
@Override
protected void onResume() {
super.onResume();
if (optionalPass == null) {
return;
}
AXT.at(this).disableRotation();
final View contentView = getLayoutInflater().inflate(R.layout.activity_pass_view, null);
setContentView(contentView);
final ViewGroup extraViewContainer = (ViewGroup) contentView.findViewById(R.id.passExtrasContainer);
final View passExtrasView = getLayoutInflater().inflate(R.layout.pass_view_extra_data, extraViewContainer, false);
extraViewContainer.addView(passExtrasView);
ButterKnife.bind(this);
setSupportActionBar(toolbar);
configureActionBar();
refresh();
}
private void addFrontFields(PassFieldList passFields) {
for (PassField field : passFields) {

View file

@ -37,11 +37,6 @@ public class PassViewActivityBase extends PassAndroidActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportActionBar() != null) {
getSupportActionBar().setHomeButtonEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
// a little hack because I strongly disagree with the style guide here
// ;-)
// not having the Actionbar overflow menu also with devices with hardware
@ -57,7 +52,6 @@ public class PassViewActivityBase extends PassAndroidActivity {
} catch (Exception ex) {
// Ignore - but at least we tried ;-)
}
}
@Override
@ -76,6 +70,16 @@ public class PassViewActivityBase extends PassAndroidActivity {
tracker.trackException("pass not present in " + this, false);
finish();
}
configureActionBar();
}
protected void configureActionBar() {
if (getSupportActionBar() != null) {
getSupportActionBar().setHomeButtonEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
}
protected void refresh() {

View file

@ -1,11 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/help_tv"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<WebView
android:id="@+id/help_webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"/>
</ScrollView>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>

View file

@ -1,8 +1,35 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<include layout="@layout/pass_list_item" />
</android.support.v4.widget.NestedScrollView>
</ScrollView>
</android.support.design.widget.CoordinatorLayout>

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:orientation="vertical">
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/new_topic_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Title for new topic" />
</android.support.design.widget.TextInputLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Suggestions:" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/suggestion_button_trash"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TRASH"
/>
<Button
android:id="@+id/suggestion_button_archive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ARCHIVE"
/>
</LinearLayout>
</LinearLayout>

View file

@ -1,17 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fab="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:fab="http://schemas.android.com/apk/res-auto"
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
<android.support.design.widget.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
@ -21,13 +48,6 @@
android:singleLine="false"
android:text="@string/empty_text_view" />
<android.support.v7.widget.RecyclerView
android:id="@+id/content_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:choiceMode="singleChoice"
android:divider="@null"
android:listSelector="@android:color/transparent" />
<net.i2p.android.ext.floatingactionbutton.FloatingActionsMenu
android:id="@+id/fam"
@ -36,9 +56,11 @@
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_gravity="right|bottom"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
app:layout_behavior=".ui.MyShyFABBehavior"
fab:fab_addButtonColorNormal="@color/accent"
fab:fab_addButtonColorPressed="@color/icon_green"
fab:fab_addButtonPlusIconColor="@android:color/black"
@ -79,6 +101,7 @@
<net.i2p.android.ext.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_action_create_pass"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -91,7 +114,7 @@
</net.i2p.android.ext.floatingactionbutton.FloatingActionsMenu>
</RelativeLayout>
</android.support.design.widget.CoordinatorLayout>
<FrameLayout
android:id="@+id/left_drawer"

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/pass_recyclerview"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:choiceMode="singleChoice"
android:divider="@null"
android:listSelector="@android:color/transparent" />

View file

@ -36,11 +36,6 @@ The app is offline usable once you downloaded the pass!
<li>The new passbook is visible in PassAndroid.</li>
</ol>
<h2>Why this app?</h2>
The tickets for the Chaos Congress were only for print and iPhone passbooks.
Ligi - the original author - didn\'t want to print the ticket or buy an Apple device, so he programmed PassAndroid.
<a href="https://twitter.com/mr_ligi/status/678708731190841346">Source</a>
<H1>Credits</H1>
Big thanks to <a href="http://ltorrecilla.com/">Luis Torrecilla</a> for the Design & <a href="https://github.com/cketti">Cketti</a> for pull-requests!
@ -123,4 +118,7 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS
<string name="pass_updated">Pass Updated</string>
<string name="expiration_date_to_calendar_warning_message">You are about to add the expiration-date to the calendar - If you think there is a better date in this pass - please send it to me - PassAndroid needs to know where this</string>
<string name="expiration_date_to_calendar_warning_title">Add Expiration-Date</string>
<string name="topic_trash">trash</string>
<string name="topic_archive">archive</string>
<string name="move_to_new_topic">Move to new Topic</string>
</resources>

View file

@ -1,4 +1,11 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppBaseThemeNoActionbar" parent="AppBaseTheme">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppBaseTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowBackground">@color/window_bg</item>
<item name="colorPrimary">@color/primary</item>
@ -8,6 +15,7 @@
<item name="actionModeBackground">@color/secondary</item>
</style>
<style name="AppTheme" parent="AppBaseTheme" />
<style name="nav_btn">

View file

@ -0,0 +1,89 @@
package org.ligi.passandroid.unittest;
import org.junit.Test;
import org.ligi.passandroid.model.FiledPass;
import org.ligi.passandroid.model.PassClassifier;
import org.ligi.passandroid.model.PassImpl;
import org.mockito.internal.util.collections.Sets;
import java.util.Collection;
import java.util.HashMap;
import static org.assertj.core.api.Assertions.assertThat;
public class ThePassClassifier {
public static final String ID_1 = "ID1";
public static final String TOPIC_1 = "topic1";
@Test
public void testThatPassIsInactiveByDefault() {
final PassClassifier tested = new PassClassifier(new HashMap<String, Collection<String>>());
assertThat(tested.getTopic(getPassWithId(ID_1)).equals(PassClassifier.DEFAULT_TOPIC));
}
@Test
public void testThatOnlyNonDefaultTopicInTopicListWhenOnePassWithNonDefaultTopic() {
final PassClassifier tested = new PassClassifier(new HashMap<String, Collection<String>>() {
{
put(TOPIC_1, Sets.newSet(ID_1));
}
});
assertThat(tested.getTopics()).containsExactly(TOPIC_1);
}
@Test
public void testThatAfterMovingFromOnlyOneTopicToDefaultTopicOnly() {
final PassClassifier tested = new PassClassifier(new HashMap<String, Collection<String>>() {
{
put(TOPIC_1, Sets.newSet(ID_1));
}
});
tested.moveToTopic(getPassWithId(ID_1), PassClassifier.DEFAULT_TOPIC);
assertThat(tested.getTopics()).containsExactly(PassClassifier.DEFAULT_TOPIC);
}
@Test
public void testThatTopicIsGoneAfterMove() {
final PassClassifier tested = new PassClassifier(new HashMap<String, Collection<String>>() {
{
put(TOPIC_1, Sets.newSet(ID_1));
}
});
tested.moveToTopic(getPassWithId(ID_1), PassClassifier.DEFAULT_TOPIC);
assertThat(tested.getTopics()).containsExactly(PassClassifier.DEFAULT_TOPIC);
}
@Test
public void testThatPassIsInTopicAsExpected() {
final PassClassifier tested = new PassClassifier(new HashMap<String, Collection<String>>() {
{
put(TOPIC_1, Sets.newSet(ID_1));
}
});
assertThat(tested.getTopic(getPassWithId(ID_1)).equals(PassClassifier.DEFAULT_TOPIC));
}
@Test
public void testHasAtLeastOneTopic() {
final PassClassifier tested = new PassClassifier(new HashMap<String, Collection<String>>());
assertThat(tested.getTopics()).containsExactly(PassClassifier.DEFAULT_TOPIC);
}
public FiledPass getPassWithId(String id) {
PassImpl result = new PassImpl();
result.setId(id);
return result;
}
}