Flutter image-picker explicitly ask permission - android

Imagepicker package says
No configuration required - the plugin should work out of the box.
It is no longer required to add
android:requestLegacyExternalStorage="true" as an attribute to the
tag in AndroidManifest.xml, as image_picker has been
updated to make use of scoped storage.
reading images from gallery.
so I think I need to ask some permission from the user as playstore also says this
New package is just working and not asking for any permission.
What permissions I need to explicitly ask
And I don't want to save it on any external directory I just want to upload image to firebase storage
Edit: image picker is not asking any permission from the user is this wrong

Permission needed to read and write files in the android are.
These permission are required to be added to your AndroidManifest.xml
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
In your scenario, you don't need to do anything as this is already handled by the library
https://pub.dev/packages/image_picker
Above mentioned library doesn't save the image in external storage.
Note: Images and videos picked using the camera are saved to your
application's local cache, and should therefore be expected to only be
around temporarily. If you require your picked image to be stored
permanently, it is your responsibility to move it to a more permanent
location.
For more info you can refer to this link
https://guides.codepath.com/android/Accessing-the-Camera-and-Stored-Media#accessing-stored-media
Update : How image picking is handled internally in image_picker for Android
For Gallery pick it opens in inbuild file picker intent using ACTION_GET_CONTENT(about action get content)
When opening file using ACTION_GET_CONTENT - Because the user is involved in selecting the files or directories that your app can access, this mechanism doesn't require any system permissions. You can read more about when permission is needed and when not in google docs
Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
pickImageIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}
pickImageIntent.setType("image/*");
activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY);
and copies the result URI in temp file in cache directory and return the path
String extension = getImageExtension(context, uri);
inputStream = context.getContentResolver().openInputStream(uri);
file = File.createTempFile("image_picker", extension, context.getCacheDir());
file.deleteOnExit();
outputStream = new FileOutputStream(file);
if (inputStream != null) {
copy(inputStream, outputStream);
success = true;
}
For Camera library request the camera permission android.permission.CAMERA from the user and save the camera image in app cache directory.
private void handleCaptureImageResult(int resultCode) {
if (resultCode == Activity.RESULT_OK) {
fileUriResolver.getFullImagePath(
pendingCameraMediaUri != null
? pendingCameraMediaUri
: Uri.parse(cache.retrievePendingCameraMediaUriPath()),
new OnPathReadyListener() {
#Override
public void onPathReady(String path) {
handleImageResult(path, true);
}
});
return;
}
// User cancelled taking a picture.
finishWithSuccess(null);
}
This code is as per version image_picker: ^0.8.4+4 code present on their github page - image picker code

You’ll have to add the above-mentioned permissions to your AndroidManifest.xml file. That is still required to access user storage.

Related

How to create a public directory on the external storage?

I am trying to save public files on the external storage.
I am following the example from Android Developers page:
https://developer.android.com/training/data-storage/files#PublicFiles
First I tried to create a directory "mydocuments" in the public directory DIRECTORY_DOCUMENTS. The code is as simple as
TextView tv= findViewById(R.id.myTextview);
// Get the directory for the user's public pictures directory.
File documents= Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOCUMENTS);
tv.append("\n documents directory=" + documents);
File file = new File(documents, "mydocuments");
tv.append("\n\nDirectory to be created=" + file);
if (file.exists())
tv.append("\n\nFile already exists:" + file.getAbsolutePath());
else{
if (!file.mkdirs())
tv.append("\n\nDirectory not created:" + file.getAbsolutePath());
I added permissions in the manifest file
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE">
</uses-permission>
However when I run this in a device (be virtual or physical) the result is always the same:
documents directory=/storage/emulated/0/Documents
Directory to be created=/storage/emulated/0/Documents/mydocuments
Directory not created:/storage/emulated/0/Download/mydocuments
How to make the Documents directory writeable?
Thank you in advance.
You are #1278 with this problem this year.
For Android 6+ you have to add code to ask the user to confirm the permissions you requested in manifest file.
Google for runtime permissions.
You could also go to the settings of your app and toggle the storage permission to on.
Thanks to #greenapps suggestion I solved my problem. This is the simplest solution I found to write in the Documents directory on the SD card, as explained in
https://developer.android.com/training/permissions/requesting
I have copied some of that here:
1.- First ask for user permissions somewhere at the begining of the code:
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
MY_PERMISSIONS);
where I previously defined the constant
final int MY_PERMISSIONS=1;
2.- Then implement my code above to write on the SD card inside the method
#Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted, yay! Do the
// task you need to do.
writeSD(); // this is the code to write on the SD card shown above
} else {
// permission denied, boo! Disable the
// functionality that depends on this permission.
tv.append("\n\n No permission given");
}
return;
}
// other 'case' lines to check for other
// permissions this app might request
}
}
This solves the issue for me. The first time it is executed, a dialog opens asking for the permissions. After that It is allowed to read the contents of the sdcard and to write in the public directories DOCUMENTS, PICTURES, DOWNLOADS, etc.

