Download in android large files - android

When I try this code, it starts download but then stuck with alert "force close"
what should I do? Use some kind of background thread?
try {
long startTime = System.currentTimeMillis();
URL u = new URL("http://file.podfm.ru/3/33/332/3322/mp3/24785.mp3");
HttpURLConnection c = (HttpURLConnection) u.openConnection();
c.setRequestMethod("GET");
c.setDoOutput(true);
c.connect();
FileOutputStream f = new FileOutputStream(new File("/sdcard/","logo.mp3"));
InputStream in = c.getInputStream();
byte[] buffer = new byte[1024];
int len1 = 0;
while ( (len1 = in.read(buffer)) != -1 ) {
f.write(buffer,0, len1);
}
f.close();
Log.d("ImageManager", "download ready in" +
((System.currentTimeMillis() - startTime) / 1000) + " sec");
}
catch (IOException e)
{
Log.d("ImageManager", "Error" +
((System.currentTimeMillis()) / 1000) + e + " sec");
}

I was dealing with similar problem last week and ended up using AsyncTask with progress bar displayed since it could take some time for the file to be downloaded. One way of doing it is to have below class nested in your Activity and just call it where you need to simply like this:
new DownloadManager().execute("here be URL", "here be filename");
Or if the class is not located within an activity and calling from an activity..
new DownloadManager(this).execute("URL", "filename");
This passes the activity so we have access to method getSystemService();
Here is the actual code doing all the dirty work. You will probably have to modify it for your needs.
private class DownloadManager extends AsyncTask<String, Integer, Drawable>
{
private Drawable d;
private HttpURLConnection conn;
private InputStream stream; //to read
private ByteArrayOutputStream out; //to write
private Context mCtx;
private double fileSize;
private double downloaded; // number of bytes downloaded
private int status = DOWNLOADING; //status of current process
private ProgressDialog progressDialog;
private static final int MAX_BUFFER_SIZE = 1024; //1kb
private static final int DOWNLOADING = 0;
private static final int COMPLETE = 1;
public DownloadManager(Context ctx)
{
d = null;
conn = null;
fileSize = 0;
downloaded = 0;
status = DOWNLOADING;
mCtx = ctx;
}
public boolean isOnline()
{
try
{
ConnectivityManager cm = (ConnectivityManager)mCtx.getSystemService(Context.CONNECTIVITY_SERVICE);
return cm.getActiveNetworkInfo().isConnectedOrConnecting();
}
catch (Exception e)
{
return false;
}
}
#Override
protected Drawable doInBackground(String... url)
{
try
{
String filename = url[1];
if (isOnline())
{
conn = (HttpURLConnection) new URL(url[0]).openConnection();
fileSize = conn.getContentLength();
out = new ByteArrayOutputStream((int)fileSize);
conn.connect();
stream = conn.getInputStream();
// loop with step
while (status == DOWNLOADING)
{
byte buffer[];
if (fileSize - downloaded > MAX_BUFFER_SIZE)
{
buffer = new byte[MAX_BUFFER_SIZE];
}
else
{
buffer = new byte[(int) (fileSize - downloaded)];
}
int read = stream.read(buffer);
if (read == -1)
{
publishProgress(100);
break;
}
// writing to buffer
out.write(buffer, 0, read);
downloaded += read;
// update progress bar
publishProgress((int) ((downloaded / fileSize) * 100));
} // end of while
if (status == DOWNLOADING)
{
status = COMPLETE;
}
try
{
FileOutputStream fos = new FileOutputStream(filename);
fos.write(out.toByteArray());
fos.close();
}
catch ( IOException e )
{
e.printStackTrace();
return null;
}
d = Drawable.createFromStream((InputStream) new ByteArrayInputStream(out.toByteArray()), "filename");
return d;
} // end of if isOnline
else
{
return null;
}
}
catch (Exception e)
{
e.printStackTrace();
return null;
}// end of catch
} // end of class DownloadManager()
#Override
protected void onProgressUpdate(Integer... changed)
{
progressDialog.setProgress(changed[0]);
}
#Override
protected void onPreExecute()
{
progressDialog = new ProgressDialog(/*ShowContent.this*/); // your activity
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.setMessage("Downloading ...");
progressDialog.setCancelable(false);
progressDialog.show();
}
#Override
protected void onPostExecute(Drawable result)
{
progressDialog.dismiss();
// do something
}
}

