onPictureTaken method - getting data as a tiff? - java

So I was reading many tutorials on how to get an image preview and then save it.
The usual tutorial involves using SurfaceHolder and SurfaceView with a PictureCallback.
As it appears on the documentation this is the signature for
public abstract void onPictureTaken (byte[] data, Camera camera)
Usually to save the image you can do something like
/** Handles data for jpeg picture */
PictureCallback jpegCallback = new PictureCallback() {
public void onPictureTaken(byte[] data, Camera camera) {
FileOutputStream outStream = null;
try {
// write to local sandbox file system
// outStream =
// CameraDemo.this.openFileOutput(String.format("%d.jpg",
// System.currentTimeMillis()), 0);
// Or write to sdcard
outStream = new FileOutputStream(String.format(
"/sdcard/%d.jpg", System.currentTimeMillis()));
outStream.write(data);
outStream.close();
Log.d(TAG, "onPictureTaken - wrote bytes: " + data.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
}
Log.d(TAG, "onPictureTaken - jpeg");
}
};
but I'm not interested in saving a jpeg. I want to save a tiff image. Reading the documentation I've found a really general comment on data format:
Called when image data is available after a picture is taken. The
format of the data depends on the context of the callback and
Camera.Parameters settings.
So I clicked on Camera.Parameters but I didn't find anything useful.
Is there a way to save the bytes as tiff? Am I doing something wrong?

No, TIFF is not supported directly. The most-compatible option to save a TIFF file is to decode the JPEG you receive from takePicture, by using BitmapFactory, and then use a TIFF library to save the bitmap to disk as a TIFF file.
Android's APIs do not support TIFF directly, so you'll have to find a 3rd-party library.
You might want to consider using PNG instead, since that's supported by the APIs directly; you could just call Bitmap.compress then.

Related

(React Native/Android) Android getFilesDir() gives me '/data/user/' instead of '/data/data/'

I am building an app which has a feature to crop images using react-native-image-crop-picker. I am trying to implement the logic to store the cropped images locally in my react native app. I could successfully implement the logic for iOS, however, I am having trouble with the Android side.
My problem is that when I store the image using reactContext.getFilesDir(), the image is stored into the /data/user/0/com.myapp/files/ directory. And the images can be accessed via 'Google Photos' app or 'Files' app. I don't want to let the users access these images.
Here is the picture describing my problem.
The things I have tried so far:
1. Use getCurrentActivity() instead of reactContext
2. Use getReactApplicationContext() instead of context
Findings:
- After saving the image, it is stored into /data/user/0/com.myapp/files/, /data/data/0/com.myapp/files/ and storage/emulated/0/Pictures/.
FileStreamHandler.java
public class FileStreamHandler extends ReactContextBaseJavaModule {
private Context context;
// private Activity mActivity;
#Nonnull
#Override
public String getName() {
return "FileStreamHandler";
}
public FileStreamHandler(ReactApplicationContext reactContext) {
super(reactContext);
// mActivity = reactContext.getCurrentActivity();
this.context = reactContext;
}
#ReactMethod
private void saveImageData(String base64String, Callback callback) {
// Generate random image name
String fileName = UUID.randomUUID().toString() + ".png";
// File fileDirectory = mActivity.getFilesDir();
File fileDirectory = context.getFilesDir();
File imageFile = new File(fileDirectory, fileName);
String imageFilePath = imageFile.getAbsolutePath();
try {
OutputStream stream = new FileOutputStream(imageFile);
//decode base64 string to image
byte[] decodedBytes = Base64.decode(base64String, Base64.DEFAULT);
Bitmap decodedImage = BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.length);
decodedImage.compress(Bitmap.CompressFormat.PNG,100, stream);
stream.flush();
stream.close();
} catch (IOException e) {
e.printStackTrace();
}
callback.invoke(imageFilePath);
}
}
The image is stored successfully without any errors. However, it is stored into /data/user/ and can be accessed via other applications such as 'Photos' or 'Files'.
Although I am using exactly the same logic in my pure Android app, I have never had this problem. Therefore, I am suspecting that the react application context is causing the problem.
Any help would be highly appreciated.
Thank you.
It turns out that the cause of the problem is the react native library that I am using. I don't know why they implemented in this way, however, it seems like the react-native-image-crop-picker library saves images into the /storage/0/Pictures/ directory after cropping.

