/* * Catroid: An on-device visual programming system for Android devices * Copyright (C) 2010-2016 The Catrobat Team * (<http://developer.catrobat.org/credits>) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * An additional term exception under section 7 of the GNU Affero * General Public License, version 3, is available at * http://developer.catrobat.org/license_additional_term * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.catrobat.catroid.scratchconverter; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import com.google.android.gms.common.images.WebImage; import com.google.common.base.Preconditions; import org.catrobat.catroid.R; import org.catrobat.catroid.common.Constants; import org.catrobat.catroid.scratchconverter.protocol.Job; import org.catrobat.catroid.ui.MainMenuActivity; import org.catrobat.catroid.ui.dialogs.CustomAlertDialogBuilder; import org.catrobat.catroid.ui.dialogs.ScratchReconvertDialog; import org.catrobat.catroid.ui.scratchconverter.BaseInfoViewListener; import org.catrobat.catroid.ui.scratchconverter.JobViewListener; import org.catrobat.catroid.utils.DownloadUtil; import org.catrobat.catroid.utils.ToastUtil; import org.catrobat.catroid.utils.Utils; import org.json.JSONException; import org.json.JSONObject; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; public class ScratchConversionManager implements ConversionManager { private static final String TAG = ScratchConversionManager.class.getSimpleName(); private Activity currentActivity; private final Client client; private final boolean verbose; private Map<String, Client.DownloadCallback> downloadCallbacks; private Set<Client.DownloadCallback> globalDownloadCallbacks; private Map<Long, Set<JobViewListener>> jobViewListeners; private Set<JobViewListener> globalJobViewListeners; private Set<BaseInfoViewListener> baseInfoViewListeners; private boolean shutdown; @SuppressLint("UseSparseArrays") public ScratchConversionManager(final Activity rootActivity, final Client client, final boolean verbose) { this.currentActivity = rootActivity; this.client = client; this.verbose = verbose; this.downloadCallbacks = new HashMap<>(); this.globalDownloadCallbacks = Collections.synchronizedSet(new HashSet<Client.DownloadCallback>()); client.setConvertCallback(this); this.jobViewListeners = Collections.synchronizedMap(new HashMap<Long, Set<JobViewListener>>()); this.globalJobViewListeners = Collections.synchronizedSet(new HashSet<JobViewListener>()); this.baseInfoViewListeners = Collections.synchronizedSet(new HashSet<BaseInfoViewListener>()); this.shutdown = false; DownloadUtil.getInstance().setDownloadCallback(this); } @Override public void setCurrentActivity(final Activity activity) { currentActivity = activity; } @Override public void addGlobalDownloadCallback(final Client.DownloadCallback callback) { globalDownloadCallbacks.add(callback); } @Override public boolean removeGlobalDownloadCallback(final Client.DownloadCallback callback) { return globalDownloadCallbacks.remove(callback); } @Override public boolean isJobInProgress(long jobID) { return client.isJobInProgress(jobID); } @Override public boolean isJobDownloading(long jobID) { return readDownloadStateFromDisk(jobID) == Job.DownloadState.DOWNLOADING; } @Override public int getNumberOfJobsInProgress() { return client.getNumberOfJobsInProgress(); } @Override public void connectAndAuthenticate() { client.connectAndAuthenticate(this); } @Override public void shutdown() { shutdown = true; DownloadUtil.getInstance().setDownloadCallback(null); if (!client.isClosed()) { client.close(); } } @Override public void convertProgram(final long jobID, final String title, final WebImage image, final boolean force) { updateDownloadStateOnDisk(jobID, Job.DownloadState.NOT_READY); client.convertProgram(jobID, title, image, verbose, force); } private void closeAllActivities() { if (!shutdown) { Intent intent = new Intent(currentActivity.getApplicationContext(), MainMenuActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); currentActivity.startActivity(intent); } } // ----------------------------------------------------------------------------------------------------------------- // ConnectAuthCallback // ----------------------------------------------------------------------------------------------------------------- @Override public void onSuccess(long clientID) { SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(currentActivity.getApplicationContext()); SharedPreferences.Editor editor = settings.edit(); editor.putLong(Constants.SCRATCH_CONVERTER_CLIENT_ID_SHARED_PREFERENCE_NAME, clientID); editor.commit(); Log.i(TAG, "Connection established (clientID: " + clientID + ")"); Preconditions.checkState(client.isAuthenticated()); client.retrieveInfo(); } @Override public void onConnectionClosed(ClientException ex) { Log.d(TAG, "Connection closed!"); final String exceptionMessage = ex.getMessage(); currentActivity.runOnUiThread(new Runnable() { @Override public void run() { if (exceptionMessage != null) { Log.e(TAG, exceptionMessage); } if (!shutdown) { ToastUtil.showError(currentActivity, R.string.connection_lost_or_closed_by_server); } closeAllActivities(); } }); } @Override public void onConnectionFailure(final ClientException ex) { Log.e(TAG, ex.getMessage()); currentActivity.runOnUiThread(new Runnable() { @Override public void run() { ToastUtil.showError(currentActivity, R.string.connection_failed); closeAllActivities(); } }); } @Override public void onAuthenticationFailure(final ClientException ex) { Log.e(TAG, ex.getMessage()); currentActivity.runOnUiThread(new Runnable() { @Override public void run() { ToastUtil.showError(currentActivity, R.string.authentication_failed); closeAllActivities(); } }); } // ----------------------------------------------------------------------------------------------------------------- // ConversionManager interface // ----------------------------------------------------------------------------------------------------------------- @Override public void addBaseInfoViewListener(BaseInfoViewListener baseInfoViewListener) { baseInfoViewListeners.add(baseInfoViewListener); } @Override public boolean removeBaseInfoViewListener(BaseInfoViewListener baseInfoViewListener) { return baseInfoViewListeners.remove(baseInfoViewListener); } @Override public void addGlobalJobViewListener(JobViewListener jobViewListener) { globalJobViewListeners.add(jobViewListener); } @Override public boolean removeGlobalJobViewListener(JobViewListener jobViewListener) { return globalJobViewListeners.remove(jobViewListener); } @Override public void addJobViewListener(long jobID, JobViewListener jobViewListener) { Set<JobViewListener> listeners = jobViewListeners.get(jobID); if (listeners == null) { listeners = new HashSet<>(); } listeners.add(jobViewListener); jobViewListeners.put(jobID, listeners); } @Override public boolean removeJobViewListener(long jobID, JobViewListener jobViewListener) { Set<JobViewListener> listeners = jobViewListeners.get(jobID); return listeners != null && listeners.remove(jobViewListener); } @NonNull private JobViewListener[] getJobViewListeners(long jobID) { final Set<JobViewListener> mergedListenersList = new HashSet<>(); final Set<JobViewListener> listenersList = jobViewListeners.get(jobID); if (listenersList != null) { mergedListenersList.addAll(listenersList); } mergedListenersList.addAll(globalJobViewListeners); return mergedListenersList.toArray(new JobViewListener[mergedListenersList.size()]); } // ----------------------------------------------------------------------------------------------------------------- // ConvertCallback // ----------------------------------------------------------------------------------------------------------------- @Override public void onInfo(final float supportedCatrobatLanguageVersion, final Job[] jobs) { for (Job job : jobs) { job.setDownloadState(readDownloadStateFromDisk(job.getJobID())); } currentActivity.runOnUiThread(new Runnable() { @Override public void run() { Log.i(TAG, "Supported Catrobat Language version: " + supportedCatrobatLanguageVersion); for (BaseInfoViewListener viewListener : baseInfoViewListeners) { viewListener.onJobsInfo(jobs); } if (Constants.CURRENT_CATROBAT_LANGUAGE_VERSION < supportedCatrobatLanguageVersion) { AlertDialog.Builder builder = new CustomAlertDialogBuilder(currentActivity); builder.setTitle(R.string.warning); builder.setMessage(R.string.error_scratch_converter_outdated_pocketcode_version); builder.setNeutralButton(R.string.close, null); Dialog errorDialog = builder.create(); errorDialog.show(); } } }); } @Override public void onJobScheduled(final Job job) { currentActivity.runOnUiThread(new Runnable() { @Override public void run() { for (JobViewListener viewListener : getJobViewListeners(job.getJobID())) { viewListener.onJobScheduled(job); } } }); } @Override public void onConversionReady(final Job job) { Log.i(TAG, "Conversion ready!"); currentActivity.runOnUiThread(new Runnable() { @Override public void run() { for (JobViewListener viewListener : getJobViewListeners(job.getJobID())) { viewListener.onJobReady(job); } } }); } @Override public void onConversionStart(final Job job) { // Note: this callback-method is not called on UI-thread Log.i(TAG, "Conversion started!"); currentActivity.runOnUiThread(new Runnable() { @Override public void run() { ToastUtil.showSuccess(currentActivity, currentActivity.getString(R.string.scratch_conversion_started)); for (JobViewListener viewListener : getJobViewListeners(job.getJobID())) { viewListener.onJobStarted(job); } } }); } @Override public void onConversionFinished(final Job job, final Client.DownloadCallback downloadCallback, final String downloadURL, final Date cachedUTCDate) { Log.i(TAG, "Conversion finished!"); updateDownloadStateOnDisk(job.getJobID(), Job.DownloadState.READY); conversionFinished(job, downloadCallback, downloadURL, cachedUTCDate); } @Override public void onConversionAlreadyFinished(Job job, Client.DownloadCallback downloadCallback, String downloadURL) { if (readDownloadStateFromDisk(job.getJobID()) == Job.DownloadState.NOT_READY) { updateDownloadStateOnDisk(job.getJobID(), Job.DownloadState.READY); } conversionFinished(job, downloadCallback, downloadURL, null); } private void conversionFinished(final Job job, final Client.DownloadCallback downloadCallback, final String downloadURL, final Date cachedUTCDate) { final String baseUrl = Constants.SCRATCH_CONVERTER_BASE_URL; final String fullDownloadURL = baseUrl.substring(0, baseUrl.length() - 1) + downloadURL; Job.DownloadState localDownloadState = readDownloadStateFromDisk(job.getJobID()); if (localDownloadState != Job.DownloadState.READY && localDownloadState != Job.DownloadState.DOWNLOADING) { return; } final Job.DownloadState finalLocalDownloadState = localDownloadState; currentActivity.runOnUiThread(new Runnable() { @Override public void run() { for (JobViewListener viewListener : getJobViewListeners(job.getJobID())) { viewListener.onJobFinished(job); } downloadCallbacks.put(downloadURL, downloadCallback); if (finalLocalDownloadState == Job.DownloadState.DOWNLOADING) { Log.i(TAG, "Download of converted project is already RUNNNING!!"); onDownloadStarted(fullDownloadURL); return; } Log.i(TAG, "Downloading missed converted project..."); if (cachedUTCDate != null) { final ScratchReconvertDialog reconvertDialog = new ScratchReconvertDialog(); reconvertDialog.setContext(currentActivity); reconvertDialog.setCachedDate(cachedUTCDate); reconvertDialog.setReconvertDialogCallback(new ScratchReconvertDialog.ReconvertDialogCallback() { @Override public void onDownloadExistingProgram() { downloadProgram(fullDownloadURL); } @Override public void onReconvertProgram() { convertProgram(job.getJobID(), job.getTitle(), job.getImage(), true); } @Override public void onUserCanceledConversion() { client.onUserCanceledConversion(job.getJobID()); for (final JobViewListener viewListener : getJobViewListeners(job.getJobID())) { viewListener.onUserCanceledJob(job); } } }); reconvertDialog.show(currentActivity.getFragmentManager(), ScratchReconvertDialog.DIALOG_FRAGMENT_TAG); return; } downloadProgram(fullDownloadURL); } }); } private void downloadProgram(final String fullDownloadURL) { Log.d(TAG, "Start download: " + fullDownloadURL); DownloadUtil.getInstance().prepareDownloadAndStartIfPossible(currentActivity, fullDownloadURL); } @Override public void onConversionFailure(@Nullable final Job job, final ClientException ex) { Log.e(TAG, "Conversion failed: " + ex.getMessage()); currentActivity.runOnUiThread(new Runnable() { @Override public void run() { if (job != null) { for (JobViewListener viewListener : getJobViewListeners(job.getJobID())) { viewListener.onJobFailed(job); } final Resources resources = currentActivity.getResources(); ToastUtil.showError(currentActivity, resources.getString(R.string.error_specific_scratch_program_conversion_failed_x, job.getTitle())); } else { ToastUtil.showError(currentActivity, R.string.error_scratch_program_conversion_failed); closeAllActivities(); } } }); } @Override public void onError(final String errorMessage) { currentActivity.runOnUiThread(new Runnable() { @Override public void run() { for (BaseInfoViewListener viewListener : baseInfoViewListeners) { viewListener.onError(errorMessage); } } }); } @Override public void onJobOutput(final Job job, final String[] lines) { currentActivity.runOnUiThread(new Runnable() { @Override public void run() { for (JobViewListener viewListener : getJobViewListeners(job.getJobID())) { viewListener.onJobOutput(job, lines); } } }); } @Override public void onJobProgress(final Job job, final short progress) { currentActivity.runOnUiThread(new Runnable() { @Override public void run() { for (JobViewListener viewListener : getJobViewListeners(job.getJobID())) { viewListener.onJobProgress(job, progress); } } }); } private void updateDownloadStateOnDisk(final long jobID, final Job.DownloadState downloadState) { Log.d(TAG, "Update download-state of program on disk"); try { SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(currentActivity .getApplicationContext()); SharedPreferences.Editor editor = sharedPref.edit(); String data = sharedPref.getString(Constants.SCRATCH_CONVERTER_DOWNLOAD_STATE_SHARED_PREFERENCE_NAME, null); HashMap<String, String> downloadStates = new HashMap<>(); if (data != null) { JSONObject jsonObject = new JSONObject(data); Iterator<String> keysItr = jsonObject.keys(); while (keysItr.hasNext()) { String key = keysItr.next(); String value = jsonObject.getString(key); downloadStates.put(key, value); } } downloadStates.put(Long.toString(jobID), Integer.toString(downloadState.getDownloadStateID())); Log.d(TAG, downloadStates.toString()); editor.putString(Constants.SCRATCH_CONVERTER_DOWNLOAD_STATE_SHARED_PREFERENCE_NAME, new JSONObject(downloadStates).toString()); editor.commit(); } catch (JSONException e) { Log.e(TAG, e.getMessage()); } } private Job.DownloadState readDownloadStateFromDisk(final long jobID) { Log.d(TAG, "Read download-state of program from disk"); try { SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(currentActivity .getApplicationContext()); String data = sharedPref.getString(Constants.SCRATCH_CONVERTER_DOWNLOAD_STATE_SHARED_PREFERENCE_NAME, null); HashMap<String, String> downloadStates = new HashMap<>(); if (data != null) { JSONObject jsonObject = new JSONObject(data); Iterator<String> keysItr = jsonObject.keys(); while (keysItr.hasNext()) { String key = keysItr.next(); String value = jsonObject.getString(key); downloadStates.put(key, value); } } String result = downloadStates.get(Long.toString(jobID)); if (result == null) { return Job.DownloadState.NOT_READY; } return Job.DownloadState.valueOf(Integer.parseInt(result)); } catch (JSONException e) { Log.e(TAG, e.getMessage()); } return Job.DownloadState.NOT_READY; } // ----------------------------------------------------------------------------------------------------------------- // DownloadCallback // ----------------------------------------------------------------------------------------------------------------- @Override public void onDownloadStarted(final String url) { final long jobID = Utils.extractScratchJobIDFromURL(url); updateDownloadStateOnDisk(jobID, Job.DownloadState.DOWNLOADING); // Note: this callback-method may not be called on UI-thread currentActivity.runOnUiThread(new Runnable() { @Override public void run() { final Client.DownloadCallback callback = downloadCallbacks.get(url); if (callback != null) { callback.onDownloadStarted(url); } for (final Client.DownloadCallback cb : globalDownloadCallbacks) { cb.onDownloadStarted(url); } } }); } @Override public void onDownloadProgress(final short progress, final String url) { // Note: this callback-method is not called on UI-thread currentActivity.runOnUiThread(new Runnable() { @Override public void run() { final Client.DownloadCallback callback = downloadCallbacks.get(url); if (callback != null) { callback.onDownloadProgress(progress, url); } for (final Client.DownloadCallback cb : globalDownloadCallbacks) { cb.onDownloadProgress(progress, url); } } }); } @Override public void onDownloadFinished(final String catrobatProgramName, final String url) { final long jobID = Utils.extractScratchJobIDFromURL(url); updateDownloadStateOnDisk(jobID, Job.DownloadState.DOWNLOADED); // Note: this callback-method is not called on UI-thread currentActivity.runOnUiThread(new Runnable() { @Override public void run() { final Client.DownloadCallback callback = downloadCallbacks.get(url); if (callback != null) { callback.onDownloadFinished(catrobatProgramName, url); } for (final Client.DownloadCallback cb : globalDownloadCallbacks) { cb.onDownloadFinished(catrobatProgramName, url); } } }); } @Override public void onUserCanceledDownload(String url) { final long jobID = Utils.extractScratchJobIDFromURL(url); updateDownloadStateOnDisk(jobID, Job.DownloadState.CANCELED); final Client.DownloadCallback callback = downloadCallbacks.get(url); if (callback != null) { callback.onUserCanceledDownload(url); } for (final Client.DownloadCallback cb : globalDownloadCallbacks) { cb.onUserCanceledDownload(url); } } }