android local webview OnPageFinished firing late - android

I have a webview which loads a local html page and I need to know when it has finished loading so I was using the function onPageFinished() which is fired straight away but for some reason it works fine on websites e.g. google.co.uk.
public class TestwebviewActivity extends Activity {
/** Called when the activity is first created. */
WebView webview = null;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
webview = (WebView) findViewById(R.id.webview);
Log.d("Webview", "created");
webview.getSettings().setJavaScriptEnabled(true);
webview.setWebViewClient(new WebViewClient()
{
public void onPageFinished(WebView view, String url)
{
Log.d("Webview", "Finished Loading");
Picture picture = view.capturePicture();
Bitmap b = Bitmap.createBitmap( 300, 300, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas( b );
picture.draw( c );
FileOutputStream fos = null;
try {
fos = new FileOutputStream( "/sdcard/google_" + System.currentTimeMillis() + ".jpg" );
if ( fos != null )
{
b.compress(Bitmap.CompressFormat.JPEG, 90, fos );
fos.close();
Log.d("Webview", "Image Created");
}
} catch( Exception e )
{
//...
}
}
});
webview.loadUrl("file:///android_asset/htmlpage.html");
}
}
This has only happened since upgraded to android 3.2 - previously on 2.3
Anyone come across the same issues or any suggestions?
Thanks if anyone can help.

Try to use deprecated PictureListener:
wv.setPictureListener(new PictureListener() {
#Override
public void onNewPicture(WebView view, Picture picture) {
if (i < pageList.size()) {
wv.loadUrl("javascript: highlightSearchTerms('" + searchKey + "');");
} else listener.onFinishSearch();
i++;
}
});

Related

Android emulator doesn't run some of my code

I have the problem that on emulator some things in my app doesn't work. The thing is onDownloadListener() method. In real device, it runs perfectly: I can download image and send it by messenger e.g. In the emulator it doesn't work, the method just doesn't run itself. It is terrible, I have tried different emulators and different devices. On each device I tried it was working but none of the emulators can run this code:
public class MyExport implements DownloadListener {
private final WebView webView;
private Context context;
private Activity activity;
public MyExport(Context c, Activity a, WebView webView) {
this.activity = a;
this.context = c;
this.webView = webView;
}
#Override
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
System.out.println("IM IN onDwoanloadStart");
String intentType = "image/png";
String fileName = "img.png";
try {
if(url != null) {
FileOutputStream fos;
fos = context.openFileOutput(fileName, Context.MODE_PRIVATE);
//conversion stuff
fos.write(decodedStr);
fos.getFD().sync();
fos.flush();
fos.close();
Intent sendIntent = new Intent();
sendIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
sendIntent.setAction(Intent.ACTION_SEND);
File filePath = new File(String.valueOf(context.getFilesDir()));
File newFile = new File(filePath, fileName);
sendIntent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".FileProvider", newFile));
sendIntent.setType(intentType);
context.startActivity(sendIntent);
}
} catch (IOException e) {
e.printStackTrace();
}
}
#JavascriptInterface
public void runExport(){
activity.runOnUiThread(new Runnable() {
#Override
public void run() {
webView.loadUrl("javascript: obj.exportImage()");
}
});
}
And this is how I add it to the webView:
MyExport export = new MyExport(activity.getBaseContext(), activity, this.webView);
this.webView.setDownloadListener(export);
this.webView.addJavascriptInterface(export, "jsint");
When I click on the button in WebView the exportImage() from Javascript is called but the onDownloadListener is not. This is happening ONLY on emulators!

Android: Toast shows up late when using threads

