/*
* 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.ui.fragment;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.Fragment;
import android.content.Intent;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import com.google.android.gms.common.images.WebImage;
import com.google.common.base.Preconditions;
import com.squareup.picasso.Picasso;
import org.catrobat.catroid.R;
import org.catrobat.catroid.common.Constants;
import org.catrobat.catroid.io.LoadProjectTask;
import org.catrobat.catroid.io.StorageHandler;
import org.catrobat.catroid.scratchconverter.Client;
import org.catrobat.catroid.scratchconverter.protocol.Job;
import org.catrobat.catroid.ui.ProjectActivity;
import org.catrobat.catroid.ui.ScratchConverterActivity;
import org.catrobat.catroid.ui.adapter.ScratchJobAdapter;
import org.catrobat.catroid.ui.adapter.ScratchJobAdapter.ScratchJobEditListener;
import org.catrobat.catroid.ui.dialogs.CustomAlertDialogBuilder;
import org.catrobat.catroid.ui.scratchconverter.BaseInfoViewListener;
import org.catrobat.catroid.ui.scratchconverter.JobViewListener;
import org.catrobat.catroid.utils.ToastUtil;
import org.catrobat.catroid.utils.Utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class ScratchConverterSlidingUpPanelFragment extends Fragment
implements BaseInfoViewListener, JobViewListener, Client.DownloadCallback, ScratchJobEditListener,
LoadProjectTask.OnLoadProjectCompleteListener {
private static final String TAG = ScratchConverterSlidingUpPanelFragment.class.getSimpleName();
private ImageView convertIconImageView;
private TextView convertPanelHeadlineView;
private TextView convertPanelStatusView;
private RelativeLayout convertProgressLayout;
private ProgressBar convertProgressBar;
private TextView convertStatusProgressTextView;
private ImageView upDownArrowImageView;
private ScrollView scrollView;
private ListView runningJobsListView;
private ListView finishedFailedJobsListView;
private Map<Long, Job> downloadJobsMap = Collections.synchronizedMap(new LinkedHashMap<Long, Job>());
private Map<Long, String> downloadedProgramsMap = Collections.synchronizedMap(new LinkedHashMap<Long, String>());
private RelativeLayout finishedFailedJobsList;
private RelativeLayout runningJobsList;
private ScratchJobAdapter runningJobsAdapter;
private ScratchJobAdapter finishedFailedJobsAdapter;
private List<Job> runningJobs;
private List<Job> finishedFailedJobs;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
runningJobs = new ArrayList<>();
finishedFailedJobs = new ArrayList<>();
final View rootView = inflater.inflate(R.layout.fragment_scratch_converter_sliding_up_panel, container, false);
convertIconImageView = (ImageView) rootView.findViewById(R.id.scratch_convert_icon);
convertPanelHeadlineView = (TextView) rootView.findViewById(R.id.scratch_convert_headline);
convertPanelStatusView = (TextView) rootView.findViewById(R.id.scratch_convert_status_text);
convertProgressLayout = (RelativeLayout) rootView.findViewById(R.id.scratch_convert_progress_layout);
convertProgressBar = (ProgressBar) rootView.findViewById(R.id.scratch_convert_progress_bar);
convertStatusProgressTextView = (TextView) rootView.findViewById(R.id.scratch_convert_status_progress_text);
upDownArrowImageView = (ImageView) rootView.findViewById(R.id.scratch_up_down_image_button);
scrollView = (ScrollView) rootView.findViewById(R.id.scratch_conversion_scroll_view);
runningJobsList = (RelativeLayout) rootView.findViewById(R.id.scratch_conversion_list);
runningJobsListView = (ListView) rootView.findViewById(R.id.scratch_conversion_list_view);
finishedFailedJobsList = (RelativeLayout) rootView.findViewById(R.id.scratch_converted_programs_list);
finishedFailedJobsListView = (ListView) rootView.findViewById(R.id.scratch_converted_programs_list_view);
convertPanelStatusView.setVisibility(View.VISIBLE);
convertProgressLayout.setVisibility(View.GONE);
runningJobsList.setVisibility(View.GONE);
finishedFailedJobsList.setVisibility(View.GONE);
return rootView;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
initAdapters();
}
public void scrollUpPanelScrollView() {
scrollView.fullScroll(ScrollView.FOCUS_UP);
}
private void initAdapters() {
Preconditions.checkState(getActivity() != null);
runningJobsAdapter = new ScratchJobAdapter(getActivity(),
R.layout.fragment_scratch_job_list_item,
R.id.scratch_job_list_item_title,
runningJobs);
runningJobsListView.setAdapter(runningJobsAdapter);
runningJobsList.setVisibility(View.GONE);
finishedFailedJobsAdapter = new ScratchJobAdapter(getActivity(),
R.layout.fragment_scratch_job_list_item,
R.id.scratch_job_list_item_title,
finishedFailedJobs);
finishedFailedJobsAdapter.setScratchJobEditListener(this);
finishedFailedJobsListView.setAdapter(finishedFailedJobsAdapter);
finishedFailedJobsList.setVisibility(View.GONE);
}
public void rotateImageButton(float degrees) {
upDownArrowImageView.setAlpha(Math.max(1.0f - (float) Math.sin(degrees / 360.0f * 2.0f * Math.PI), 0.3f));
upDownArrowImageView.setRotation(degrees);
}
public boolean hasVisibleJobs() {
return runningJobs.size() > 0 || finishedFailedJobs.size() > 0;
}
private void setIconImageView(final WebImage webImage) {
final Activity activity = getActivity();
if (activity != null && activity.getResources() != null && webImage != null && webImage.getUrl() != null) {
final int height = activity.getResources().getDimensionPixelSize(R.dimen.scratch_project_tiny_thumbnail_height);
final String originalImageURL = webImage.getUrl().toString();
// load image but only thumnail!
// in order to download only thumbnail version of the original image
// we have to reduce the image size in the URL
final String thumbnailImageURL = Utils.changeSizeOfScratchImageURL(originalImageURL, height);
Picasso.with(getActivity()).load(thumbnailImageURL).into(convertIconImageView);
} else {
convertIconImageView.setImageBitmap(null);
}
}
private void updateAdapterSingleJob(final Job job) {
if (job.isInProgress()) {
if (finishedFailedJobs.contains(job)) {
finishedFailedJobs.remove(job);
finishedFailedJobsAdapter.notifyDataSetChanged();
Utils.setListViewHeightBasedOnItems(finishedFailedJobsListView);
if (finishedFailedJobs.size() == 0) {
finishedFailedJobsList.setVisibility(View.GONE);
}
}
if (!runningJobs.contains(job)) {
runningJobs.add(0, job);
runningJobsAdapter.notifyDataSetChanged();
Utils.setListViewHeightBasedOnItems(runningJobsListView);
runningJobsList.setVisibility(View.VISIBLE);
} else {
runningJobsAdapter.notifyDataSetChanged();
}
return;
}
if (runningJobs.contains(job)) {
runningJobs.remove(job);
runningJobsAdapter.notifyDataSetChanged();
Utils.setListViewHeightBasedOnItems(runningJobsListView);
if (runningJobs.size() == 0) {
runningJobsList.setVisibility(View.GONE);
}
}
if (!finishedFailedJobs.contains(job)) {
finishedFailedJobs.add(0, job);
finishedFailedJobsAdapter.notifyDataSetChanged();
Utils.setListViewHeightBasedOnItems(finishedFailedJobsListView);
finishedFailedJobsList.setVisibility(View.VISIBLE);
} else {
finishedFailedJobsAdapter.notifyDataSetChanged();
}
}
private void updateConvertPanel(Job job, int statusTextID, boolean showProgress, int progress) {
updateAdapterSingleJob(job);
HashSet allRunningJobs = new HashSet<>(runningJobs);
allRunningJobs.addAll(downloadJobsMap.values());
if (allRunningJobs.size() > 1) {
showPanelBarSummary();
return;
}
convertPanelHeadlineView.setText(job.getTitle());
if (showProgress) {
convertProgressBar.setProgress(progress);
convertStatusProgressTextView.setText(String.format(Locale.getDefault(), "%1$d%%", progress));
convertPanelStatusView.setVisibility(View.GONE);
convertProgressLayout.setVisibility(View.VISIBLE);
} else {
convertPanelStatusView.setText(statusTextID);
convertPanelStatusView.setVisibility(View.VISIBLE);
convertProgressLayout.setVisibility(View.GONE);
}
setIconImageView(job.getImage());
}
private void showPanelBarSummary() {
int numberFinishedJobs = 0;
WebImage webImage = null;
if (!runningJobs.isEmpty()) {
for (Job job : runningJobs) {
if (webImage == null && job.getImage() != null && job.getImage().getUrl() != null) {
webImage = job.getImage();
break;
}
}
} else if (!downloadJobsMap.isEmpty()) {
for (Map.Entry<Long, Job> entry : downloadJobsMap.entrySet()) {
Job job = entry.getValue();
if (webImage == null && job.getImage() != null && job.getImage().getUrl() != null) {
webImage = job.getImage();
break;
}
}
}
for (Job job : finishedFailedJobs) {
if (webImage == null && job.getImage() != null && job.getImage().getUrl() != null) {
webImage = job.getImage();
}
if (job.getState() == Job.State.FINISHED && job.getDownloadState() != Job.DownloadState.DOWNLOADING) {
numberFinishedJobs++;
}
}
HashSet allRunningJobs = new HashSet<>(runningJobs);
allRunningJobs.addAll(downloadJobsMap.values());
int totalRunningJobs = allRunningJobs.size();
int totalFinishedJobs = numberFinishedJobs;
convertPanelHeadlineView.setText(getResources().getQuantityString(R.plurals.status_in_progress_x_jobs,
totalRunningJobs, totalRunningJobs));
convertPanelStatusView.setText(getResources().getQuantityString(R.plurals.status_completed_x_jobs,
totalFinishedJobs, totalFinishedJobs));
convertPanelStatusView.setVisibility(View.VISIBLE);
convertProgressLayout.setVisibility(View.GONE);
setIconImageView(webImage);
}
private void downloadInProgress(int progress, String url) {
final long jobID = Utils.extractScratchJobIDFromURL(url);
if (jobID == Constants.INVALID_SCRATCH_PROGRAM_ID) {
return;
}
final Job job = downloadJobsMap.get(jobID);
if (job == null) {
Log.e(TAG, "No job with ID " + jobID + " found in downloadJobsMap!");
return;
}
job.setDownloadState(Job.DownloadState.DOWNLOADING);
job.setDownloadProgress((short) progress);
updateConvertPanel(job, R.string.status_downloading, true, progress);
}
//------------------------------------------------------------------------------------------------------------------
// BaseInfoViewListener callbacks
//------------------------------------------------------------------------------------------------------------------
@Override
public void onJobsInfo(final Job[] jobs) {
if (jobs == null || jobs.length == 0) {
((ScratchConverterActivity) getActivity()).hideSlideUpPanelBar();
return;
}
runningJobs.clear();
finishedFailedJobs.clear();
for (Job job : jobs) {
if (job.isInProgress()) {
runningJobs.add(job);
} else if (job.getState() != Job.State.UNSCHEDULED) {
finishedFailedJobs.add(job);
}
}
if (runningJobs.size() > 0) {
Utils.setListViewHeightBasedOnItems(runningJobsListView);
runningJobsList.setVisibility(View.VISIBLE);
runningJobsAdapter.notifyDataSetChanged();
} else {
runningJobsList.setVisibility(View.GONE);
}
if (finishedFailedJobs.size() > 0) {
Utils.setListViewHeightBasedOnItems(finishedFailedJobsListView);
finishedFailedJobsList.setVisibility(View.VISIBLE);
finishedFailedJobsAdapter.notifyDataSetChanged();
} else {
finishedFailedJobsList.setVisibility(View.GONE);
}
if (hasVisibleJobs()) {
((ScratchConverterActivity) getActivity()).showSlideUpPanelBar(0);
}
showPanelBarSummary();
scrollUpPanelScrollView();
}
@Override
public void onError(final String errorMessage) {
if (!Looper.getMainLooper().equals(Looper.myLooper())) {
throw new AssertionError("You should not change the UI from any thread except UI thread!");
}
Log.e(TAG, "An error occurred: " + errorMessage);
ToastUtil.showError(getActivity(), errorMessage);
showPanelBarSummary();
}
//------------------------------------------------------------------------------------------------------------------
// JobViewListener callbacks
//------------------------------------------------------------------------------------------------------------------
@Override
public void onJobScheduled(final Job job) {
((ScratchConverterActivity) getActivity()).showSlideUpPanelBar(0);
updateConvertPanel(job, R.string.status_scheduled, false, 0);
}
@Override
public void onJobReady(final Job job) {
job.setProgress((short) 0);
updateConvertPanel(job, R.string.status_waiting_for_worker, false, 0);
}
@Override
public void onJobStarted(final Job job) {
job.setProgress((short) 0);
updateConvertPanel(job, R.string.status_started, false, 0);
}
@Override
public void onJobProgress(final Job job, final short progress) {
updateConvertPanel(job, R.string.status_started, true, progress);
}
@Override
public void onJobOutput(final Job job, @NonNull final String[] lines) {
// reserved for later use (i.e. next ScratchConverter release)!
}
@Override
public void onJobFinished(final Job job) {
downloadJobsMap.put(job.getJobID(), job);
updateConvertPanel(job, R.string.status_conversion_finished, false, 0);
}
@Override
public void onJobFailed(final Job job) {
updateConvertPanel(job, R.string.status_conversion_failed, false, 0);
}
@Override
public void onUserCanceledJob(Job job) {
updateConvertPanel(job, R.string.status_conversion_canceled, false, 0);
}
@Override
public void onDownloadStarted(String url) {
downloadInProgress(0, url);
}
@Override
public void onDownloadProgress(short progress, String url) {
downloadInProgress(progress, url);
}
@Override
public void onDownloadFinished(final String catrobatProgramName, final String url) {
Log.i(TAG, "Download of program '" + catrobatProgramName + "' finished (URL was " + url + ")");
final long jobID = Utils.extractScratchJobIDFromURL(url);
if (jobID == Constants.INVALID_SCRATCH_PROGRAM_ID) {
Log.w(TAG, "Received download-finished call for program: '" + catrobatProgramName + "' with invalid url: " + url);
return;
}
final Job job = downloadJobsMap.remove(jobID);
downloadedProgramsMap.put(jobID, catrobatProgramName);
if (job == null) {
Log.e(TAG, "No job with ID " + jobID + " found in downloadJobsMap!");
return;
}
try {
Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
Ringtone r = RingtoneManager.getRingtone(getActivity().getApplicationContext(), notification);
r.play();
} catch (Exception ex) {
Log.e(TAG, ex.getMessage());
}
job.setDownloadState(Job.DownloadState.DOWNLOADED);
updateConvertPanel(job, R.string.status_download_finished, false, 0);
}
@Override
public void onUserCanceledDownload(final String url) {
Log.i(TAG, "User canceled download with URL: " + url);
final long jobID = Utils.extractScratchJobIDFromURL(url);
if (jobID == Constants.INVALID_SCRATCH_PROGRAM_ID) {
Log.w(TAG, "Received download-canceled call for program with invalid url: " + url);
return;
}
final Job job = downloadJobsMap.remove(jobID);
if (job == null) {
Log.e(TAG, "No job with ID " + jobID + " found in downloadJobsMap!");
return;
}
job.setDownloadState(Job.DownloadState.CANCELED);
updateConvertPanel(job, R.string.status_download_canceled, false, 0);
}
@Override
public void onProjectEdit(int position) {
if (!Looper.getMainLooper().equals(Looper.myLooper())) {
throw new AssertionError("You should not change the UI from any thread except UI thread!");
}
Log.i(TAG, "User clicked on position: " + position);
final Job job = finishedFailedJobsAdapter.getItem(position);
if (job == null) {
Log.e(TAG, "Job not found in runningJobsAdapter!");
return;
}
if (job.getState() == Job.State.FAILED) {
ToastUtil.showError(getActivity(), R.string.error_cannot_open_failed_scratch_program);
return;
}
String catrobatProgramName = downloadedProgramsMap.get(job.getJobID());
catrobatProgramName = catrobatProgramName == null ? job.getTitle() : catrobatProgramName;
if (job.getDownloadState() == Job.DownloadState.DOWNLOADING) {
AlertDialog.Builder builder = new CustomAlertDialogBuilder(getActivity());
builder.setTitle(R.string.warning);
builder.setMessage(R.string.error_cannot_open_currently_downloading_scratch_program);
builder.setNeutralButton(R.string.close, null);
Dialog errorDialog = builder.create();
errorDialog.show();
return;
}
if (job.getDownloadState() == Job.DownloadState.NOT_READY || job.getDownloadState() == Job.DownloadState.CANCELED) {
AlertDialog.Builder builder = new CustomAlertDialogBuilder(getActivity());
builder.setTitle(R.string.warning);
builder.setMessage(R.string.error_cannot_open_not_yet_downloaded_scratch_program);
builder.setNeutralButton(R.string.close, null);
Dialog errorDialog = builder.create();
errorDialog.show();
return;
}
if (!StorageHandler.getInstance().projectExists(catrobatProgramName)) {
AlertDialog.Builder builder = new CustomAlertDialogBuilder(getActivity());
builder.setTitle(R.string.warning);
builder.setMessage(R.string.error_cannot_open_not_existing_scratch_program);
builder.setNeutralButton(R.string.close, null);
Dialog errorDialog = builder.create();
errorDialog.show();
return;
}
LoadProjectTask loadProjectTask = new LoadProjectTask(getActivity(), catrobatProgramName, true, false);
loadProjectTask.setOnLoadProjectCompleteListener(this);
loadProjectTask.execute();
}
@Override
public void onLoadProjectSuccess(boolean startProjectActivity) {
Intent intent = new Intent(getActivity(), ProjectActivity.class);
intent.putExtra(Constants.PROJECT_OPENED_FROM_PROJECTS_LIST, true);
getActivity().startActivity(intent);
}
@Override
public void onLoadProjectFailure() {
AlertDialog.Builder builder = new CustomAlertDialogBuilder(getActivity());
builder.setTitle(R.string.warning);
builder.setMessage(R.string.error_cannot_open_not_existing_scratch_program);
builder.setNeutralButton(R.string.close, null);
Dialog errorDialog = builder.create();
errorDialog.show();
}
}