/* * 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.riddle; import android.content.Context; import android.content.SharedPreferences; import android.os.AsyncTask; import android.support.annotation.NonNull; import android.util.Log; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import dan.dit.whatsthat.riddle.types.RiddleType; import dan.dit.whatsthat.util.general.PercentProgressListener; /** * A class that is responsible for initializing the unsolved riddles at application start and remembering * which images were already used by riddles.<br> * The RiddleManager can be accessed through this singleton class after it is initialized. * Created by daniel on 23.04.15. */ public final class RiddleInitializer { /** * The instance, can be freely accessed. State control is ensured through exceptions. Needs to be initialized first! */ public static final RiddleInitializer INSTANCE = new RiddleInitializer(); private static final String IMPORTANT_PREFERENCES_FILE = "dan.dit.whatsthat.important_preferences"; /* * Key to shared preferences entry that stores the highest used id for Riddle cores. Used to retrieve new ids * for new cores as those are not assigned by the database. The database takes over these ids and so its possible * to uniquely identify a riddle before it is saved to permanent storage. This will overflow at max long and cause * errors, but if this ever happens we face a bug or a serious addict. */ private static final String KEY_HIGHEST_USED_ID = "highest_used_id"; private InitTask mInitTask; private List<InitProgressListener> mListener = new LinkedList<>(); private final Map<RiddleType, Set<String>> mUsedImagesForType = new HashMap<>(); private RiddleManager mManager; private long mHighestUsedId; private SharedPreferences mPrefs; private RiddleInitializer() {} // singleton /** * If this class is no longer initializing and the RiddleManager is available, returns it. * @return The RiddleManager instance created by this RiddleInitializer. */ public RiddleManager getRiddleManager() { if (mManager == null || isInitializing()) { throw new IllegalStateException("No manager yet available, initialize first!"); } return mManager; } /** * Registers a riddle. The image will be remembered to be already used by the riddle's type. * @param riddle The riddle to register. Not null. */ void registerUsedRiddleImage(Riddle riddle) { if (riddle == null) { throw new IllegalArgumentException("Null riddle given, what image you want to register?"); } Set<String> typeSet = mUsedImagesForType.get(riddle.getType()); if (typeSet == null) { typeSet = new HashSet<>(); mUsedImagesForType.put(riddle.getType(), typeSet); } typeSet.add(riddle.getImageHash()); } /** * Checks if this RiddleInitializer is initialized and will return a valid RiddleManager and not throw * an Exception. * @return If it is not initialized. */ public boolean isNotInitialized() { return mManager == null || isInitializing(); } // used in success or failure, in good days or in bad days private void onInitFinish() { mInitTask = null; mListener.clear(); } public long nextId() { long next = ++mHighestUsedId; saveHighestUsedId(); return next; } /** * The cancellable task that loads the list of unsolved riddles, used images and other data * stored in the RiddleTable. */ protected class InitTask extends AsyncTask<Void, Integer, List<Riddle>> implements InitProgressListener { private int mBaseProgress; private static final int INIT_STEPS = 2; // step1: unsolved riddles, step2: used images private Context mContext; public InitTask(Context context) { mContext = context; } public List<Riddle> doInBackground(Void... voids) { mBaseProgress = 0; checkedIdsInit(mContext); List<Riddle> unsolved = Riddle.loadUnsolvedRiddles(mContext, this); if (isCancelled()) { return null; } long highestUnsolvedId = Long.MIN_VALUE; for (Riddle rid : unsolved) { if (rid.getId() > highestUnsolvedId) { highestUnsolvedId = rid.getId(); } } if (highestUnsolvedId > mHighestUsedId) { Log.e("Riddle", "Unsolved riddle id higher than loaded highest used id! " + mHighestUsedId + " < " + highestUnsolvedId); } mUsedImagesForType.putAll(Riddle.loadUsedImagesForTypes(mContext, this)); if (isCancelled()) { return null; } return unsolved; } @Override protected void onProgressUpdate(Integer... progress) { if (progress.length == 1) { for (InitProgressListener listener : mListener) { listener.onProgressUpdate(progress[0]); } } } @Override public void onCancelled(List<Riddle> nothing) { onInitFinish(); } @Override public void onPostExecute(List<Riddle> unsolved) { mManager = new RiddleManager(unsolved); List<InitProgressListener> listeners = new ArrayList<>(mListener); // copy as it will be cleared by onInitFinished onInitFinish(); // finish first, so that listeners that check the state are not mislead for (InitProgressListener listener : listeners) { listener.onInitComplete(); } } @Override public void onProgressUpdate(int progress) { //intern progress, not on UI thread here! publishProgress(mBaseProgress + progress / INIT_STEPS); } @Override public void onInitComplete() { // intern progress, not on UI thread here! mBaseProgress += PercentProgressListener.PROGRESS_COMPLETE / INIT_STEPS; publishProgress(mBaseProgress); } } private long loadHighestUsedId() { return mPrefs.getLong(KEY_HIGHEST_USED_ID, Riddle.NO_ID); } private void saveHighestUsedId() { if (mPrefs.getLong(KEY_HIGHEST_USED_ID, Riddle.NO_ID) < mHighestUsedId) { mPrefs.edit().putLong(KEY_HIGHEST_USED_ID, mHighestUsedId).apply(); } } public void checkedIdsInit(Context context) { if (mPrefs == null && context != null) { mPrefs = context.getSharedPreferences(IMPORTANT_PREFERENCES_FILE, Context.MODE_PRIVATE); mHighestUsedId = loadHighestUsedId(); } } /** * Unregisters previously registered listeners. * @param listener The listener to unregister. Does nothing if null. */ public void unregisterInitProgressListener(InitProgressListener listener) { if ( listener != null) { mListener.remove(listener); } } /** * Cancels and finishes previously running initialization tasks. */ public void cancelInit() { if (mInitTask != null) { mInitTask.cancel(true); } onInitFinish(); } /** * Returns true if the RiddleInitializer is currently working and not yet finished. * @return If initializing is still in progress and not yet completed. */ public boolean isInitializing() { return mInitTask != null; } /** * Cancels previous started initialization and starts a new initialization task. * @param context The context. * @param listener The listener. */ public void init(@NonNull final Context context, @NonNull InitProgressListener listener) { cancelInit(); mListener.add(listener); mInitTask = new InitTask(context); mInitTask.execute(); } /** * The progress and callback interface for the RiddleInitializer. */ public interface InitProgressListener extends PercentProgressListener { void onInitComplete(); } /** * Returns a copy of the map of used images for each riddle type. * This is a deep copy meaning that the sets contained in the map are not backed by the initializer * can be changed freely. * @return A copy of the image hashes used by the riddle types. Can be empty. */ Map<RiddleType, Set<String>> makeUsedImagesCopy() { // required because we might run into concurrency issues if an image gets added while we are still iterating over the list when making a new riddle Map<RiddleType, Set<String>> usedImagesForType = new HashMap<>(); for (RiddleType type : mUsedImagesForType.keySet()) { usedImagesForType.put(type, new HashSet<>(mUsedImagesForType.get(type))); // make a copy of the Set<String>s } return usedImagesForType; } }