share app screenshot without WRITE_EXTERNAL_STORAGE permission?

Is it possible to share own app screenshots without adding any permissions ?
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
And this is my others apps code which is works on copy to cache and share
public void sendFile(File paramFile) {
String fileExtension = ".apk";
File temporaryFile;
try {
temporaryFile = File.createTempFile(paramFile.getName(), fileExtension, getApplicationContext().getExternalCacheDir());
copy(paramFile, temporaryFile);
} catch (Exception e) {
temporaryFile = paramFile;
}
Intent localIntent = new Intent();
localIntent.setAction("android.intent.action.SEND");
localIntent.putExtra("android.intent.extra.STREAM", Uri.fromFile(temporaryFile));
localIntent.setType("application/*");
localIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(localIntent, ""));
}
Save your image to internal storage (e.g., getCacheDir()), then use FileProvider to make it available to other apps. See the documentation for more.
You are using getExternalCacheDir()
it absolute the path to youraplication specific directory on the primary
external storage device where the application can place cache
files it owns. These files are internal to the application, and not
typically visible to the user as media.
There is no security enforced with these files.
no need to add the permissions to read or write to the returned path. it's always accessible to the youraplication app. This only applies to paths generated for package name of the youraplication.
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
android.Manifest.permission.READ_EXTERNAL_STORAGE

java.io.IOException: Cannot make changes to file