You should try using an AsyncTask. You are getting the force quit dialog because you are trying to do too much work on the UI thread and Android judges that your application has become unresponsive.
The answers to this question have some good links.
Something like the following would be a good start:
private class DownloadLargeFileTask extends AsyncTask<Void, Void, Void> {
private final ProgressDialog dialog;
public DownloadLargeFileTask(ProgressDialog dialog) {
this.dialog = dialog;
}
protected void onPreExecute() {
dialog.show();
}
protected void doInBackground(Void... unused) {
downloadLargeFile();
}
protected void onPostExecute(Void unused) {
dialog.dismiss();
}
}
and then execute the task with:
ProgressDialog dialog = new ProgressDialog(this);
dialog.setMessage("Loading. Please Wait...");
new DownloadLargeFileTask(dialog).execute();

Related

How to continue the download if it did not get complete over even after a long time and closing or canceling the download?

I have an application which has a button to start downloading a file and it works completely with no problem.
Now I want to do something more. If the download did not get complete (not overed or finished) or it canceled by user or other things (like losing internet connection), then again user click on the starting download button, the download should start from the continuation of the file but sadly it removes the file and start from the beginning of downloading the file.
What should I do to continue it and not starting again from the beginning?
public class Main extends AppCompatActivity {
private ProgressDialog pddDialog;
public static final int progress_bar_type = 0;
private Button startinstall;
private DownloadTask downloadTask;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layoutmain);
final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
final NetworkInfo ni = cm.getActiveNetworkInfo();
Button bdd = (Button) findViewById(R.id.downdata);
bdd.setTypeface(tf);
bdd.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
v.startAnimation(anim);
File filedata = new File(Environment.getExternalStorageDirectory() + "/areasoft/dpei.cso");
if (filedata.exists()) {
Toast.makeText(Main.this, "you have downloaded the file completly.", Toast.LENGTH_SHORT).show();
}else {
if (ni == null) {
Toast.makeText(Main.this, " you are not connecting to the internet.", Toast.LENGTH_SHORT).show();
} else {
downloadTask = new DownloadTask(Main.this);
downloadTask.execute("download link");
}
}
}
});
}
#Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case progress_bar_type:
pddDialog = new ProgressDialog(this);
pddDialog.setMessage("downloading fileā€¦");
pddDialog.setIndeterminate(false);
pddDialog.setMax(100);
pddDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pddDialog.setCancelable(true);
pddDialog.setButton("cancel", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
pddDialog.dismiss();
downloadTask.cancel(true);
}
});
pddDialog.show();
return pddDialog;
default:
return null;
}
}
class DownloadTask extends AsyncTask<String, String, String> {
#Override
protected void onPreExecute() {
super.onPreExecute();
showDialog(progress_bar_type);
}
private Context context;
private PowerManager.WakeLock mWakeLock;
public DownloadTask(Context context) {
this.context = context;
}
#Override
protected String doInBackground(String... sUrl) {
InputStream input = null;
OutputStream output = null;
HttpURLConnection connection = null;
try {
URL url = new URL(sUrl[0]);
connection = (HttpURLConnection) url.openConnection();
connection.connect();
// HTTP 200 OK
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
return "Server returned HTTP " + connection.getResponseCode()
+ " " + connection.getResponseMessage();
}
int fileLength = connection.getContentLength();
input = connection.getInputStream();
output = new FileOutputStream("/sdcard/areasoft/dpei.cso");
byte data[] = new byte[4096];
long total = 0;
int count;
while ((count = input.read(data)) != -1) {
if (isCancelled()) {
input.close();
return null;
}
total += count;
if (fileLength > 0)
publishProgress("" + (int) ((total * 100) / fileLength));
output.write(data, 0, count);
}
} catch (Exception e) {
return e.toString();
} finally {
try {
if (output != null)
output.close();
if (input != null)
input.close();
} catch (IOException ignored) {
}
if (connection != null)
connection.disconnect();
}
return null;
}
protected void onProgressUpdate(String... progress) {
pddDialog.setProgress(Integer.parseInt(progress[0]));
}
#Override
protected void onPostExecute(String file_url) {
dismissDialog(progress_bar_type);
}
}
}

