package yuku.alkitab.base.util;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.util.Log;
import yuku.afw.storage.Preferences;
import yuku.alkitab.base.App;
import yuku.alkitab.base.S;
import yuku.alkitab.base.U;
import yuku.alkitab.base.model.SyncShadow;
import yuku.alkitab.base.storage.Prefkey;
import yuku.alkitab.base.sync.Sync;
import yuku.alkitab.base.sync.Sync_History;
import yuku.alkitab.debug.BuildConfig;
import yuku.alkitab.model.util.Gid;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
public class History {
static final String TAG = History.class.getSimpleName();
private static final int MAX_HISTORY_ENTRIES = 20;
public static class HistoryEntry {
public String gid;
public int ari;
public long timestamp;
public String creator_id;
public static HistoryEntry createEmptyEntry() {
return new HistoryEntry();
}
}
static class HistoryJson {
public List<HistoryEntry> entries;
}
final List<HistoryEntry> entries;
private static History instance;
public static History getInstance() {
if (instance == null) {
instance = new History();
}
return instance;
}
private History() {
entries = new ArrayList<>();
final String s = Preferences.getString(Prefkey.history);
if (s != null) {
final HistoryJson obj = App.getDefaultGson().fromJson(s, HistoryJson.class);
entries.addAll(obj.entries);
}
}
public synchronized void save() {
final HistoryJson obj = new HistoryJson();
obj.entries = this.entries;
final String new_json = App.getDefaultGson().toJson(obj);
final String old_json = Preferences.getString(Prefkey.history);
if (!U.equals(old_json, new_json)) {
Preferences.setString(Prefkey.history, new_json);
Sync.notifySyncNeeded(SyncShadow.SYNC_SET_HISTORY);
} else {
Log.d(TAG, "History not changed.");
}
}
public synchronized void add(int ari) {
add(ari, System.currentTimeMillis());
}
synchronized void add(int ari, long timestamp) {
// check: do we have this previously?
for (int i = entries.size() - 1; i >= 0; i--) {
final HistoryEntry entry = entries.get(i);
if (entry.ari == ari) {
// YES. Remove this.
entries.remove(i);
}
}
// Add it to the front
final HistoryEntry entry = new HistoryEntry();
entry.gid = Gid.newGid();
entry.ari = ari;
entry.timestamp = timestamp;
entry.creator_id = U.getInstallationId();
entries.add(0, entry);
// and remove if overflow
while (entries.size() > MAX_HISTORY_ENTRIES) {
entries.remove(MAX_HISTORY_ENTRIES);
}
}
public synchronized int getSize() {
return entries.size();
}
public synchronized int getAri(final int position) {
return entries.get(position).ari;
}
public synchronized long getTimestamp(final int position) {
return entries.get(position).timestamp;
}
public synchronized String getCreatorId(final int position) {
return entries.get(position).creator_id;
}
public List<HistoryEntry> listAllEntries() {
return new ArrayList<>(entries);
}
/**
* Makes the current history updated with patches (append delta) from server.
* Also updates the shadow (both data and the revno).
* @return {@link yuku.alkitab.base.sync.Sync.ApplyAppendDeltaResult#ok} if history and sync shadow are updated. Otherwise else.
*/
@NonNull public Sync.ApplyAppendDeltaResult applyHistoryAppendDelta(final int final_revno, @NonNull final Sync.Delta<Sync_History.Content> append_delta, @NonNull final List<Sync.Entity<Sync_History.Content>> entitiesBeforeSync, @NonNull final String simpleTokenBeforeSync) {
final ArrayList<HistoryEntry> entriesCopy = new ArrayList<>(entries);
Sync.notifySyncUpdatesOngoing(SyncShadow.SYNC_SET_HISTORY, true);
try {
{ // if the current entities are not the same as the ones had when contacting server, reject this append delta.
final List<Sync.Entity<Sync_History.Content>> currentEntities = Sync_History.getEntitiesFromCurrent();
if (!Sync.entitiesEqual(currentEntities, entitiesBeforeSync)) {
return Sync.ApplyAppendDeltaResult.dirty_entities;
}
}
{ // if the current simpleToken has changed (sync user logged off or changed), reject this append delta
final String simpleToken = Preferences.getString(Prefkey.sync_simpleToken);
if (!U.equals(simpleToken, simpleTokenBeforeSync)) {
return Sync.ApplyAppendDeltaResult.dirty_sync_account;
}
}
for (final Sync.Operation<Sync_History.Content> o : append_delta.operations) {
switch (o.opkind) {
case del:
deleteByGid(entriesCopy, o.gid);
break;
case add:
case mod:
addOrModByGid(entriesCopy, o.gid, o.content, o.creator_id);
break;
}
}
// sort by timestamp desc
Collections.sort(entriesCopy, (a, b) -> (a.timestamp < b.timestamp) ? +1 : ((a.timestamp > b.timestamp) ? -1 : 0));
// commit changes
this.entries.clear();
this.entries.addAll(new ArrayList<>(entriesCopy));
// if we reach here, the local database has been updated with the append delta.
final SyncShadow ss = Sync_History.shadowFromEntities(Sync_History.getEntitiesFromCurrent(), final_revno);
S.getDb().insertOrUpdateSyncShadowBySyncSetName(ss);
this.save();
// when debugging, print
if (BuildConfig.DEBUG) {
Log.d(TAG, "After sync, the history entries are:");
Log.d(TAG, String.format(" ari ==== timestamp =============== %-40s %-40s", "gid", "creator_id"));
for (final HistoryEntry entry : entries) {
Log.d(TAG, String.format("- 0x%06x %tF %<tT %<tz %-40s %-40s", entry.ari, entry.timestamp, entry.gid, entry.creator_id));
}
}
return Sync.ApplyAppendDeltaResult.ok;
} finally {
Sync.notifySyncUpdatesOngoing(SyncShadow.SYNC_SET_HISTORY, false);
}
}
private static void deleteByGid(@NonNull final ArrayList<HistoryEntry> entries, @NonNull final String gid) {
for (int i = 0; i < entries.size(); i++) {
final HistoryEntry entry = entries.get(i);
if (U.equals(entry.gid, gid)) {
// delete and exit
entries.remove(i);
return;
}
}
}
private static void addOrModByGid(@NonNull final ArrayList<HistoryEntry> entries, @NonNull final String gid, @NonNull final Sync_History.Content content, @NonNull final String creator_id) {
for (int i = 0; i < entries.size(); i++) {
final HistoryEntry entry = entries.get(i);
if (U.equals(entry.gid, gid)) {
// update!
entry.ari = content.ari;
entry.timestamp = content.timestamp;
entry.creator_id = creator_id;
return;
}
}
// not found, create new one
final HistoryEntry entry = HistoryEntry.createEmptyEntry();
entry.gid = gid;
entry.ari = content.ari;
entry.timestamp = content.timestamp;
entry.creator_id = creator_id;
entries.add(entry);
}
public static void migrateOldHistoryWhenNeeded() {
if (OldHistoryMigrator.needsMigration()) {
OldHistoryMigrator.migrate();
}
}
static class OldHistoryMigrator {
private static final String HISTORY_PREFIX = "sejarah/";
private static final String FIELD_SEPARATOR_STRING = ":";
private static final Pattern FIELD_SEPARATOR_PATTERN = Pattern.compile(FIELD_SEPARATOR_STRING);
static class ClientHistoryEntry {
public int ari;
public long timestamp;
public boolean savedInServer;
}
static List<ClientHistoryEntry> load() {
final List<ClientHistoryEntry> entries = new ArrayList<>();
// instant preferences
final SharedPreferences preferences = App.context.getSharedPreferences(App.context.getPackageName(), 0);
int n = preferences.getInt(HISTORY_PREFIX + "n", 0);
final Map<String, ?> all = preferences.getAll();
for (int i = n - 1; i >= 0; i--) {
final ClientHistoryEntry entry = new ClientHistoryEntry();
final Object val = all.get(HISTORY_PREFIX + i);
if (val instanceof Integer) {
// for compatibility when upgrading from older version without sync and timestamp support
entry.ari = (Integer) val;
entry.savedInServer = false;
entry.timestamp = System.currentTimeMillis();
} else if (val instanceof String) {
// v1:ari:timestamp:(int)savedinserver
final String[] splits = FIELD_SEPARATOR_PATTERN.split((String) val);
entry.ari = Integer.parseInt(splits[1]);
entry.timestamp = Long.parseLong(splits[2]);
entry.savedInServer = Integer.parseInt(splits[3]) != 0;
}
entries.add(entry);
}
return entries;
}
static void deleteAll() {
// instant preferences
final SharedPreferences preferences = App.context.getSharedPreferences(App.context.getPackageName(), 0);
int n = preferences.getInt(HISTORY_PREFIX + "n", 0);
final SharedPreferences.Editor editor = preferences.edit();
for (int i = 0; i < n; i++) {
editor.remove(HISTORY_PREFIX + i);
}
editor.remove(HISTORY_PREFIX + "n");
editor.apply();
}
static boolean needsMigration() {
// to prevent accessing/creating the instant preferences, we check the default preferences instead.
return !Preferences.contains(Prefkey.history);
}
static void migrate() {
try {
final History history = History.getInstance();
final List<ClientHistoryEntry> entries = load();
for (final ClientHistoryEntry entry : entries) {
history.add(entry.ari, entry.timestamp);
}
deleteAll();
history.save();
} catch (Exception e) {
Log.e(TAG, "Error when migrating history", e);
}
}
}
}