/*
* 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.achievement;
import android.text.TextUtils;
import android.util.Log;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import dan.dit.whatsthat.util.compaction.Compactable;
import dan.dit.whatsthat.util.compaction.CompactedDataCorruptException;
import dan.dit.whatsthat.util.compaction.Compacter;
/**
* A class that times data that an Achievement received. Data is kept in a TimeKeeper and can
* be accessed by a unique (for an AchievementDataTimer instance) key. A TimeKeeper holds timestamps
* that mark the system times when the TimeKeeper was updated and can hold a duration supplied by the client.
* The amount of these timestamps and durations hold is fixed and the oldest one will be discarded if
* new ones are added and the TimeKeeper is already full.<br>
* So this can be used to remember certain timestamps (like completion) for events that happened. Additionally you can
* remember the durations for the events and finally check if a certain amount of these events were within a defined
* time span. The meaning of "within" is here by defined differently, as the maximum spent time or only
* the durations for (some) events might be of interest.
* Created by daniel on 14.05.15.
*/
public class AchievementDataTimer extends AchievementData {
private Map<String, TimeKeeper> mTimeKeepers = new HashMap<>();
private AchievementDataEvent mEvent = new AchievementDataEvent();
/**
* Loads an AchievementDataTimer with the given data name.
* @param dataName The AchievementData name.
* @param compactedData The comapted data. Can be null to create a new instance.
* @throws CompactedDataCorruptException If there was an error reading the data.
*/
protected AchievementDataTimer(String dataName, Compacter compactedData) throws CompactedDataCorruptException {
super(dataName);
unloadData(compactedData);
}
/**
* Creates a new AchievementDataTimer with the given data name.
* @param dataName The AchievementData name.
*/
protected AchievementDataTimer(String dataName) {
super(dataName);
}
@Override
protected synchronized void resetData() {
mTimeKeepers.clear();
notifyListeners(mEvent.initReset(this));
}
@Override
public String compact() {
Compacter cmp = new Compacter(mTimeKeepers.size());
for (String key : mTimeKeepers.keySet()) {
cmp.appendData(mTimeKeepers.get(key).compact());
}
return cmp.compact();
}
@Override
public void unloadData(Compacter compactedData) throws CompactedDataCorruptException {
if (compactedData == null) {
return;
}
for (int i = 0; i < compactedData.getSize(); i++) {
TimeKeeper timestamp = null;
try {
timestamp = new TimeKeeper(new Compacter(compactedData.getData(i)));
} catch (CompactedDataCorruptException e) {
Log.e("Achievement", "Compacted timestamp corrupt: " + e);
}
if (timestamp != null) {
mTimeKeepers.put(timestamp.mKey, timestamp);
}
}
}
/**
* Creates a new TimeKeeper for a key with a fixed amount of timestamps it will hold.
* Does nothing if a parameter is illegal.
* @param key The key for the new TimeKeeper.
* @param amount The positive amount of timestamps to hold.
*/
private synchronized void newTimeKeeper(String key, int amount) {
if (key == null || amount <= 0) {
return;
}
mTimeKeepers.put(key, new TimeKeeper(key, amount));
}
/**
* Ensures that there is a TimeKeeper for the given key with the given amount. Will not create a
* new TimeKeeper if there is one already with the same amount.
* @param key The key for the TimeKeeper.
* @param amount The positive amount of timestamps to hold.
* @return True only if a new TimeKeeper was created.
*/
public synchronized boolean ensureTimeKeeper(String key, int amount) {
if (key == null || amount <= 0) {
return false;
}
if (!mTimeKeepers.containsKey(key) || mTimeKeepers.get(key).mTimestamps.length != amount) {
newTimeKeeper(key, amount);
return true;
}
return false;
}
/**
* Removes the TimeKeeper for a key. Does nothing if key illegal or no TimeKeeper with
* this key available.
* @param key The key to remove the TimeKeeper for.
*/
public void removeTimerKeeper(String key) {
if (key == null) {
return;
}
mTimeKeepers.remove(key);
}
/**
* The TimeKeeper will be notified to update, using the current execution time
* as a timestamp and the given duration as the a duration.
* @param key The key to identify the TimeKeeper. Does nothing if key illegal or no TimeKeeper found.
* @param duration The duration for this timed data. Will use 0 if negative.
*/
public synchronized void onTimeKeeperUpdate(String key, long duration) {
if (key == null) {
return;
}
TimeKeeper timeKeeper = mTimeKeepers.get(key);
if (timeKeeper != null) {
timeKeeper.update(duration);
notifyListeners(mEvent.init(this, AchievementDataEvent.EVENT_TYPE_DATA_UPDATE, key));
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{!");
for (String key : mTimeKeepers.keySet()) {
builder.append(mTimeKeepers.get(key).toString()).append("!");
}
builder.append("}");
return builder.toString();
}
/**
* Returns the TimeKeeper identified by the given key.
* @param key The key for the TimeKeeper.
* @return The TimeKeeper.
*/
public TimeKeeper getTimeKeeper(String key) {
if (key == null) {
return null;
}
return mTimeKeepers.get(key);
}
/**
* The TimeKeeper class managed by the AchievementDataTimer.
*/
public static class TimeKeeper implements Compactable {
private long[] mTimestamps;
private long[] mDuration;
private String mKey;
private int mCurrIndex;
private TimeKeeper(String key, int count) {
if (count <= 0) {
throw new IllegalArgumentException("Illegal count: " + count);
}
if (TextUtils.isEmpty(key)) {
throw new IllegalArgumentException("Illegal key: " + key);
}
mKey = key;
mTimestamps = new long[count];
mDuration = new long[count];
mCurrIndex = 0;
}
private TimeKeeper(Compacter data) throws CompactedDataCorruptException {
unloadData(data);
}
@Override
public String compact() {
Compacter cmp = new Compacter(1 + 2 * mTimestamps.length);
cmp.appendData(mKey);
cmp.appendData(mTimestamps.length);
for (int i = 0; i < mTimestamps.length; i++) {
cmp.appendData(mTimestamps[i]);
cmp.appendData(mDuration[i]);
}
return cmp.compact();
}
@Override
public void unloadData(Compacter compactedData) throws CompactedDataCorruptException {
if (compactedData == null || compactedData.getSize() < 3) {
throw new CompactedDataCorruptException("Timestamp data missing").setCorruptData(compactedData);
}
mKey = compactedData.getData(0);
int length = compactedData.getInt(1);
if (length <= 0) {
throw new CompactedDataCorruptException("Null or negative length.").setCorruptData(compactedData);
}
mTimestamps = new long[length];
mDuration = new long[length];
for (int i = 0; i < length && 2 + 2 * i + 1 < compactedData.getSize(); i++) {
mTimestamps[i] = compactedData.getLong(2 + 2 * i);
mDuration[i] = compactedData.getLong(2 + 2 * i + 1);
mCurrIndex = i;
}
mCurrIndex++;
}
@Override
public String toString() {
return mKey + ": " + Arrays.toString(mTimestamps) + "|" + Arrays.toString(mDuration);
}
private void update(long duration) {
if (mCurrIndex == mTimestamps.length) {
// end reached, rotate data left
for (int i = 1; i < mTimestamps.length; i++) {
mTimestamps[i - 1] = mTimestamps[i];
mDuration[i - 1] = mDuration[i];
}
mCurrIndex--;
}
if (mCurrIndex < mTimestamps.length) {
// more space to add data
mTimestamps[mCurrIndex] = System.currentTimeMillis();
mDuration[mCurrIndex] = duration >= 0L ? duration : 0L;
}
mCurrIndex++;
}
/**
* Returns the sum of all provided durations, ignoring the timestamps.
* @return The sum of all durations.
*/
public long sumDurations() {
long duration = 0L;
for (int i = 0; i < mDuration.length; i++) {
duration += mDuration[i];
}
return duration;
}
/**
* Returns the total time consumed by this TimeKeeper. This is defined
* as the sum of the greater value of the times between two timestamps and the duration for
* the event. So this is useful if the events are not strictly sequential but the durations
* can overlap the timestamps (for example when timestamps mark completion points and multiple
* data events can be started at the same time).
* @return The total time consumed for the events.
*/
public long getTotalTimeConsumed() {
long time = 0L;
for (int i = 0; i < mDuration.length; i++) {
if (i == 0) {
time += mDuration[i];
} else {
time += Math.max(mDuration[i], mTimestamps[i] - mTimestamps[i - 1]);
}
}
return time;
}
/**
* Returns the amount of event times saved by the TimeKeeper. Is smaller
* than the initial capacity and greater than or equal to zero.
* @return The amount of update events received.
*/
public int getTimesCount() {
return mCurrIndex;
}
}
}