package org.geogebra.common.main; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.geogebra.common.kernel.commands.CmdGetTime; import org.geogebra.common.move.ggtapi.models.Chapter; import org.geogebra.common.move.ggtapi.models.Material; import org.geogebra.common.move.ggtapi.models.SyncEvent; import org.geogebra.common.move.ggtapi.requests.MaterialCallbackI; import org.geogebra.common.util.debug.Log; import org.geogebra.common.util.lang.Unicode; public abstract class MaterialsManager implements MaterialsManagerI { /** prefix for autosave items in storage */ public static final String AUTO_SAVE_KEY = "autosave"; /** prefix for files in storage */ public static final String FILE_PREFIX = "file_"; /** characters not allowed in filename */ public static final String reservedCharacters = "*/:<>?\\|+,.;=[]"; private int notSyncedFileCount; /** files waiting for download */ int notDownloadedFileCount; /** * @param matID * local ID of material * @param title * of material * @return creates a key (String) for the stockStore */ public static String createKeyString(int matID, String title) { StringBuilder sb = new StringBuilder(title.length() + 12); sb.append(FILE_PREFIX); sb.append(matID); sb.append('_'); appendTitleWithoutReservedCharacters(title, sb); return sb.toString(); } /** * Remove all reserved characters from a ggb file title * * @param title * title for ggb file * @return title without reserved characters */ public static String getTitleWithoutReservedCharacters(String title) { StringBuilder sb = new StringBuilder(title.length()); appendTitleWithoutReservedCharacters(title, sb); return sb.toString(); } private static void appendTitleWithoutReservedCharacters(String title, StringBuilder sb) { for (int i = 0; i < title.length(); i++) { if (reservedCharacters.indexOf(title.charAt(i)) == -1) { sb.append(title.charAt(i)); } } } /** * @param mat * material * @return storage key based on id and title */ public static String getFileKey(Material mat) { return createKeyString(mat.getLocalID(), mat.getTitle()); } /** * returns the ID from the given key. (key is of form "file_ID_fileName") * * @param key * String * @return int ID */ public static int getIDFromKey(String key) { return Integer.parseInt(key.substring(FILE_PREFIX.length(), key.indexOf("_", FILE_PREFIX.length()))); } /** * key is of form "file_ID_title" * * @param key * file key * @return the title */ public static String getTitleFromKey(String key) { return key.substring(key.indexOf("_", key.indexOf("_") + 1) + 1); } /** * uploads the material and removes it from localStorage * * @param mat * {@link Material} */ public void upload(final Material mat) { final String localKey = getFileKey(mat); mat.setTitle(getTitleFromKey(mat.getTitle())); getApp().getLoginOperation().getGeoGebraTubeAPI().uploadLocalMaterial( mat, new MaterialCallbackI() { @Override public void onLoaded(final List<Material> parseResponse, ArrayList<Chapter> meta) { if (parseResponse.size() == 1) { mat.setTitle(getTitleFromKey(mat.getTitle())); mat.setLocalID( MaterialsManager.getIDFromKey(localKey)); final Material newMat = parseResponse.get(0); if (mat.thumbnailIsBase64()) { newMat.setThumbnailBase64(mat.getThumbnail()); } else { newMat.setThumbnailUrl(mat.getThumbnail()); } newMat.setSyncStamp(newMat.getModified()); if (!MaterialsManager.this .shouldKeep(mat.getId())) { delete(mat, true, new Runnable() { @Override public void run() { // TODO Auto-generated method stub } }); } else { // Meta may have changed (tube ID), sync // timestamp needs changing always MaterialsManager.this.setTubeID(localKey, newMat); } // TODO moved out of refresh material; do we need it // twice (see above)? newMat.setSyncStamp(newMat.getModified()); refreshMaterial(newMat); } } @Override public void onError(final Throwable exception) { // TODO } }); } /** * Synchronize the material and mark corresponding event as resolved * * @param mat * material * @param events * sync events */ public void sync(final Material mat, ArrayList<SyncEvent> events) { if (mat.getId() == 0) { upload(mat); } else { for (SyncEvent event : events) { if (event.getID() == mat.getId()) { sync(mat, event); event.setZapped(true); break; } } } this.notSyncedFileCount--; Log.debug("SYNC remains " + this.notSyncedFileCount); checkMaterialsToDownload(events); sync(mat, new SyncEvent(0, 0)); } /** * @param count * number of files waiting for sync * @param events * sync events */ public void setNotSyncedFileCount(int count, ArrayList<SyncEvent> events) { this.notSyncedFileCount = count; checkMaterialsToDownload(events); } /** * @param events * sync events */ public void ignoreNotSyncedFile(ArrayList<SyncEvent> events) { this.notSyncedFileCount--; checkMaterialsToDownload(events); } @Override public void getFromTube(final int id, final boolean fromAnotherDevice) { getApp().getLoginOperation().getGeoGebraTubeAPI().getItem(id + "", new MaterialCallbackI() { @Override public void onLoaded(final List<Material> parseResponse, ArrayList<Chapter> meta) { MaterialsManager.this.notDownloadedFileCount--; // edited on Tube, not edited locally if (parseResponse.size() == 1) { Log.debug("SYNC downloading file:" + id); Material tubeMat = parseResponse.get(0); tubeMat.setSyncStamp(tubeMat.getModified()); tubeMat.setFromAnotherDevice(fromAnotherDevice); MaterialsManager.this.updateFile(null, tubeMat.getModified(), tubeMat); } } @Override public void onError(final Throwable exception) { MaterialsManager.this.notDownloadedFileCount--; Log.debug("SYNC error loading from tube" + id); } }); } private void getFromTube(final Material mat) { getApp().getLoginOperation().getGeoGebraTubeAPI() .getItem(mat.getId() + "", new MaterialCallbackI() { @Override public void onLoaded(final List<Material> parseResponse, ArrayList<Chapter> meta) { // edited on Tube, not edited locally if (mat.getModified() <= mat.getSyncStamp()) { Log.debug("SYNC incomming changes:" + mat.getId()); MaterialsManager.this.updateFile(getFileKey(mat), parseResponse.get(0).getModified(), parseResponse.get(0)); } } @Override public void onError(final Throwable exception) { Log.debug("SYNC error loading from tube" + mat.getId()); } }); } /** * Update loacl copy * * @param title * new title * @param modified * timestamp * @param material * material */ protected abstract void updateFile(String title, long modified, Material material); @Override public boolean isSyncing() { return this.notDownloadedFileCount > 0 || this.notSyncedFileCount > 0; } private void checkMaterialsToDownload(ArrayList<SyncEvent> events) { if (notSyncedFileCount == 0) { for (SyncEvent event : events) { if (event.isFavorite() && !event.isZapped() && this.shouldKeep(event.getID())) { this.notDownloadedFileCount++; getFromTube(event.getID(), true); } } } } private void deleteFromTube(final Material mat, final Runnable onDelete) { if (!getApp().getLoginOperation().owns(mat)) { delete(mat, true, onDelete); return; } getApp().getLoginOperation().getGeoGebraTubeAPI().deleteMaterial(mat, new MaterialCallbackI() { @Override public void onLoaded(final List<Material> parseResponse, ArrayList<Chapter> meta) { // edited on Tube, not edited locally delete(mat, true, onDelete); } @Override public void onError(final Throwable exception) { Log.debug( "SYNC error deleting from tube" + mat.getId()); } }); } private void sync(final Material mat, SyncEvent event) { long tubeTimestamp = event.getTimestamp(); Runnable dummyCallback = new Runnable() { @Override public void run() { // TODO Auto-generated method stub } }; // First check for conflict if (mat.getSyncStamp() < mat.getModified() && (tubeTimestamp != 0 && tubeTimestamp > mat.getSyncStamp())) { fork(mat); return; } if (event.isDelete()) { delete(mat, true, dummyCallback); } else if (event.isUnfavorite() && mat.isFromAnotherDevice()) { // remove from local device delete(mat, true, dummyCallback); } else if (tubeTimestamp != 0 && tubeTimestamp > mat.getSyncStamp()) { getFromTube(mat); } else { // no changes in Tube if (mat.isDeleted()) { Log.debug("SYNC outgoing delete:" + mat.getId()); deleteFromTube(mat, dummyCallback); } else if (mat.getId() > 0 && mat.getModified() <= mat.getSyncStamp()) { Log.debug("SYNC material up to date" + mat.getId()); } else { Log.debug("SYNC outgoing changes:" + mat.getId()); upload(mat); } } } private void fork(final Material mat) { showTooltip(mat); Log.debug("SYNC fork: " + mat.getId() + "," + mat.getSyncStamp() + "," + mat.getTimestamp()); final String format = getApp().getLocalization() .isRightToLeftReadingOrder() ? "\\Y " + Unicode.LeftToRightMark + "\\F" + Unicode.LeftToRightMark + " \\j" : "\\j \\F \\Y"; String newTitle = mat.getTitle(); // make sure there's room to add the date String suffix = " (" + CmdGetTime.buildLocalizedDate(format, new Date(), getApp().getLocalization()) + ")"; if (newTitle.length() + suffix.length() > 60) { newTitle = newTitle.substring(0, 60 - suffix.length()); } // put date on end so the filename is different for the fork newTitle = newTitle + suffix; final String newTitle2 = newTitle; this.rename(newTitle2, mat, new Runnable() { @Override public void run() { mat.setTitle(newTitle2); mat.setId(0); upload(mat); } }); } protected abstract void showTooltip(Material mat); protected abstract App getApp(); protected abstract void refreshMaterial(Material newMat); protected abstract void setTubeID(String localKey, Material newMat); }