I am using JAudioTagger library for reading and writing tags for an audio file. I am able to read the tags but unable to write them.
I am retrieving audio file path like this :
private String getSongPath(long songId) {
String path = null;
ContentResolver contentResolver = getContentResolver();
Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] projection = {MediaStore.Audio.Media.DATA};
String selection = MediaStore.Audio.Media._ID + " == ?";
String[] selectionArgs = {String.valueOf(songId)};
Cursor cursor = contentResolver.query(uri, projection, selection, selectionArgs, null);
if (cursor != null) {
int pathCol = cursor.getColumnIndexOrThrow(projection[0]);
cursor.moveToFirst();
path = cursor.getString(pathCol);
cursor.close();
}
return path;
}
Then to write tags using JAudioTagger :
File songFile = new File(path); // path looks like /storage/3932-3434/Music/xyz.mp3
AudioFile audiofile = = AudioFileIO.read(songFile);
Tag tag = = audiofile.getTag();
tag.setField(FieldKey.TITLE, title);
// some more setField calls for different feilds
audiofile.commit();
The commit() method is giving following Exception :
org.jaudiotagger.audio.exceptions.CannotWriteException:
java.io.IOException: Cannot make changes to file xyz.mp3 at
org.jaudiotagger.audio.mp3.MP3File.commit(MP3File.java:799) at
com.techapps.musicplayerplus.MainActivity$17.onClick(MainActivity.java:2125)
at
android.support.v7.app.AlertController$ButtonHandler.handleMessage(AlertController.java:157)
at android.os.Handler.dispatchMessage(Handler.java:102) at
android.os.Looper.loop(Looper.java:148) at
android.app.ActivityThread.main(ActivityThread.java:5417) 06-18
10:59:48.134 8802-8802/com.techapps.musicplayerplus W/System.err:
at java.lang.reflect.Method.invoke(Native Method) at
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) Caused
by: java.io.IOException: Cannot make changes to file Saibo.mp3 at
org.jaudiotagger.audio.mp3.MP3File.precheck(MP3File.java:824) at
org.jaudiotagger.audio.mp3.MP3File.save(MP3File.java:850) at
org.jaudiotagger.audio.mp3.MP3File.save(MP3File.java:783) at
org.jaudiotagger.audio.mp3.MP3File.commit(MP3File.java:795)
I am running this code on Android 6 while my app is targeted at SDK 22. I have also mentioned following permission in manifest.
android.permission.WRITE_EXTERNAL_STORAGE
Still I am unable to write to SD card. Please help me. Thanks in advance.
You have to use Storage Access Framework (SAF) to access SD Card from API 19 (Kitkat) onward.
First we need to ask user to provide a URI of the folder we want to access. If we want access to entire SD card, user needs to provide URI of SD card's root folder.
For example, when user hits Edit button, we have to first show hint dialog box, asking user to select required directory in SD Card which we want to access. You can display following image in hint dialog box to ask user to select root directory of SD Card :
When user dismisses hint dialog box, you need to trigger Storage Access Framework :
private void triggerStorageAccessFramework() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
}
public final void onActivityResult(final int requestCode, final int resultCode, final Intent resultData) {
if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE_STORAGE_ACCESS) {
Uri treeUri = null;
// Get Uri from Storage Access Framework.
treeUri = resultData.getData();
pickedDir= DocumentFile.fromTreeUri(this, treeUri);
if (!isSDCardRootDirectoryUri(treeUri)) {
Toast.makeText(this, "Wrong directory selected. Please select SD Card root directory.", Toast.LENGTH_LONG).show();
createSDCardHintDialog().show();
return;
}
// Persist URI in shared preference so that you can use it later.
SharedPreferences sharedPreferences = getSharedPreferences(App.PREFERENCE_FILENAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(App.SDCARD_URI_KEY, treeUri.toString());
editor.apply();
// Persist access permissions, so you dont have to ask again
final int takeFlags = resultData.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
}
private boolean isSDCardRootDirectoryUri(Uri treeUri) {
String uriString = treeUri.toString();
return uriString.endsWith("%3A");
}
Once you get Uri of user picked directory, you can perform write operation using SAF : (creadit : this answer )
public void writeFile(DocumentFile pickedDir) {
try {
DocumentFile file = pickedDir.createFile("image/jpeg", "try2.jpg");
OutputStream out = getContentResolver().openOutputStream(file.getUri());
try {
// write the image content
} finally {
out.close();
}
} catch (IOException e) {
throw new RuntimeException("Something went wrong : " + e.getMessage(), e);
}
}
It could be that you pointing to non existing file.
Check your path file by using Log.
Log.d("Activity", "path = " + path);
Android-M or API 23 introduced Runtime Permissions for reducing security flaws in android device.
To update your apps using Google Play services to handle Android 6.0 permissions, it’s good practice to manage the user’s expectations in setting permissions that the runtime may require. The following link will help you avoid potential issues.
https://developer.android.com/training/permissions/requesting.html
have you declared the permission
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> ?
I saw that you already created an issue in the JAudioTagger GitHub repository which was advisable, but never got a universally working solution. My findings so far:
The answer mentioning SAF is correct, but it won't help you as SAF will provide a DocumentFile, not a File.
You might try to modify JAudioTagger to your needs, replacing File with DocumentFile, but the latter one has not all functions you will need.
Also InputStream and OutputStream will not help you, as JAudioTagger needs File and internally heavily uses RandomAccessFile which is not available either.
Google "forgot" to provide some getRandomAccessFileFromUri() which makes things even worse (Yes, there are hacks using Java reflection to work around this limitation...).
The "/proc/self/fd" method (How to handle SAF when I can only handle File or file-path?) will also not work immediately, as JAudioTagger needs copy and renaming functions that are not applicable to this kind of files. Particularly JAudioTagger will not find a suitable file name extension like ".m4a". Of course you could try to change JAudioTagger accordingly.
You might follow the advice to make a copy of the file to your personal storage, then apply JAudioTagger to it and finally copy it back to SD card, but:
If you want to use JAudioTagger to read from SD card, this will, as announced by Google, fail with Android 10. Starting with that version, you will not even have read access to the SD card via the File interface.
Further, the File interface gives you read access to SD cards with Android 9 and below, but not to other SAF devices, like USB OTG memory or SMB shares etc.
Of course you could also copy each file in order to read its metadata, but this will be awfully slow and is not suitable if you have more than a few files.
So my current advices are:
Try the "/proc/self/fd" method and modify JAudioTagger accordingly.
If the changes are too heavy, use the fd method for reading the tags and the copy method for writing.
BTW: I am currently modifying an older version of JAudioTagger for using both File and DocumentFile transparently, but the changes are tremendous, bear a high risk, need some help classes, and the work is unfinished, yet.
BTSW: The DocumentFile functions are painfully slow, compared to the File functions.

How can I let users access the internal storage directory of my app?

My app stores files in its internal storage directory (/Android/data/com.mycompany.myapp/files, as returned by getFilesDir()), and I would like to allow users to access those files directly from a file management app on their mobile device or the Android File Transfer desktop appplication.
The Storage Options developer guide says:
By default, files saved to the internal storage are private to your application and other applications cannot access them (nor can the user).
"By default" implies that I can change the default permissions to allow users to access these files, but I can't find any documentation of how to do that. Currently the com.mycompany.myapp directory is hidden when I browse the /Android/data directory with a file management app.
I'm currently saving file data like this:
String fileName = "myfile.jpg";
String filePath = getFilesDir() + File.separator + fileName;
FileOutputStream fileStream = new FileOutputStream(filePath);
fileData.writeTo(fileStream); // fileData is a ByteArrayOutputStream
fileStream.close();
I tried setting the permissions of the individual files to world-readable, but this didn't make the directory visible:
FileOutputStream fileStream = this.app.openFileOutput(fileName, Context.MODE_WORLD_READABLE);
I also checked the documentation for the AndroidManifest file and didn't see anything there. Does anyone know how I can do this?
I took a closer look at the result of getFilesDir() vs getExternalFilesDir() and found that getFilesDir() returns /data/data/[packagename]/files while getExternalFilesDir() returns /Android/data/[packagename]/files. I thought the app files I was browsing in /Android/data were the internal storage directories, but now I see that those are actually the external storage directories.
If the internal storage directories are indeed never available to regular users, I wish the documentation said that instead of saying they are not available "by default." To me at least, saying "by default" implies that the default behavior can be changed.
Anyway, I tested and confirmed that if I delete my app, files saved to the getExternalFilesDir() are deleted automatically. So that meets my need for a storage location that is clearly connected with the app (uses an app-specific directory name and is deleted with the app) but is accessible to users for occasional manual file management.
Here's a comparison that might be helpful to someone else reading this:
getFilesDir() - creates an app-specific directory; hidden from users; deleted with the app
getExternalFilesDir() - creates an app-specific directory; accessible to users; deleted with the app
getExternalStoragePublicDirectory() - uses a shared directory (e.g., Music); accessible to users; remains when the app is deleted
I think you are getting confused by the developers documentation, I can't blame you, it's not the best documentation I've ever read. Anyway, Android provides two ways of saving files to the file system:
Using the Internal Storage
Using the External Storage
The Internal Storage is ALWAYS available and is ONLY accessible to your app, no other app in the system can access the files your app saved to this partition.
Now, I think what you need is the External Storage because it is "world-readable" and can be accessed by anyone and any any apps. The downside is that it may not be available since the user could mount it on a USB device - which can be removed by the user at anytime.
You shouldn't use neither MODE_WORLD_WRITEABLE nor MODE_WORLD_READABLE as per the documentation states, because it is very dangerous. Furthermore, these constants have been deprecated since API Level 17
Recommendation
If you need to make your files public, then save them to the External Storage. You will need to declare a permission in your manifest file to avoid your app from crashing everytime it access the External Storage...
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
because the External Storage might not be available you will need to determine the state of it before you perform any operation, otherwise your app will crash...
public enum StorageState{
NOT_AVAILABLE, WRITEABLE, READ_ONLY
}
public StorageState getExternalStorageState() {
StorageState result = StorageState.NOT_AVAILABLE;
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return StorageState.WRITEABLE;
}
else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
return StorageState.READ_ONLY;
}
return result;
}
There is more information on the documentation and things that you should be aware of. For example, you can give your app ownership of these files so that when your app is uninstalled the system can automatically delete these files. For more info please refer to the documentation on the Android Developers Site
add this to manifest
then add this in your activity:
private static final String RequieredPermission = Manifest.permission.READ_EXTERNAL_STORAGE;
then call HandlePermission() where your require to check for permission, usually in Oncreate method of an activity. These functions are then required:
#Override
public void onRequestPermissionsResult(int requestCode, #NonNull String[] permissions, #NonNull int[] grantResults) {
Log.d("amir", "onRequestPermissionsResult: called");
switch (requestCode) {
case REQUEST_PERMISSION_READING_STATE:
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(MainActivity.this, "Permission Granted!", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "Permission Denied!", Toast.LENGTH_SHORT).show();
}
}
}
private void HandlePermission() {
int permissionCheck = ContextCompat.checkSelfPermission(
this, RequieredPermission);
if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
RequieredPermission)) {
showExplanationaboutPermission("Permission Needed", "Rationale", RequieredPermission, REQUEST_PERMISSION_READING_STATE);
} else {
requestPermission(RequieredPermission, REQUEST_PERMISSION_READING_STATE);
}
} else {
Toast.makeText(MainActivity.this, "Permission (already) Granted!", Toast.LENGTH_SHORT).show();
}
}
private void showExplanationaboutPermission(String title,
String message,
final String permission,
final int permissionRequestCode) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(title)
.setMessage(message)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
requestPermission(permission, permissionRequestCode);
}
});
builder.create().show();
}
private void requestPermission(String permissionName, int permissionRequestCode) {
ActivityCompat.requestPermissions(this,
new String[]{permissionName}, permissionRequestCode);
}