So I have code put together to download an image that's displayed in an ImageView, and I'm trying to put up a Toast when the user clicks the download button so they know some-thing's happening. The problem is that the Toast is coming up after a few seconds rather than when they have clicked the download button. I've tried wrapping it in a runOnUiThread method while it's in the new thread as well, but the issue is still there. Anyone able to point me in the right direction for fixing this? Code from the class is below.
public static final String EXTRA_IMAGE_URL = "extra_image_url";
private ImageView mImage;
private ImageView mDownloadImage;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView( R.layout.activity_social_image_download );
if( getIntent() == null || getIntent().getExtras() == null || !getIntent().getExtras().containsKey( EXTRA_IMAGE_URL ) ) {
finish();
}
String imageUrl = getIntent().getExtras().getString( EXTRA_IMAGE_URL, "" );
if( TextUtils.isEmpty( imageUrl ) ) {
finish();
}
mImage = (ImageView) findViewById( R.id.image );
mDownloadImage = (ImageView) findViewById( R.id.download_button );
mDownloadImage.setOnClickListener( new View.OnClickListener() {
#Override
public void onClick(View v) {
saveImage();
}
});
Picasso.with( this ).load( imageUrl ).into( mImage );
}
private void saveImage() {
Toast.makeText( this, "Downloading Image", Toast.LENGTH_SHORT ).show();
new Thread(new Runnable() {
#Override
public void run() {
File file1 = new File( Environment.getExternalStorageDirectory().getAbsolutePath() + "/DCIM/" + getString( R.string.app_name ) );
if( !file1.isDirectory() )
file1.mkdir();
File file = new File( file1.getPath() + "/" + (new Date()).getTime() + ".png");
ByteArrayOutputStream out = new ByteArrayOutputStream();
((BitmapDrawable) mImage.getDrawable() ).getBitmap().compress(Bitmap.CompressFormat.PNG, 100, out);
try {
file.createNewFile();
FileOutputStream fout = new FileOutputStream( file );
if( fout != null ) {
fout.write( out.toByteArray() );
fout.flush();
fout.close();
}
MediaScannerConnection.scanFile(getApplicationContext(), new String[] { file.getAbsolutePath() }, null, new MediaScannerConnection.OnScanCompletedListener() {
#Override
public void onScanCompleted(String path, Uri uri) {
}
});
} catch( IOException e ) {}
}
}).start();
}
Maybe if you were to use an AsyncTask ,you could handle this.Because the onPreExecute() could give you the the toast that you need.
private class SaveImageTask extends AsyncTask<Void, Void, Void> {
#Override
protected void onPreExecute (){
Toast.makeText( this, "Downloading Image", Toast.LENGTH_SHORT ).show();
}
#Override
protected void doInBackground(URL... urls) {
File file1 = new File( Environment.getExternalStorageDirectory().getAbsolutePath() + "/DCIM/" + getString( R.string.app_name ) );
if( !file1.isDirectory() )
file1.mkdir();
File file = new File( file1.getPath() + "/" + (new Date()).getTime() + ".png");
ByteArrayOutputStream out = new ByteArrayOutputStream();
((BitmapDrawable) mImage.getDrawable() ).getBitmap().compress(Bitmap.CompressFormat.PNG, 100, out);
try {
file.createNewFile();
FileOutputStream fout = new FileOutputStream( file );
if( fout != null ) {
fout.write( out.toByteArray() );
fout.flush();
fout.close();
}
}catch(Exception e){
//Error handling.
}
}
#Override
protected void onPostExecute(Long result) {
MediaScannerConnection.scanFile(getApplicationContext(), new String[] { file.getAbsolutePath() }, null, new MediaScannerConnection.OnScanCompletedListener() {
#Override
public void onScanCompleted(String path, Uri uri) {
}
});
}
}
}
Turned out the issue has nothing to do with the threads and toast. There's a couple second delay between when the activity is created and when the button is able to respond to a click. If the activity is started and I wait to press the download button until after four seconds, everything works as expected. Trying to hunt down what would cause the UI thread to hang in such a basic Activity now. Even when Picasso is removed and the bitmap is just passed as an extra in the intent, the issue persists.

how to maintain user picture from server?

I need to show picture of user in my application and I retrieve that picture from server since my application also works in offline mode so I need to save that picture from server to my SD card , also I when i sync data from server next time If picture has changed then i need to change that picture in my SD card too how to determine if picture for particular user has changed
currently i save the image from server as follow though I use hardcoded url as of now and static user id
public class fetchImage extends Activity implements OnClickListener {
int id;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
id = 1;// declaring static as of now
}
{
new BackgroundTask().execute();
File storagePath = Environment.getExternalStorageDirectory();
File imgFile = new File(storagePath, "/Pictures/" + id + ".jpg");
if (imgFile.exists()) {
Bitmap myBitmap = BitmapFactory.decodeFile(imgFile
.getAbsolutePath());
}
}
class BackgroundTask extends AsyncTask<Void, Void, Void> {
ProgressDialog mDialog;
protected void onPreExecute() {
mDialog = ProgressDialog.show(fetchImage.this, "",
getString(R.string.progress_bar_loading), true);
};
#Override
protected Void doInBackground(Void... params) {
try {
savesd(id, null);
} catch (final Exception e) {
}
return null;
}
private void savesd(int id, URL uri) throws IOException {
URL url;
if (uri == null) {
url = new URL("http://i.zdnet.com/blogs/3-29-androids.jpg");
} else {
url = uri;
}
InputStream input = url.openStream();
try {
File storagePath = Environment.getExternalStorageDirectory();
OutputStream output = new FileOutputStream(new File(
storagePath, "/Pictures/" + id + ".jpg"));
try {
byte[] buffer = new byte[20000];
int bytesRead = 0;
while ((bytesRead = input.read(buffer, 0, buffer.length)) >= 0) {
output.write(buffer, 0, bytesRead);
}
} finally {
output.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
input.close();
}
}
protected void onPostExecute(Void result) {
mDialog.dismiss();
};
}
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
}
}
Also i've got one problem that when I uninstall this app from Device it should also clear these user images from sd card
I used timestamp to save the time when I last sync data and download files only after that time stamp