android :: show progress dialog till asynctask does not get complete

I am downloading a zip file of 15 mb and then unzip it in the sd card.
I am using progress dialog to show the status. First time it works perfectly, and when I change the db version number on server to download new file and start the app again then progress dialog disappears in between and causes crash in the app.
Below is the code.
class CheckInAppUpdatesAsyncTask extends AsyncTask<Void, Void, Void> {
Dialog progress;
#Override
protected Void doInBackground(Void... params) {
try {
downloadDB();
} catch (Exception e) {
}
}
#Override
protected void onPostExecute(final Void result) {
stopWorking();
}
#Override
protected void onPreExecute() {
startWorking();
}
};5
private void startWorking() {
synchronized (this.diagSynch) {
if (this.pDiag != null) {
this.pDiag.dismiss();
}
this.pDiag = ProgressDialog.show(Browse.context, "Working...",
"Please wait while we load the encyclopedia.", true, false);
}
}
private void stopWorking() {
synchronized (this.diagSynch) {
if (this.pDiag != null) {
this.pDiag.dismiss();
}
}
}
Download code
URL url = new URL(serverFileURL);
Log.d("FILE_URLLINK", "serverFileURL " + serverFileURL);
URLConnection connection = url.openConnection();
InputStream input = connection.getInputStream();
connection.getContentLength();
byte data[] = new byte[1024];
input = new GZIPInputStream(input);
InputSource is = new InputSource(input);
InputStream in = new BufferedInputStream(is.getByteStream());
String inAppDBName = Constants.NEW_DB_NAME_TO_DOWNLOAD;
OutputStream output = new BufferedOutputStream(new FileOutputStream(dir + "/" + inAppDBName));
int length;
while ((length = in.read(data)) > 0) {
output.write(data, 0, length);
}
output.flush();
output.close();
input.close();
Any idea?
You should put unzip code before stopWorking(); in onPostExecute.
It will not close till all things happen.

Maintain progress of progressbar on orientation change in android

I am trying to download multiple videos from server using AsyncTask, I have list of progress bar for videos but I am unable to maintain the progress for each progress bar on orientation change of my phone.
I am calling downlodThreadVideos() in adapter of listview
public UserVideoDTO downlodThreadVideos(final ProgressBar _progress, ImageView _imgLaunch, ImageView _imgDownload, UserVideoDTO vpideoDTO)
{
DownloadVideoFileAsyncTask mDownloadFileAsync = new DownloadVideoFileAsyncTask(videoDTO,_progress,_imgLaunch,_imgDownload);
mDownloadFileAsync.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,videoDTO);
return mDownloadFileAsync._videoDTO;
}
private class DownloadVideoFileAsyncTask extends AsyncTask<UserVideoDTO, Integer, UserVideoDTO> {
ProgressBar _progress;
ImageView _imgLaunch;
ImageView _imgDownload;
public UserVideoDTO _videoDTO;
String OfflinePath=null;
public DownloadVideoFileAsyncTask(UserVideoDTO videoDTO,ProgressBar progress, ImageView imgLaunch, ImageView imgDownload) {
// TODO Auto-generated constructor stub
_videoDTO=videoDTO;
_progress = progress;
_imgLaunch=imgLaunch;
_imgDownload=imgDownload;
}
protected UserVideoDTO doInBackground(UserVideoDTO... params) {
UserVideoDTO videoDTO = _videoDTO;
try {
String _videoURL="http://mylinkforvideodownload/videoDTO.onlinepath";
if (cancelThread)
return null;
String Path = new String(_videoURL);
Path = Path.replaceAll(" ", "%20");
URL url = new URL(Path);
long startTime = System.currentTimeMillis();
HttpURLConnection ucon = (HttpURLConnection) url.openConnection();
ucon.setConnectTimeout(60000);
File folder = new File(getExternalFilesDir(null).getAbsolutePath()+"/Download");
folder.mkdir();
String fileName = getExternalFilesDir(null).getAbsolutePath()+ "/Download/videos_";
File file = new File(fileName);
String offlineFileName = videoDTO.lmsvideoid;
String offlineFilePath = file + offlineFileName + ".mp4";
BufferedInputStream inStream = new BufferedInputStream(ucon.getInputStream());
FileOutputStream outStream = new FileOutputStream(offlineFilePath);
byte[] buff = new byte[1024];
int lengthOfFile = ucon.getContentLength();
int len;
long total = 0;
try {
while (!cancelThread && ((len = inStream.read(buff)) != -1)) {
total += len;
publishProgress((int) ((total * 100) / lengthOfFile));
outStream.write(buff, 0, len);
}
} catch (Exception e) {
cancelThread = true;
}
outStream.flush();
outStream.close();
inStream.close();
}
catch (Exception e) {
e.printStackTrace();
}
return videoDTO;
}
protected void onProgressUpdate(Integer... progress) {
Log.d("ANDRO_ASYNC", progress[0].toString());
_progress.setProgress(progress[0]);
}
protected void onPreExecute() {
super.onPreExecute();
_progress.setMax(100);
}
protected void onPostExecute(UserVideoDTO result) {
super.onPostExecute(result);
_progress.setProgress(100);
}
protected void onCancelled() {
super.onCancelled();
}
}