How To Play A Local Sound File

I want to make a program that plays sounds and displays png images onto the JFrame. I am trying to put the png and sound files (.wav) into the package that the class that's displaying it is in. I can't seem to get it working though. I've looked up many methods on how to do it, it every time they all pop up NullPointer errors. Or that it couldn't find the file, even though the file path specified was exactly where it was when I went into File Explorer. So if anyone can help me find a way to play music and display the picture (getting the png file and making it an ImageIcon), that would be great.
Here is java code
Play Button action Performed
private void play_btnActionPerformed(java.awt.event.ActionEvent evt) {
SetImage();
PlaySound();
}
Play audio
void PlaySound() {
try (InputStream in = getClass().getResourceAsStream("sam.wav")) {
InputStream bufferedInS = new BufferedInputStream(in);
try (AudioInputStream audioInS = AudioSystem.getAudioInputStream(bufferedInS)) {
Clip clip = AudioSystem.getClip();
clip.open(audioInS);
clip.start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
set image
void SetImage() {
audio_icon.setIcon(new javax.swing.ImageIcon(getClass().getResource("/player/player.png")));
}

I need to download several images to directory so that content can be accessed offline

I get some JSON data which contains some food menu items
Please note: this is just a sample, there are more than 2 images sometimes and many more menu items in the array!
{
"menu": [
{
"url": "/api/v1/menu/1",
"name": "Best Food",
"description": "really nice food",
"opening_time": "every day from 9am to 6pm",
"contact_email": "info#food.com",
"tel_number": "+54 911 3429 5762",
"website": "http://bestfood.com",
"images": [
{
"url": "https://blahblah/image1.jpg"
},
{
"url": "https://blahblah/image2.jpg"
}
]
},
]
}
Each item has some info and an array of image URLs.
I am using the Glide image library to process these images and Retrofit 2.0 to download the JSON data from the endpoint. All is well at this point.
However, I need to store this downloaded data for offline access.
Currently, I am using ORM Lite on my existing models to store all the JSON data in a database. This part is OK.
However, in my database I only store the image URLs as I was told that it is not good approach to store images (as blob) in the database.
So there is a section in my app to view the saved menu's with an option to download it for offline access if the user chooses to.
It is worth mentioning at this point, that I already have the raw menu information in the database because the user would have to view the menu in the first place to get it in DB.
But the problem is the images.
This is where I do not know how to proceed, but I list the solutions and problems that I am thinking about and was hoping people could advise me on what is the best course of action is.
Use a service to download the images. This I feel is mandatory because I do not know how many images there will be, and I want the download to proceed even if user exits the app
Glide has a download only option for images and you can configure where its cache is located (internal private or external public) as I read here and here. Problem is I do not feel comfortable with setting the cache size as I do not know what is required. I would like to set unlimited.
I need to be able to delete the saved menu data especially if its saved on the external public directory as this is not removed when the app is deleted etc. or if the user chooses to delete a saved menu from within the app. I was thinking I could store the file image URIs or location of the entire saved menu in database for this but not sure if this is a good way
I read in different sources and answers that in this use case for just caching images to SD card etc. that I should specifically use a network library to do so to avoid the allocation of a bitmap to heap memory. I am using OK HTTP in my app at the moment.
I'm using ormlite to store objects with urls too, I have a synchronization after the "sign in" screen on my app, on my experience I really recommend this library https://github.com/thest1/LazyList
It's very simple:
ImageLoader imageLoader=new ImageLoader(context);
imageLoader.DisplayImage(url, imageView);
This library saves the image using the url on the external sd with basic and simple configuration about the memory issues, so if you actually have two or more items with the same url this library works perfectly, the url and imageView are the parameters, if the image is not on the phone begins a new task and put the image in the view when the download is finish, and btw this library also saves the images encoded, so these pictures don't appear on the gallery.
Actually you only need these files to implement the library:https://github.com/thest1/LazyList/tree/master/src/com/fedorvlasov/lazylist
If you wanna manipulate some files, you can change the folder name in the FileCache class:
public FileCache(Context context){
//Find the dir to save cached images
...
cacheDir=new File(android.os.Environment.getExternalStorageDirectory(),"LazyList");
...
}
Where "LazyList" is the folder name, and them you can delete, move, etc.
Delete sample:
/**
* This method delete a file if exist
*/
public static void deleteFile(File file){
if(file!=null && file.exists()) {
file.delete();
}
}
Now I learned more about memory cache and the allocation of a bitmap to heap memory, for the first time manipulating images online and offline, I recommend this library, also when you learn more about it, you can implement and edit the library to your needs.
1: Use an IntentService to do your downloads.
http://developer.android.com/reference/android/app/IntentService.html
2: Set up your IntentService using AlarmManager so that it runs even if the
application is not running. You register with the AlarmManager
http://developer.android.com/reference/android/app/AlarmManager.html
There are a variety of ways you can have the AlarmManager start your
intent.
For Example:
// Register first run and then interval for repeated cycles.
alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + DEFAULT_INITIAL_RUN,
DEFAULT_RUN_INTERVAL, pi);
3: Storing Data
There are several options here depending on how public you want your
pictures/data to be.
http://developer.android.com/reference/android/os/Environment.html
Example: External Public Storage
File dirBackup = Environment.getExternalStoragePublicDirectory(
"YourDirectory" );
4: Downloading
Your option here. You can using anything from your current API to a
basic URLConnection.
You may want to look at:
http://developer.android.com/reference/android/app/DownloadManager.html
Also, watch your permissions you will need to add
and
Hope this points you in a useful direction.
Try out this library to manage images loading.
Use a service to download the images. This I feel is mandatory because
I do not know how many images there will be, and I want the download
to proceed even if user exits the app
All downloading done in worker threads so it's alive while application process is alive. There may a problem appear: application dies while loading is in progress. To workaround this I suggest to use AlarmManager in combination with Service. Set it up to start by timer, check you database or UIL cache for image files being not loaded and start their loading again.
Glide has a download only option for images and you can configure
where its cache is located (internal private or external public) as I
read here and here. Problem is I do not feel comfortable with setting
the cache size as I do not know what is required. I would like to set
unlimited.
UIL has several disc cache implementations out of the box including unlimited one. It also provides you cache interface so you can implement your own.
I need to be able to delete the saved menu data especially if its
saved on the external public directory as this is not removed when the
app is deleted etc. or if the user chooses to delete a saved menu from
within the app. I was thinking I could store the file image URIs or
location of the entire saved menu in database for this but not sure if
this is a good way
UIL generates unique filename for each loaded file using provided file link. You can delete any loaded image or cancel any download using link from your JSON.
I read in different sources and answers that in this use case for just
caching images to SD card etc. that I should specifically use a
network library to do so to avoid the allocation of a bitmap to heap
memory. I am using OK HTTP in my app at the moment.
UIL does it OK. It manages memory very accurately also provide you several options for memory management configuration. For example you can choose between several memory cache implementations out of the box.
In conclusion I suggest you to the visit the link above and read library documentation/description by yourself. It's very flexible and contatins lots of useful features.
Using a service can be a good option if you want the downloads to continue even if the user exits. Images that are stored in directories created using getExternalStorageDirectory() are automatically deleted when your app is uninstalled. Moreover you can check if the internal memory is large enough to store images. If you use this methods these images will be deleted upon the uninstallion of the app.
I use this class when downloading images, it caches the images, next time you will be downloading them it will just load from external memory, it manages the cache for you as well so you wont have to worry about setting cache to limited or unlimited, pretty efficient and fast.
public class ImageLoader {
MemoryCache memoryCache = new MemoryCache();
FileCache fileCache;
private Map<ImageView, String> imageViews = Collections
.synchronizedMap(new WeakHashMap<ImageView, String>());
ExecutorService executorService;
// Handler to display images in UI thread
Handler handler = new Handler();
public ImageLoader(Context context) {
fileCache = new FileCache(context);
executorService = Executors.newFixedThreadPool(5);
}
final int stub_id = R.drawable.placeholder;
public void DisplayImage(String url, ImageView imageView) {
imageViews.put(imageView, url);
Bitmap bitmap = memoryCache.get(url);
if (bitmap != null)
imageView.setImageBitmap(bitmap);
else {
queuePhoto(url, imageView);
imageView.setImageResource(stub_id);
}
}
private void queuePhoto(String url, ImageView imageView) {
PhotoToLoad p = new PhotoToLoad(url, imageView);
executorService.submit(new PhotosLoader(p));
}
private Bitmap getBitmap(String url) {
File f = fileCache.getFile(url);
Bitmap b = decodeFile(f);
if (b != null)
return b;
// Download Images from the Internet
try {
Bitmap bitmap = null;
URL imageUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection) imageUrl
.openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(true);
InputStream is = conn.getInputStream();
OutputStream os = new FileOutputStream(f);
Utils.CopyStream(is, os);
os.close();
conn.disconnect();
bitmap = decodeFile(f);
return bitmap;
} catch (Throwable ex) {
ex.printStackTrace();
if (ex instanceof OutOfMemoryError)
memoryCache.clear();
return null;
}
}
// Decodes image and scales it to reduce memory consumption
private Bitmap decodeFile(File f) {
try {
// Decode image size
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
FileInputStream stream1 = new FileInputStream(f);
BitmapFactory.decodeStream(stream1, null, o);
stream1.close();
// Find the correct scale value. It should be the power of 2.
// Recommended Size 512
final int REQUIRED_SIZE = 70;
int width_tmp = o.outWidth, height_tmp = o.outHeight;
int scale = 1;
while (true) {
if (width_tmp / 2 < REQUIRED_SIZE
|| height_tmp / 2 < REQUIRED_SIZE)
break;
width_tmp /= 2;
height_tmp /= 2;
scale *= 2;
}
// Decode with inSampleSize
BitmapFactory.Options o2 = new BitmapFactory.Options();
o2.inSampleSize = scale;
FileInputStream stream2 = new FileInputStream(f);
Bitmap bitmap = BitmapFactory.decodeStream(stream2, null, o2);
stream2.close();
return bitmap;
} catch (FileNotFoundException e) {
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
// Task for the queue
private class PhotoToLoad {
public String url;
public ImageView imageView;
public PhotoToLoad(String u, ImageView i) {
url = u;
imageView = i;
}
}
class PhotosLoader implements Runnable {
PhotoToLoad photoToLoad;
PhotosLoader(PhotoToLoad photoToLoad) {
this.photoToLoad = photoToLoad;
}
#Override
public void run() {
try {
if (imageViewReused(photoToLoad))
return;
Bitmap bmp = getBitmap(photoToLoad.url);
memoryCache.put(photoToLoad.url, bmp);
if (imageViewReused(photoToLoad))
return;
BitmapDisplayer bd = new BitmapDisplayer(bmp, photoToLoad);
handler.post(bd);
} catch (Throwable th) {
th.printStackTrace();
}
}
}
boolean imageViewReused(PhotoToLoad photoToLoad) {
String tag = imageViews.get(photoToLoad.imageView);
if (tag == null || !tag.equals(photoToLoad.url))
return true;
return false;
}
// Used to display bitmap in the UI thread
class BitmapDisplayer implements Runnable {
Bitmap bitmap;
PhotoToLoad photoToLoad;
public BitmapDisplayer(Bitmap b, PhotoToLoad p) {
bitmap = b;
photoToLoad = p;
}
public void run() {
if (imageViewReused(photoToLoad))
return;
if (bitmap != null)
photoToLoad.imageView.setImageBitmap(bitmap);
else
photoToLoad.imageView.setImageResource(stub_id);
}
}
public void clearCache() {
memoryCache.clear();
fileCache.clear();
}
}
To use it Just create an instance of it like
ImageLoader Imageloaer = new ImageLoader(getBaseContext());
Imageloaer.DisplayImage(imageUrl, imageView);
You should try downloading with this,
class DownloadFile extends AsyncTask<String,Integer,Long> {
ProgressDialog mProgressDialog = new ProgressDialog(MainActivity.this);// Change Mainactivity.this with your activity name.
String strFolderName;
#Override
protected void onPreExecute() {
super.onPreExecute();
mProgressDialog.setMessage("Downloading Image ...");
mProgressDialog.setIndeterminate(false);
mProgressDialog.setMax(100);
mProgressDialog.setCancelable(false);
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
mProgressDialog.show();
}
#Override
protected Long doInBackground(String... aurl) {
int count;
try {
URL url = new URL((String) aurl[0]);
URLConnection conexion = url.openConnection();
conexion.connect();
String targetFileName="downloadedimage.jpg";//Change name and subname
int lenghtOfFile = conexion.getContentLength();
String PATH = Environment.getExternalStorageDirectory()+"/myImage/";
File folder = new File(PATH);
if(!folder.exists()){
folder.mkdir();//If there is no folder it will be created.
}
InputStream input = new BufferedInputStream(url.openStream());
OutputStream output = new FileOutputStream(PATH+targetFileName);
byte data[] = new byte[1024];
long total = 0;
while ((count = input.read(data)) != -1) {
total += count;
publishProgress ((int)(total*100/lenghtOfFile));
output.write(data, 0, count);
}
output.flush();
output.close();
input.close();
} catch (Exception e) {}
return null;
}
protected void onProgressUpdate(Integer... progress) {
mProgressDialog.setProgress(progress[0]);
if(mProgressDialog.getProgress()==mProgressDialog.getMax()){
mProgressDialog.dismiss();
Toast.makeText(getApplicationContext(), "Download Completed !", Toast.LENGTH_LONG).show();
}
}
protected void onPostExecute(String result) {
}
}
This code will let you to download all the url of images,
new DownloadFile().execute("https://i.stack.imgur.com/w4kCo.jpg");
.....
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Change Folder Name as you desired and try to set this images to app bitmap and also avoiding rotate error of images by using this.
The important things to think about here is
Thread , Service ,File , Json, Context,Receiver & if else & for
maybe i did not understand your question but this is not a big deal sir,
your programm your app to work in way where your app starts when the os broadcast onBootCompleted, then create a Thread where you are going to do a lot of code - getting your json file-(when you need it), since its an array you get your jsonObject images, whether its a thousand or million you just iterate it and use any approach to download it, i'd say use the traditional way of downloading your images so as you better control it.
With the help of File class you save it alongside Context you can get your app's cache's directory, which is an internal memory save it there and create a column in your database where you can save the path to the file in your database as String.
When your app start in onPrepareOptionsMenu() check if your app's cache's directory is empty-if not you have some files, now since you have every file and its respective path you can check if it exists with File.exist() if it does no need to download.
if you need pace you can always create new Threads. The Reciever was to be the guy who gets notified when your device boots, if else for a lot of logic checking, for for your loopings, Service to be able to do long running work and have a way to communicate between the UI and background thread.
sorry for the last paragraph i was just trying to buy space :)

Decoding Bitmap stream failed

I'm trying to work my through "Sam's Teach Yourself Android Application Development in 24 Hours" and have become stuck in hour 12. The problem appears to be in this section:
private Drawable getQuestionImageDrawable(int questionNumber) {
Drawable image;
URL imageUrl;
try {
// Create a Drawable by decoding a stream from a remote URL
imageUrl = new URL(getQuestionImageUrl(questionNumber));
InputStream stream = imageUrl.openStream();
Bitmap bitmap = BitmapFactory.decodeStream(stream);
image = new BitmapDrawable(getResources(), bitmap);
} catch (Exception e) {
Log.e(TAG, "Decoding Bitmap stream failed");
image = getResources().getDrawable(R.drawable.noquestion);
}
return image;
}
The questionNumber and getQuestionImageUrl() have been tested and are returning what I believe are the correct values(1 and http://www.perlgurl.org/Android/BeenThereDoneThat/Questions/q1.png). There is an image at that url, but I always get the exception. I have tried several variations but when none of them worked I went back to this code from the book. What am I doing wrong here?
I'm new to java and android, so I'm probably missing something simple. I have had many other problems with the code in the book and the updated code from the website (all of which have been solved either here or with developer.android.com). This is my first question, so if I failed to provide any information please let me know.
I would do the following and it might work:
private Drawable getQuestionImageDrawable(int questionNumber) {
Drawable image;
URL imageUrl;
try {
// Create a Drawable by decoding a stream from a remote URL
imageUrl = new URL(getQuestionImageUrl(questionNumber));
HttpURLConnection conn = (HttpURLConnection) imageUrl.openConnection();
conn.setDoInput(true);
conn.connect();
InputStream stream = conn.getInputStream();
Bitmap bitmap = BitmapFactory.decodeStream(stream);
image = new BitmapDrawable(getResources(), bitmap);
} catch (Exception e) {
Log.e(TAG, "Decoding Bitmap stream failed");
image = getResources().getDrawable(R.drawable.noquestion);
}
return image;
}
Make sure you do this kind of heavy operations in a background thread insteaad of the main one and had INERNET permission to your application's manifest.
Let me know about your progress.
Probably the exception is because you are making a network connection form the app ui thread. That works on older Android versions but not in the newer.
Take a look to Android network operation section.
The main thing to do is use an AsyncTask

Android Load Camera image as Bitmap

I am using BitmapFactory.decodeFile to load Bitmaps of images into my application. However, the function returns null on large images (such as those from the camera). The filepath is definitely correct, I just can't figure out why it would return null. I tried supersampling, but it didn't seem to help.
Does anyone have any idea why it would do this or how I could more easily load images taken from the camera into a Bitmap?
Here's the code I am using:
public static Bitmap loadBitmap(String filePath){
Bitmap result = BitmapFactory.decodeFile(filePath);
if(result == null){
if(filePath.contains(".jpg") || filePath.contains(".png")){
//This is the error that occurs when I attempt to load an image from the Camera DCIM folder or a large png I imported from my computer.
Utils.Toast("Could not load file -- too big?");
} else {
Utils.Toast("Could not load file -- image file type is not supported");
}
}
return result;
}
You will need to provide more info about your problem, such as a snippet of code that you are using. If you want to know when/why the BitmapFactory.decodeFile method would return null, you can read directly its source code: http://casidiablo.in/BitmapFactory
For example, one of the reasons that causes BitmapFactory.decodeFile to return null is if there's a problem while openning the file. Curiously, the developers dont't log anything with such a problem... look at the comment "do nothing. If the exception happened on open, bm will be null."
public static Bitmap decodeFile(String pathName, Options opts) {
Bitmap bm = null;
InputStream stream = null;
try {
stream = new FileInputStream(pathName);
bm = decodeStream(stream, null, opts);
} catch (Exception e) {
/* do nothing.
If the exception happened on open, bm will be null.
*/
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
// do nothing here
}
}
}
return bm;
}
As you can see, the BitmapFactory.decodeFile does not work standalone... but it uses some other methods of the BitmapFactory class (for instance, BitmapFactory.decodeStream, BitmapFactory.nativeDecodeStream, BitmapFactory.finishDecode, etc.). The problem could be on one of those methods, so if I were you, I would try to read and understand how they work so that I could know in which cases they return null.
Maybe inSampleSize option can help you?
Strange out of memory issue while loading an image to a Bitmap object
It may sound obvious, but check that your filePath actually points to a file. You mention that you are using a file manager to select the image to open - it's possible that the file manager is returning a path to a content provider instead of a file.
There is a more robust way to open files using the ContentResolver class, which can open an InputStream to a content provider, file, or resource, without you needing to know in advance what kind of path you are passing it.
The only catch is that you need to pass a Uri object on the call to openInputStream() instead of a String.
public static Bitmap loadBitmap(String filePath, Context c) {
InputStream inStream;
try {
inStream = c.getContentResolver().openInputStream( Uri.parse(filePath) );
} catch (FileNotFoundException e) {
// handle file not found
}
return BitmapFactory.decodeStream(inStream);
}
This also happens to be how the ImageView widget attempts to load images when you use its setImageURI method.

Categories

Resources