Internal memory full of pictures, probably caused by Bitmap.compress(format, int, stream)

My app is a Wifi chat app with which you can communicate between two Android units with text messages and snap camera pictures and send them. The pictures are stored to the SD-card.
I used to have an OutOfMemoryError thrown after a couple of sent images, but I solved that problem by sending the
options.inPurgeable = true;
and
options.inInputShareable = true;
to the BitmapFactory.decodeByteArray method. This makes the pixels "deallocatable" so new images can use the memory. Thus, the error no longer remains.
But, the internal memory is still full of images and the "Low on space: Phone storage space is getting low" warning appears. The app no longer crashes but there's no more memory on the phone after the app finishes. I have to manually clear the app's data in Settings > Applications > Manage Applications.
I tried recycling the bitmaps and even tried to explicitly empty the app's cache, but it doesn't seem to do what i expect.
This function receives the picture via a TCP socket, writes it to the SD-card and starts my custom Activity PictureView:
public void receivePicture(String fileName) {
try {
int fileSize = inStream.readInt();
Log.d("","fileSize:"+fileSize);
byte[] tempArray = new byte[200];
byte[] pictureByteArray = new byte[fileSize];
path = Prefs.getPath(this) + "/" + fileName;
File pictureFile = new File(path);
try {
if( !pictureFile.exists() ) {
pictureFile.getParentFile().mkdirs();
pictureFile.createNewFile();
}
} catch (IOException e) { Log.d("", "Recievepic - Kunde inte skapa fil.", e); }
int lastRead = 0, totalRead = 0;
while(lastRead != -1) {
if(totalRead >= fileSize - 200) {
lastRead = inStream.read(tempArray, 0, fileSize - totalRead);
System.arraycopy(tempArray, 0, pictureByteArray, totalRead, lastRead);
totalRead += lastRead;
break;
}
lastRead = inStream.read(tempArray);
System.arraycopy(tempArray, 0, pictureByteArray, totalRead, lastRead);
totalRead += lastRead;
}
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(pictureFile));
bos.write(pictureByteArray, 0, totalRead);
bos.flush();
bos.close();
bos = null;
tempArray = null;
pictureByteArray = null;
setSentence("<"+fileName+">", READER);
Log.d("","path:"+path);
try {
startActivity(new Intent(this, PictureView.class).putExtra("path", path));
} catch(Exception e) { e.printStackTrace(); }
}
catch(IOException e) { Log.d("","IOException:"+e); }
catch(Exception e) { Log.d("","Exception:"+e); }
}
Here's PictureView. It creates a byte[ ] from the file on the SD-card, decodes the array to a Bitmap, compresses the Bitmap and writes it back to the SD-card. Lastly, in the Progress.onDismiss, the picture is set as the image of a full screen imageView:
public class PictureView extends Activity {
private String fileName;
private ProgressDialog progress;
public ImageView view;
#Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
Log.d("","onCreate() PictureView");
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
view = new ImageView(this);
setContentView(view);
progress = ProgressDialog.show(this, "", "Laddar bild...");
progress.setOnDismissListener(new OnDismissListener() {
public void onDismiss(DialogInterface dialog) {
File file_ = getFileStreamPath(fileName);
Log.d("","SETIMAGE");
Uri uri = Uri.parse(file_.toString());
view.setImageURI(uri);
}
});
new Thread() { public void run() {
String path = getIntent().getStringExtra("path");
Log.d("","path:"+path);
File pictureFile = new File(path);
if(!pictureFile.exists())
finish();
fileName = path.substring(path.lastIndexOf('/') + 1);
Log.d("","fileName:"+fileName);
byte[] pictureArray = new byte[(int)pictureFile.length()];
try {
DataInputStream dis = new DataInputStream( new BufferedInputStream(
new FileInputStream(pictureFile)) );
for(int i=0; i < pictureArray.length; i++)
pictureArray[i] = dis.readByte();
} catch(Exception e) { Log.d("",""+e); e.printStackTrace(); }
/**
* Passing these options to decodeByteArray makes the pixels deallocatable
* if the memory runs out.
*/
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPurgeable = true;
options.inInputShareable = true;
Bitmap pictureBM =
BitmapFactory.decodeByteArray(pictureArray, 0, pictureArray.length, options);
OutputStream out = null;
try {
out = openFileOutput(fileName, MODE_PRIVATE);
/**
* COMPRESS !!!!!
**/
pictureBM.compress(CompressFormat.PNG, 100, out);
pictureBM = null;
progress.dismiss(); }
catch (IOException e) { Log.e("test", "Failed to write bitmap", e); }
finally {
if (out != null)
try { out.close(); out = null; }
catch (IOException e) { }
} }
}.start();
}
#Override
protected void onStop() {
super.onStop();
Log.d("","ONSTOP()");
Drawable oldDrawable = view.getDrawable();
if( oldDrawable != null) {
((BitmapDrawable)oldDrawable).getBitmap().recycle();
oldDrawable = null;
Log.d("","recycle");
}
Editor editor =
this.getSharedPreferences("clear_cache", Context.MODE_PRIVATE).edit();
editor.clear();
editor.commit();
}
}
When the user presses the back key, the picture isn't supposed to be available anymore from within the app. Just stored on the SD-card.
In onStop() I recycle the old Bitmap and even try to empty the app's data. Still the "Low on space" warning appears. How can I be sure the images won't allocate the memory anymore when they're not needed?
EDIT: It appears the problem is the compress method. If everything after compress is commented, the problem remains. If I delete compress, the problem disappears. Compress seems to allocate memory that's never released, and it's 2-3 MB per image.
Ok, I solved it. The problem was, I was passing an OutputStream to compress, which is a stream to a private file in the app's internal memory. That's what I set as the image later. This file is never allocated.
I didn't get that I had two files: one on the SD-card and one in the internal memory, both with the same name.
Now, I'm just setting the SD-card file as the ImageView's image. I never read the file into the internal memory as a byte[], thus never decoding the array to a bitmap, thus never compressing the bitmap into the internal memory.
This is the new PictureView:
public class PictureView extends Activity {
public ImageView view;
private String path;
#Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
Log.d("","onCreate() PictureView");
path = getIntent().getStringExtra("path");
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
view = new ImageView(this);
setContentView(view);
Uri uri = Uri.parse( new File(path).toString() );
view.setImageURI(uri);
}
#Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
Log.d("","Back key pressed");
Drawable oldDrawable = view.getDrawable();
if( oldDrawable != null) {
((BitmapDrawable)oldDrawable).getBitmap().recycle();
oldDrawable = null;
Log.d("","recycle");
}
view = null;
}
return super.onKeyDown(keyCode, event);
}
}
Is it bad practice to put an external file as the image of an ImageView? Should I load it into internal memory first?
If you specifically want the image to be nullified from memory for sure when a user presses back you could override the back button and make your image clean up calls there. I do that in some of my apps and it seems to work. maybe something like this:
#Override
protected void onBackPressed() {
super.onBackPressed();
view.drawable = null;
jumpBackToPreviousActivity();
}
Im pretty sure there are some view methods that clear other caches and things like that. You can recycle the bitmap but that doesnt guarantee that it will be dumped right then but only at some point when the gc gets to it.....but Im sure you probably know that already :)
EDIT: You could also do the same thing in the onPause method. That one is guaranteed to get called. The other two may never get called according to the android docs.
http://developer.android.com/reference/android/app/Activity.html