Progress bar implementation with write method of dataoutputstream

Is it possible to incorporate a horizontal progress bar in the following code? I was thinking os AsyncTask but then I realized, I can't pass an integer value to ProgressUpdate() method inside doInBackground(). Please help!
public void sendFileDOS() throws FileNotFoundException {
runOnUiThread( new Runnable() {
#Override
public void run() {
registerLog("Sending. . . Please wait. . .");
}
});
final long startTime = System.currentTimeMillis();
final File myFile= new File(filePath); //sdcard/DCIM.JPG
byte[] mybytearray = new byte[(int) myFile.length()];
FileInputStream fis = new FileInputStream(myFile);
BufferedInputStream bis = new BufferedInputStream(fis);
DataInputStream dis = new DataInputStream(bis);
try {
dis.readFully(mybytearray, 0, mybytearray.length);
OutputStream os = socket.getOutputStream();
//Sending file name and file size to the server
DataOutputStream dos = new DataOutputStream(os);
dos.writeUTF(myFile.getName());
dos.writeLong(mybytearray.length);
dos.write(mybytearray, 0, mybytearray.length);
dos.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
runOnUiThread( new Runnable() {
#Override
public void run() {
long estimatedTime = (System.currentTimeMillis() - startTime)/1000;
registerLog("File successfully sent");
registerLog("File size: "+myFile.length()/1000+" KBytes");
registerLog("Elapsed time: "+estimatedTime+" sec. (approx)");
registerLog("Server stopped. Please restart for another session.");
final Button startServerButton=(Button)findViewById(R.id.button1);
startServerButton.setText("Restart file server");
}
});
}
For those facing similar issues, here is the working method for file transfer using Data Output Stream. The main idea was to break file into multiple chunks (i have divided into 100 chunks) and write to DOS in a while loop. Use your loop counter to update progress bar. Make sure to update progress bar in the main UI thread or else the application would crash. Here goes the code:
public void sendFileDOS() throws FileNotFoundException {
runOnUiThread( new Runnable() {
#Override
public void run() {
registerLog("Sending. . . Please wait. . .");
}
});
final long startTime = System.currentTimeMillis();
final File myFile= new File(filePath); //sdcard/DCIM.JPG
byte[] mybytearray = new byte[(int) myFile.length()];
FileInputStream fis = new FileInputStream(myFile);
BufferedInputStream bis = new BufferedInputStream(fis);
DataInputStream dis = new DataInputStream(bis);
try {
dis.readFully(mybytearray, 0, mybytearray.length);
OutputStream os = socket.getOutputStream();
//Sending file name and file size to the server
DataOutputStream dos = new DataOutputStream(os);
dos.writeUTF(myFile.getName());
dos.writeLong(mybytearray.length);
int i = 0;
final ProgressBar myProgBar=(ProgressBar)findViewById(R.id.progress_bar);
while (i<100) {
dos.write(mybytearray, i*(mybytearray.length/100), mybytearray.length/100);
final int c=i;
runOnUiThread( new Runnable() {
#Override
public void run() {
registerLog("Completed: "+c+"%");
myProgBar.setProgress(c);
}
});
i++;
}
dos.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
runOnUiThread( new Runnable() {
#Override
public void run() {
long estimatedTime = (System.currentTimeMillis() - startTime)/1000;
registerLog("File successfully sent");
registerLog("File size: "+myFile.length()/1000+" KBytes");
registerLog("Elapsed time: "+estimatedTime+" sec. (approx)");
registerLog("Server stopped. Please restart for another session.");
final Button startServerButton=(Button)findViewById(R.id.button1);
startServerButton.setText("Restart file server");
}
});
}
Cheers! :)
You can use AsyncTask that gets a progress bar like that:
public abstract class BaseTask extends AsyncTask<String, Integer, String>
{
private ProgressBar m_progressBar;
protected BaseTask(ProgressBar p)
{
m_progressBar = p;
}
#Override
protected void onPreExecute()
{
if (m_progressBar != null)
{
m_progressBar.setProgress(0);
}
}
#Override
protected void onPostExecute(String result)
{
if (m_progressBar != null)
m_progressBar.setVisibility(ProgressBar.GONE);
}
public void OnProgress(int prog)
{
if (m_progressBar != null)
{
m_progressBar.setProgress(prog);
}
}
}
To add a progress bar in your xml:
<ProgressBar
android:id="#+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp" />
To initialize the progress bar in your code:
ProgressBar p = (ProgressBar)findViewById(R.id.progressBar);
p.setVisibility(ProgressBar.VISIBLE);
p.setMax(100);
I can't pass an integer value to ProgressUpdate() method inside doInBackground()
Yes, you can !
Use publishProgress with the required parameters inside doInBackground to trigger onProgressUpdate:
final class MyTask extends AsyncTask<Void, Integer, Void> {
private final ProgressBar progress;
public MyTask(final ProgressBar progress) {
this.progress = progress;
}
#Override
protected void onPreExecute() {
progress.setMax(100);
}
#SuppressWarnings("unchecked")
#Override
protected Void doInBackground(final Void... params) {
...
int progress_val = // What ever you want
publishProgress(progress_val); // Here we trigger 'onProgressUpdate'
// with the updated integer as parameter
return null;
}
#Override
protected void onProgressUpdate(final Integer... values) {
progress.incrementProgressBy(values[0]); // Here we update the bar
}
#Override
protected void onPostExecute(final Void result) {
parent.finish();
}
}
EDIT :
Suggestion to write by blocks:
int i = 0;
int offset = 0;
int buflen = mybytearray.length/100;
while (i<100) {
dos.write(mybytearray, offset, buflen);
offset += buflen;
i++;
}
dos.write(mybytearray, offset, mybytearray.length%100);

Trying to get downloading file size, but get an error

private class DownloadLargeFileTask extends AsyncTask<Void, Void, Void>
{
private final ProgressDialog dialog;
public DownloadLargeFileTask(ProgressDialog dialog) {
this.dialog = dialog;
}
protected void onPreExecute() {
dialog.show();
}
protected void onPostExecute(Void unused) {
dialog.dismiss();
}
#Override
protected Void doInBackground(Void... arg0) {
download();
return null;
}
}
private void download ()
{
try
{
long startTime = System.currentTimeMillis();
URL u = new URL("http://mds.podfm.ru/188/download/040_Sergejj_Palijj_-_Karantin_CHast_2.mp3");
HttpURLConnection c = (HttpURLConnection) u.openConnection();
c.setRequestMethod("GET");
c.setDoOutput(true);
c.connect();
FileOutputStream f = new FileOutputStream(new File("/sdcard/","my.mp3"));
InputStream in = c.getInputStream();
byte[] buffer = new byte[1024];
int len1 = 0;
int downloadedSize = 0;
while ( (len1 = in.read(buffer)) != -1 )
{
f.write(buffer,0, len1);
downloadedSize += len1;
ReturnDownloadedBytes(downloadedSize);
}
f.close();
Log.d("ImageManager", "download ready in" + ((System.currentTimeMillis() - startTime) / 1000) + " sec");
}
catch (IOException e)
{
Log.d("ImageManager", "Error" + ((System.currentTimeMillis()) / 1000) + e + " sec");
}
}
private void ReturnDownloadedBytes(int size)
{
text.setText(String.valueOf(size));
}
Error says: Only the original thread that created a view hierarchy can touch its views.
i think this means that i create textview from one therad and trying to get access from another one (AsyncTask) but how to get it?
Thanks
EDIT
here is the all code. but even if i send in publishProgress(downloadedSize) int value = 10 it alwys outputs in text.setText(String.valueOf(progress)); different values like [Ljava.lang.float;#43ed62f78
public class list extends Activity {
private TextView text;
#Override
public void onCreate(Bundle icicle)
{
super.onCreate(icicle);
setContentView(R.layout.main);
text = (TextView)findViewById(R.id.result);
}
public void selfDestruct(View view) {
ProgressDialog dialog = new ProgressDialog(this);
dialog.setMessage("????????. ?????...");
new DownloadLargeFileTask(dialog).execute();
}
private class DownloadLargeFileTask extends AsyncTask<Void, Integer, Void>
{
private final ProgressDialog dialog;
public DownloadLargeFileTask(ProgressDialog dialog) {
this.dialog = dialog;
}
protected void onPreExecute() {
dialog.show();
}
protected void onPostExecute(Void unused) {
dialog.dismiss();
}
protected void onProgressUpdate(Integer...progress) {
text.setText(String.valueOf(progress));
}
#Override
protected Void doInBackground(Void... arg0) {
try
{
long startTime = System.currentTimeMillis();
URL u = new URL("http://mds.podfm.ru/188/download/040_Sergejj_Palijj_-_Karantin_CHast_2.mp3");
HttpURLConnection c = (HttpURLConnection) u.openConnection();
c.setRequestMethod("GET");
c.setDoOutput(true);
c.connect();
FileOutputStream f = new FileOutputStream(new File("/sdcard/","my.mp3"));
InputStream in = c.getInputStream();
byte[] buffer = new byte[1024];
int len1 = 0;
int downloadedSize = 10;
while ( (len1 = in.read(buffer)) != -1 )
{
f.write(buffer,0, len1);
//downloadedSize += len1;
**publishProgress(downloadedSize);**
}
f.close();
Log.d("ImageManager", "download ready in" + ((System.currentTimeMillis() - startTime) / 1000) + " sec");
}
catch (IOException e)
{
Log.d("ImageManager", "Error" + ((System.currentTimeMillis()) / 1000) + e + " sec");
}
return null;
}
}
You are correct in your assessment of the error. I'm assuming that text is a TextView object that is defined in your Activity, and as such it is created in the UI thread. The code that runs within doInBackground() runs in a separate thread. Only the UI thread can perform updates to UI elements, so when you try to call setText you get the error message that you reported.
Abhinav is also correct in how to fix the issue, as AsyncTask has a method that you can call to send updates from the background thread to the UI thread: publishProgress, which calls onProgressUpdate().
Add this method to your AsyncTask:
#Override
protected void onProgressUpdate(Integer... integer){
text.setText(integer[0].toString());
}
And change the while loop in download():
while ( (len1 = in.read(buffer)) != -1 )
{
f.write(buffer,0, len1);
downloadedSize += len1;
publishProgress(downloadedSize);
}
You can change the UI by publishing the progress to the UI thread. Call publishProgress from doInBackground. This will call onProgressUpdate in your AsyncTask from where you can update the UI.
You will have to define onProgressUpdate in your AsyncTask. http://developer.android.com/reference/android/os/AsyncTask.html#onProgressUpdate(Progress...)

Categories

Resources