/* * Copyright 2015 Daniel Dittmar * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package dan.dit.whatsthat.testsubject.intro; import android.content.SharedPreferences; import android.content.res.Resources; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; import java.util.Set; import dan.dit.whatsthat.R; import dan.dit.whatsthat.testsubject.TestSubjectLevel; import dan.dit.whatsthat.util.compaction.CompactedDataCorruptException; /** * Created by daniel on 08.08.15. */ public class Intro { private static final String KEY_LAST_INTRO_LEVEL = "dan.dit.whatsthat.LAST_INTRO_LEVEL"; private static final String KEY_CURRENT_EPISODE = "dan.dit.whatsthat.CURRENT_MAIN_EPISODE"; private static final int INTERACTION_NEXT_UNMANAGED_EPISODE = 6; public static final int INTERACTION_QUESTION_ANSWERED = 7; private static final int INTERACTION_CURRENT_EPISODE_NOT_YET_DONE = -1; private static final int INTERACTION_NEXT_EPISODE = 1; private final View mIntroView; protected Episode mEpisode; private List<OnEpisodeSkippedListener> mListeners = new LinkedList<>(); private OnInteractionListener mInteractionListener; public void setOnInteractionListener(OnInteractionListener listener) { mInteractionListener = listener; } public interface OnEpisodeSkippedListener { void onEpisodeSkipped(Episode skipped); } public interface OnInteractionListener { void onIntroInteraction(int actionCode); } private Intro(View introView) { mIntroView = introView; if (introView == null) { throw new IllegalArgumentException("No intro view given."); } } public static Intro makeIntro(View introView, TestSubjectLevel level) { return new Intro(introView).initEpisodes(level); } private Intro initEpisodes(TestSubjectLevel level) { mEpisode = level.makeEpisodes(this); if (mEpisode == null) { throw new IllegalArgumentException("No starting episode given."); } return this; } public void addOnEpisodeSkippedListener(OnEpisodeSkippedListener listener) { if (!mListeners.contains(listener)) { mListeners.add(listener); } } public void removeOnEpisodeSkippedListener(OnEpisodeSkippedListener listener) { mListeners.remove(listener); } public View findViewById(int viewId) { return mIntroView.findViewById(viewId); } public void load(SharedPreferences data, int level) { int savedLevel = data.getInt(KEY_LAST_INTRO_LEVEL, TestSubjectLevel.LEVEL_NONE); if (savedLevel != level) { return; } String key = data.getString(KEY_CURRENT_EPISODE, null); mEpisode = searchEpisode(mEpisode, key); try { mEpisode.init(key); } catch (CompactedDataCorruptException e) { Log.e("HomeStuff", "Failed initializing loaded episode: " + e); try { mEpisode.init(null); } catch (CompactedDataCorruptException ee) { Log.e("HomeStuff", "Failed again initialing episode from scratch!"); //ignore } } } private static class EpisodeIterator implements Iterator<Episode> { private Set<EpisodeNode> mAllNodes; private List<EpisodeNode> mStack; private boolean mFoundCycle; private Episode mNext; private static class EpisodeNode { private Episode mEpisode; private int mChildIndex; public EpisodeNode(Episode episode) { mEpisode = episode; } @Override public int hashCode() { return mEpisode.hashCode(); } @Override public boolean equals(Object other) { if (other instanceof EpisodeNode) { return mEpisode.equals(((EpisodeNode) other).mEpisode); } return super.equals(other); } } public EpisodeIterator(Episode start) { mAllNodes = new HashSet<>(); mStack = new LinkedList<>(); mFoundCycle = false; EpisodeNode startNode = new EpisodeNode(start); mAllNodes.add(startNode); mStack.add(startNode); mNext = start; } private void prepareNext() { EpisodeNode node; do { node = depthFirstSearchStep(); } while (node == null && !mStack.isEmpty()); mNext = node != null ? node.mEpisode : null; } private EpisodeNode getEpisodeNode(Episode of) { for (EpisodeNode node : mAllNodes) { if (node.mEpisode.equals(of)) { return node; } } return null; } public boolean foundCycle() { return mFoundCycle; } @Override public boolean hasNext() { return mNext != null; } @Override public Episode next() { if (!hasNext()) { throw new NoSuchElementException(); } Episode curr = mNext; prepareNext(); return curr; } private EpisodeNode depthFirstSearchStep() { EpisodeNode currNode = mStack.get(mStack.size() - 1); Episode curr = currNode.mEpisode; EpisodeNode newNode = null; if (currNode.mChildIndex < curr.getChildrenCount()) { Episode next = curr.getChild(currNode.mChildIndex); currNode.mChildIndex++; EpisodeNode nextNode = getEpisodeNode(next); if (nextNode != null) { // this node was already reached sometime, check if in recursion stack if (mStack.contains(nextNode)) { mFoundCycle = true; } else { mStack.add(nextNode); } } else { nextNode = new EpisodeNode(next); mStack.add(nextNode); mAllNodes.add(nextNode); newNode = nextNode; } } else { mStack.remove(mStack.size() - 1); } return newNode; } @Override public void remove() { throw new UnsupportedOperationException("Removing of episodes not supported."); } } private @NonNull Episode searchEpisode(@NonNull Episode start, @Nullable String key) { String episodeKey = TextUtils.isEmpty(key) ? null : Episode.extractEpisodeKey(key); Iterator<Episode> it = new EpisodeIterator(start); while (it.hasNext()) { Episode next = it.next(); if (next.getEpisodeKey().equals(episodeKey)) { return next; } } return null; } public void save(SharedPreferences.Editor editor, int level) { editor.putInt(KEY_LAST_INTRO_LEVEL, level).putString(KEY_CURRENT_EPISODE, mEpisode.compact()).apply(); } protected void applyMessage(int messageResId) { TextView view = ((TextView) mIntroView.findViewById(R.id.intro_message)); view.setVisibility(messageResId == 0 ? View.INVISIBLE : View.VISIBLE); view.setText(messageResId); } protected void applyMessage(String message) { TextView view = ((TextView) mIntroView.findViewById(R.id.intro_message)); view.setVisibility(TextUtils.isEmpty(message) ? View.INVISIBLE : View.VISIBLE); view.setText(message); } protected void applyIcon(int iconResId) { ImageView view = ((ImageView) mIntroView.findViewById(R.id.intro_icon)); view.clearAnimation(); view.setVisibility(iconResId == 0 ? View.INVISIBLE : View.VISIBLE); view.setImageResource(iconResId); } public Episode getCurrentEpisode() { return mEpisode; } public boolean isMandatoryEpisodeMissing() { Iterator<Episode> it = new EpisodeIterator(mEpisode); while (it.hasNext()) { Episode next = it.next(); if (next.isMandatory()) { return true; } } return false; } public Episode nextEpisode() { return nextEpisode(-1); } public Episode nextEpisode(int childIndex) { // make sure current episode is done // before getting the next episode if (!mEpisode.isDone()) { Log.d("HomeStuff", "Attempting to get next episode where current is not yet done: " + mEpisode); onInteraction(INTERACTION_CURRENT_EPISODE_NOT_YET_DONE); return mEpisode; } Episode next = mEpisode.next(childIndex); if (next != null) { startEpisode(next); onInteraction(INTERACTION_NEXT_EPISODE); return next; } return mEpisode; } private void onInteraction(int actionId) { if (mInteractionListener != null) { mInteractionListener.onIntroInteraction(actionId); } } public void startUnmanagedEpisode(Episode episode) { if (episode == null || episode.mIntro != this) { return; } onInteraction(INTERACTION_NEXT_UNMANAGED_EPISODE); episode.start(); } private void startEpisode(Episode episode) { if (episode == null) { return; } Episode current = getCurrentEpisode(); if (current != null) { for (OnEpisodeSkippedListener listener : mListeners) { listener.onEpisodeSkipped(current); } } episode.start(); mEpisode = episode; } protected void onQuestionAnswered(QuestionEpisode question) { onInteraction(INTERACTION_QUESTION_ANSWERED); } public Resources getResources() { return mIntroView.getResources(); } }