Generate bitmap from HTML in Android

How do can you generate a bitmap from HTML in Android?
Can the WebView be used for this or is there a better approach (like maybe using the WebView rendering engine directly)? How?
I would like to implement the following method...
public Bitmap toBitmap(Context context, String html, Rect rect);
...where html is the html to render and rect is the frame of the desired bitmap.
A synchronous method that generates a bitmap from an HTML string using a WebView, and can be used within an AsyncTask:
public Bitmap getBitmap(final WebView w, int containerWidth, int containerHeight, final String baseURL, final String content) {
final CountDownLatch signal = new CountDownLatch(1);
final Bitmap b = Bitmap.createBitmap(containerWidth, containerHeight, Bitmap.Config.ARGB_8888);
final AtomicBoolean ready = new AtomicBoolean(false);
w.post(new Runnable() {
#Override
public void run() {
w.setWebViewClient(new WebViewClient() {
#Override
public void onPageFinished(WebView view, String url) {
ready.set(true);
}
});
w.setPictureListener(new PictureListener() {
#Override
public void onNewPicture(WebView view, Picture picture) {
if (ready.get()) {
final Canvas c = new Canvas(b);
view.draw(c);
w.setPictureListener(null);
signal.countDown();
}
}
});
w.layout(0, 0, rect.width(), rect.height());
w.loadDataWithBaseURL(baseURL, content, "text/html", "UTF-8", null);
}});
try {
signal.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return b;
}
It has some limitations, but it's a start.
You can use the draw method to let it draw in a Bitmap of your choice. I made an example, don't forget internet and external storage rights of your manifest:
public class MainActivity extends Activity {
private WebView mWebView;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mWebView = new WebView(this);
setContentView(mWebView);
mWebView.loadUrl("http://tea.ch");
}
#Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode != KeyEvent.KEYCODE_BACK) return super.onKeyDown(keyCode, event);
Bitmap bm = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm);
mWebView.draw(c);
OutputStream stream = null;
try {
stream = new FileOutputStream(Environment.getExternalStorageDirectory() +"/teach.png");
bm.compress(CompressFormat.PNG, 80, stream);
if (stream != null) stream.close();
} catch (IOException e) {
} finally {
bm.recycle();
}
return super.onKeyDown(keyCode, event);
}
}
Why not use the WebView method : capturePicture() which returns a Picture and is available since API level 1 ?
It returns a picture of the entire document.
You could then crop the result with your rectangle and save the bitmap from there.
This example shows how to capture webView content last picture (it waits until webview complete rendering picture), it is an example of convert HTML to PNG using Android
Activity Code
public class HtmlViewer extends Activity {
private String HTML;
private Context ctx;
private Picture pic = null;
private int i=0; suppose this is the last pic
private int oldi = 0;
private Timer myTimer; // timer for waiting until last picture loaded
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_html_viewer);
Intent intent = getIntent();
HTML = intent.getStringExtra("HTML");
ctx = this;
WebView wv = (WebView)findViewById(R.id.webView1);
wv.setPictureListener(new PictureListener(){
public void onNewPicture(WebView view, Picture picture) {
Log.w("picture", "loading.." + String.valueOf(view.getProgress()));
pic = picture;
i++;
}
});
wv.loadData(HTML, "text/html; charset=utf-8", null);
wv.setWebViewClient(new WebViewClient()
{
public void onPageFinished(WebView wv, String url)
{
Picture p = wv.capturePicture();
myTimer = new Timer();
myTimer.schedule(new TimerTask() {
#Override
public void run() {
if (i > oldi)
oldi = i;
else
if (i != 0)
{
Log.w("picture", "finished");
cancel();
Picture picture = pic;
Log.w("picture", "onNewPicture- Height"+ picture.getHeight());
Log.w("picture", "onNewPicture- Width"+ picture.getWidth());
File sdCard = Environment.getExternalStorageDirectory();
if (picture != null)
{
Log.w("picture", " P OK");
Bitmap image = Bitmap.createBitmap(picture.getWidth(),picture.getHeight(), Config.ARGB_8888);
Canvas canvas = new Canvas(image);
picture.draw(canvas);
Log.w("picture", "C OK");
if (image != null) {
Log.w("picture", "I OK");
ByteArrayOutputStream mByteArrayOS = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.PNG, 90, mByteArrayOS);
try {
File file = new File(sdCard, "AccountView.PNG");
FileOutputStream fos = new FileOutputStream(file);
fos.write(mByteArrayOS.toByteArray());
fos.flush();
fos.close();
Log.w("picture", "F OK " + String.valueOf(mByteArrayOS.size()) + " ? " + String.valueOf(file.length()));
Intent sharingIntent = new Intent(Intent.ACTION_SEND);
Uri screenshotUri = Uri.fromFile(file);
sharingIntent.setType("image/png");
sharingIntent.putExtra(Intent.EXTRA_STREAM, screenshotUri);
startActivity(Intent.createChooser(sharingIntent, getResources().getString(R.string.ACCOUNT_VIEW_TITLE)));
((Activity)ctx).finish();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}, 0, 1000);
Log.w("picture", "done");
loadcompleted = true;
}
});
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.activity_html_viewer, menu);
return true;
}
}
Layout
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".HtmlViewer" >
<WebView
android:id="#+id/webView1"
android:layout_width="match_parent"
android:layout_height="match_parent" />
This is a good library that can be used to convert any HTML content to bitmap.
It supports both URL and HTML String
https://github.com/iZettle/android-html2bitmap

Categories

Resources