Android: OpenCV: imwrite always returns false and fails to write

This code used to work but on Android 4.2 and OpenCV 2.4.4 it fails, but I don't know why. Can anyone shed any light on it for me?
Thanks for any help.
Baz
public void SaveImage (Mat mat) {
Mat mIntermediateMat = new Mat();
Imgproc.cvtColor(mRgba, mIntermediateMat, Imgproc.COLOR_RGBA2BGR, 3);
File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
String filename = "barry.png";
File file = new File(path, filename);
Boolean bool = null;
filename = file.toString();
bool = Highgui.imwrite(filename, mIntermediateMat);
if (bool == true)
Log.d(TAG, "SUCCESS writing image to external storage");
else
Log.d(TAG, "Fail writing image to external storage");
}
bool comes back false every time. The file path/name is as it should be ("storage/emulated/0/Pictures/barry.png") for User #0 and the directory Pictures is there.
What can make imwrite return false?
I could convert the mat to a bmp and save it myself (which I do elsewhere in a non-OpenCV app) but since Highgui.imwrite is there, and this code used to work when I was writing to 2.4.0 and testing on Android 3.x, I'd like to use it if I can.
Many thanks
Baz
I can't believe it. [facepalm].
Add this to your manifest file Barry:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Would somebody come and slap me please? I managed to remove the write permission from the manifest so really there shouldn't be any surprise that it wouldn't write...
Doh.
Barry
There are actually two possible problems:
Android permissions. To fix this problem add the permission to the manifest
"android.permission.WRITE_EXTERNAL_STORAGE"
You are trying to save file to SD card and your device is connected to PC in "Disk drive"
mode. It means that you share the access rights with PC. Connect your device in mode "Charge only". To check the connection mode use
Environment.getExternalStorageState(), it will return shared for "Disk mode" and mounted for "Charge only".
I modified your code and now it looks like
public void SaveImage (Mat mat) {
Mat mIntermediateMat = new Mat();
Imgproc.cvtColor(mRgba, mIntermediateMat, Imgproc.COLOR_RGBA2BGR, 3);
File path = new File(Environment.getExternalStorageDirectory() + "/Images/");
path.mkdirs();
File file = new File(path, "image.png");
filename = file.toString();
Boolean bool = Highgui.imwrite(filename, mIntermediateMat);
if (bool)
Log.i(TAG, "SUCCESS writing image to external storage");
else
Log.i(TAG, "Fail writing image to external storage");
}
Take care with versions +4.4 (KitKat) because android.permission.WRITE_EXTERNAL_STORAGE doesn't allow to write on the external SD card:
The WRITE_EXTERNAL_STORAGE permission must only grant write access to
the primary external storage on a device. Apps must not be allowed to
write to secondary external storage devices, except in their
package-specific directories as allowed by synthesized permissions.
Restricting writes in this way ensures the system can clean up files
when applications are uninstalled.
Take a look to: Writing to SD Card always failing
From Android 6+
Beginning in Android 6.0 (API level 23), users grant permissions to apps while the app is running, not when they install the app...
And you need to add something like:
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
{
ActivityCompat.requestPermissions(
this,
new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE },
PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE);
}
https://developer.android.com/training/permissions/requesting.html

Categories

Resources