I am trying to create a file on sd-card on lollipop device. I am aware of ACTION_OPEN_DOCUMENT_TREE, and how to get permission for root of sd card.
What I want to achieve is this:
in my own folder browser, user picks a folder (on sdcard) where he wants file to be created (for example "/storage/emulated/0/a/b/c/d")
first time this happens, I use ACTION_OPEN_DOCUMENT_TREE, and then in onActivityResult I use findFile to create file in correct location:
next time user picks a folder on sd-card, he does not need to use ACTION_OPEN_DOCUMENT_TREE
code:
public void test()
{
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, 1);
}
public void onActivityResult(int requestCode, int resultCode, Intent resultData)
{
if (resultCode == RESULT_OK)
{
Uri treeUri = resultData.getData();
final int takeFlags = resultData.getFlags() & Intent.FLAG_GRANT_READ_URI_PERMISSION;
getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
//assuming he picked "/storage/emulated/0/a/b/c/d"
DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);
DocumentFile a = pickedDir.findFile("a");
DocumentFile b = a.findFile("b");
DocumentFile c = b.findFile("c");
DocumentFile d = c.findFile("d");
DocumentFile newFile = d.createFile("text/plain", "somefile.txt");
OutputStream out;
try
{
out = getContentResolver().openOutputStream(newFile.getUri());
out.write("A long time ago...".getBytes());
out.close();
}
catch (FileNotFoundException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
The question is how do I know in onActivityResult that user actually picked root of sdcard? He could have picked /storage/emulated/0/a1/a2, and if that folder has subfolders a/b/c/d, I would create file in wrong folder (because findFile("a");findFile("b"); etc.. would also succeed).
Also, next time user picks a folder (with my own folder picker), I get path, not Uri, how do I translate that path to Uri which can be used with DocumentFile?
You still have read access to the removable external storage using File. So you can create a temporary file with a unique name in the directory of the Uri that came back from ACTION_OPEN_DOCUMENT_TREE, using DocumentFile. and Uri. Then check if it is where you think it should be using File and the path. The following code returns true if myPath matches treeUri.
String myPath = "/storage/sdcard1/Podcasts";
Uri treeUri = resultIntent.getData();
int i = 0;
File f;
String s;
while ((f = new File(myPath + (s = "/tmp" + i + ".mp3"))).exists()) ++i;
final DocumentFile d = treeDir.createFile("audio/mp3", s);
if (d == null) return false;
try {
OutputStream str = getContentResolver().openOutputStream(d.getUri());
str.write(new byte[10]);
str.close();
} catch (IOException e) {
return false;
}
final boolean fExists = f.exists();
d.delete();
return fExists;
Related
I am writing a new Application on Android 11 (SDK Version 30) and I simply cannot find an example on how to save a file to the external storage.
I read their documentation and now know that they basicly ignore Manifest Permissions (READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE). They also ignore the android:requestLegacyExternalStorage="true" in the manifest.xml application tag.
In their documentation https://developer.android.com/about/versions/11/privacy/storage they write you need to enable the DEFAULT_SCOPED_STORAGE and FORCE_ENABLE_SCOPED_STORAGE flags to enable scoped storage in your app.
Where do I have to enable those?
And when I've done that how and when do I get the actual permission to write to the external storage? Can someone provide working code? I want to save .gif, .png and .mp3 files. So I don't want to write to the gallery.
Thanks in advance.
Corresponding To All Api, included Api 30, Android 11 :
public static File commonDocumentDirPath(String FolderName)
{
File dir = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
{
dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/" + FolderName);
}
else
{
dir = new File(Environment.getExternalStorageDirectory() + "/" + FolderName);
}
// Make sure the path directory exists.
if (!dir.exists())
{
// Make it, if it doesn't exit
boolean success = dir.mkdirs();
if (!success)
{
dir = null;
}
}
return dir;
}
Now, use this commonDocumentDirPath for saving file.
A side note from comments, getExternalStoragePublicDirectory with certain scopes are now working with Api 30, Android 11. Cheers! Thanks to CommonsWare hints.
You can save files to the public directories on external storage.
Like Documents, Download, DCIM, Pictures and so on.
In the usual way like before version 10.
**Simplest Answer and Tested ( Java ) **
private void createFile(String title) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/html");
intent.putExtra(Intent.EXTRA_TITLE, title);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.parse("/Documents"));
}
createInvoiceActivityResultLauncher.launch(intent);
}
private void createInvoice(Uri uri) {
try {
ParcelFileDescriptor pfd = getContentResolver().
openFileDescriptor(uri, "w");
if (pfd != null) {
FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());
fileOutputStream.write(invoice_html.getBytes());
fileOutputStream.close();
pfd.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/////////////////////////////////////////////////////
// You can do the assignment inside onAttach or onCreate, i.e, before the activity is displayed
String invoice_html;
ActivityResultLauncher<Intent> createInvoiceActivityResultLauncher;
#Override
protected void onCreate(Bundle savedInstanceState) {
invoice_html = "<h1>Just for testing received...</h1>";
createInvoiceActivityResultLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
// There are no request codes
Uri uri = null;
if (result.getData() != null) {
uri = result.getData().getData();
createInvoice(uri);
// Perform operations on the document using its URI.
}
}
});
I'm using this method and it really worked for me
I hope I can help you. Feel free to ask me if something is not clear to you
Bitmap imageBitmap;
OutputStream outputStream ;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
{
ContentResolver resolver = context.getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME,"Image_"+".jpg");
contentValues.put(MediaStore.MediaColumns.MIME_TYPE,"image/jpeg");
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH,Environment.DIRECTORY_PICTURES + File.separator+"TestFolder");
Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,contentValues);
try {
outputStream = resolver.openOutputStream(Objects.requireNonNull(imageUri) );
imageBitmap.compress(Bitmap.CompressFormat.JPEG,100,outputStream);
Objects.requireNonNull(outputStream);
Toast.makeText(context, "Image Saved", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Toast.makeText(context, "Image Not Not Saved: \n "+e, Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
manifest file (Add Permission)
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
I can save the captured image to Pictures folder however i cannot save it to app folder. I give permissions for camera and write permission dynamically. I write read write camera permission in manifests.xml. I checked permission at debug mode. There is no problem with permissions.
Camera activity starts and i take picture and click OK. Then in onActivityResult() i checked the image file's size.It's zero byte. Image file exists but zero length.
Here is how i retrieve image path :
public static File getImageFile(Context context, int food_id) {
try {
//File storageDir = new File(context.getFilesDir().getAbsolutePath() + File.separator + IMAGES_DIRECTORY); // not works !!!!!!!!!
File storageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString() + File.separator + IMAGES_DIRECTORY); // works
if (!storageDir.exists()) {
storageDir.mkdirs();
}
File photoFile = new File(storageDir.getAbsolutePath() + File.separator + food_id + ".jpg");
/* if(!photoFile.exists())
photoFile.createNewFile();*/
return photoFile;
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
if (Build.VERSION.SDK_INT >= 23) {
hasPermissionCamera = ContextCompat.checkSelfPermission(FoodDetailsActivity.this, Manifest.permission.CAMERA);
if (hasPermissionCamera != PackageManager.PERMISSION_GRANTED) {
if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
getErrorDialog(getString(R.string.permission_error_dialog_camera), FoodDetailsActivity.this, true).show();
} else {
requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CODE_ASK_PERMISSIONS_CAMERA);
}
} else { // open camera
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (cameraIntent.resolveActivity(getPackageManager()) != null) // intent düzgün mü diye kontrol eder.
{
File photoFile = AppUtil.getImageFile(FoodDetailsActivity.this,food_id);
if (photoFile != null) {
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile));
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
startActivityForResult(cameraIntent, REQUEST_IMAGE_CAPTURE);
}
} else {
}
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent intentx) {
super.onActivityResult(requestCode, resultCode, intentx);
if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
File imageFile = AppUtil.getImageFile(this,food_id);
try {
mImageBitmap = BitmapFactory.decodeFile(imageFile.getAbsolutePath()); // mImageBitmap is null here. imageFile exists.
Log.d("eeffs","size : " + imageFile.length() + " - exists() : " + imageFile.exists()); exists return true. length is zero
int widthBitmap = mImageBitmap.getWidth(); // exception here because bitmap is null
...
} catch (IOException e) {
e.printStackTrace();
}
}
}
i cannot save it to app folder
I am going to guess that you mean:
File storageDir = new File(context.getFilesDir().getAbsolutePath() + File.separator + IMAGES_DIRECTORY); // not works !!!!!!!!!
Third party apps have no ability to write to your app's portion of internal storage, and you are invoking a third-party camera app via ACTION_IMAGE_CAPTURE.
You can use FileProvider and its getUriForFile() method to provide selective access to your app's portion of internal storage. This sample app demonstrates the technique, where I also write to a location inside of getFilesDir().
As a bonus, using FileProvider will allow you to get rid of that ugly StrictMode hack that you are using to try to get past the ban on file Uri schemes.
TL:DR; I explained how to use create folders and subfolders using DocumentFile and how to delete file created using this class. Uri returned from onActvityResult() and documentFile.getUri.toString() are not same. My question is how to get a valid Uri to manipulate folders and files without using SAF UI, if possible not without using hack.
Let me share what i've learned so far and ask my questions.
If you want get Uri of folder and work on it, you should use Intent with ACTION_OPEN_DOCUMENT_TREE to get an Uri to access folders and set W/R permission for that uri.
Persistible permission granted onActivityResult with:
final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Check for the freshest data.
getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
If you select the main folder of device:
Uri treeUri = data.getData();
treeUri.toString()
Returns: content://com.android.externalstorage.documents/tree/primary:
File mediaStorageDir = new File(Environment.getExternalStorageDirectory(), "");
Returns: storage/emulated/0
new File(treeUri.toString()).getAbsolutePath();
Returns: content:/com.android.externalstorage.documents/tree/primary:
If you use the DocumentFile class for getting path of the main folder you get
DocumentFile saveDir = null;
saveDir = DocumentFile.fromFile(Environment.getExternalStorageDirectory());
String uriString = saveDir.getUri().toString();
Returns: file:///storage/emulated/0
My first question is how can get the Uri with content using DocumentFile class.
I'm building a photography app and as default i'd like to set an initial folder for images using DocumentFile class.
#TargetApi(19)
protected DocumentFile getSaveDirMainMemory() {
DocumentFile saveDir = null;
saveDir = DocumentFile.fromFile(Environment.getExternalStorageDirectory());
// saveDir =
// DocumentFile.fromFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
// saveDir =
// DocumentFile.fromFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES));
DocumentFile newDir = null;
/*
* Check or create Main Folder
*/
// Check if main folder exist
newDir = saveDir.findFile(DIR_MAIN);
// Folder does not exist, create it
if (newDir == null || !newDir.exists()) {
newDir = saveDir.createDirectory(DIR_MAIN);
}
/*
* Check or create Sub-Folder
*/
DocumentFile newSubDir = null;
// Check if sub-folder exist
newSubDir = newDir.findFile(DIR_SUB);
// Folder does not exist, create it
if (newSubDir == null || !newSubDir.exists()) {
newSubDir = newDir.createDirectory(DIR_SUB);
}
if (newSubDir != null && newSubDir.exists()) {
return newSubDir;
} else if (newDir != null && newDir.exists()) {
return newDir;
} else {
return saveDir;
}
}
This method creates DIR_MAIN/DIR_SUB inside main memory of the device or PICTURES or DCIM folder depending on choice. Using this default folder i save images to this created sub folder.
I get newSubDir.getUri().toString(): file:///storage/emulated/0/MainFolder/SubFolder I named DIR_MAIN MainFolder, DIR_SUB: SubFolder to test.
To access or delete images i use this path and image name i created as
DocumentFile imageToDeletePath = DocumentFile.fromFile(new File(lastSavedImagePath));
DocumentFile imageToDelete = imageToDeletePath.findFile(lastSavedImageName);
imageDelete returns null because Uri is not in correct format.
If i open SAF ui and get UI onActivityResult and save it as string i use this method to get a directory and check Uri permissions
#TargetApi(19)
protected DocumentFile getSaveDirNew(String uriString) {
DocumentFile saveDir = null;
boolean canWrite = isUriWritePermission(uriString);
if (canWrite) {
try {
saveDir = DocumentFile.fromTreeUri(MainActivity.this, Uri.parse(uriString));
} catch (Exception e) {
saveDir = null;
}
}
return saveDir;
}
Check if Uri from string has write permission, it may not have if you don't take or remove persistable permissions.
private boolean isUriWritePermission(String uriString) {
boolean canWrite = false;
List<UriPermission> perms = getContentResolver().getPersistedUriPermissions();
for (UriPermission p : perms) {
if (p.getUri().toString().equals(uriString) && p.isWritePermission()) {
Toast.makeText(this, "canWrite() can write URI:: " + p.getUri().toString(), Toast.LENGTH_LONG).show();
canWrite = true;
break;
}
}
return canWrite;
}
After saving image with valid uri and using
DocumentFile imageToDeletePath = DocumentFile.fromTreeUri(this, Uri.parse(lastSavedImagePath));
DocumentFile imageToDelete = imageToDeletePath.findFile(lastSavedImageName);
Uri.fromFile() and DocumentFile.fromTreeUri() create uris from two different worlds.
While they currently look very similar, this is just coincidence and could change with any future Android release.
There is no "non-hacky" way to convert from one to the other. If you want a dirty solution, you can go for reflection (view the source code of DocumentFile.fromTreeUri and possibly use the Storage class on newer Android versions.
Also see:
Android - Storage Access Framework - Uri into local file
In my application of image gallery I use media content provider image to inflate recycler view . On long press on an image, I give user option to rename that image file. So I have complete file path (Ex:- /storage/sdcard1/DCIM/100ANDRO/ak.jpg ) for each image in recycler view. Then I want to rename that file.
Now the issue is that as the file path provided is that of External SD Card, and for Android 5 & up, SAF(Storage Access Framework) is required to write a file.
So generally we use this code for renaming a file using SAF:-
public void onActivityResult(int requestCode, int resultCode, Intent resultData){
if (resultCode == RESULT_OK) {
Uri treeUri = resultData.getData();
getContentResolver().takePersistableUriPermission(treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);
DocumentFile newFIle = pickedDir.createFile("text/plain","MyFile")
// or rename as
pickedDir.renameTo("fdtd.jpg");
} else {
Log.d("test","NOt OK RESULT");
}
}
But that is in the case when we know the TreeUri. Here in my case I know fle path and hence want to convert that into TreeUri.
To convert file path to uri use this:-
DocumentFile fileuri = DocumentFile.fromFile(new File(filepath));
Then you can perform delete,rename operations on this fileuri.
If you dont want to use ACTION_OPEN_DOCUMENT_TREE or ACTION_OPEN_DOCUMENT to get an Uri, you can convert FILE to Uri (SAF) using the following method valid from API19(Android4.4-Kitkat) to API28(Android8-Oreo). The returned Uri's are the same that return the dialog and its valid for API 28 security restrictions (SAF permissions), if you want to access external removable storage outside your application...
/**
* Ing.N.Nyerges 2019 V2.0
*
* Storage Access Framework(SAF) Uri's creator from File (java.IO),
* for removable external storages
*
* #param context Application Context
* #param file File path + file name
* #return Uri[]:
* uri[0] = SAF TREE Uri
* uri[1] = SAF DOCUMENT Uri
*/
#RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static Uri[] getSafUris (Context context, File file) {
Uri[] uri = new Uri[2];
String scheme = "content";
String authority = "com.android.externalstorage.documents";
// Separate each element of the File path
// File format: "/storage/XXXX-XXXX/sub-folder1/sub-folder2..../filename"
// (XXXX-XXXX is external removable number
String[] ele = file.getPath().split(File.separator);
// ele[0] = not used (empty)
// ele[1] = not used (storage name)
// ele[2] = storage number
// ele[3 to (n-1)] = folders
// ele[n] = file name
// Construct folders strings using SAF format
StringBuilder folders = new StringBuilder();
if (ele.length > 4) {
folders.append(ele[3]);
for (int i = 4; i < ele.length - 1; ++i) folders.append("%2F").append(ele[i]);
}
String common = ele[2] + "%3A" + folders.toString();
// Construct TREE Uri
Uri.Builder builder = new Uri.Builder();
builder.scheme(scheme);
builder.authority(authority);
builder.encodedPath("/tree/" + common);
uri[0] = builder.build();
// Construct DOCUMENT Uri
builder = new Uri.Builder();
builder.scheme(scheme);
builder.authority(authority);
if (ele.length > 4) common = common + "%2F";
builder.encodedPath("/document/" + common + file.getName());
uri[1] = builder.build();
return uri;
}
you have to set fullpath in renameTo method.
Use my example to work.
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == RESULT_LOAD_IMAGE && resultCode == RESULT_OK && null != data) {
Uri selectedImage = data.getData();
String[] filePathColumn = {MediaStore.Images.Media.DATA};
Cursor cursor = getContentResolver().query(selectedImage,
filePathColumn, null, null, null);
if (cursor != null) {
cursor.moveToFirst();
int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
String picturePath = cursor.getString(columnIndex);
cursor.close();
ImageView imageView = (ImageView) findViewById(R.id.image);
imageView.setOnClickListener(this);
Bitmap bmp = null;
try {
bmp = getBitmapFromUri(selectedImage);
} catch (IOException e) {
e.printStackTrace();
}
imageView.setImageBitmap(bmp);
//get file
File photo = new File(picturePath);
//file name
String fileName = photo.getName();
//resave file with new name
File newFile = new File(photo.getParent() + "/fdtd." + fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length()) );
photo.renameTo(newFile);
}
}
}
Remember that in my example i consider maintain the default extension of original file, because if you try to rename PNG to JPG you can have problem.
I have similar problems but I think I may have a solution. As per my experience using Windows MTP api. Which is very similar to Android SAF.
With SAF android doesnt want you to have direct access to files and instead gives you an ID to the files. If you check DocumentsContract.Document, you notice there is no column for the file path, only a display name.
However with recursion I think we can find the matching Uri. Sorry I cant give an example, but just simply walking the file tree using SAF api until you get all brances of your file path is the idea.
I am making an app that allows that user to record audio. I used Audio intent for this. What I am trying to do is to record audio, set its name, and save it in a folder. In my code, the audio was saved and named properly but when I try to play it, it says that "Sorry, it cannot be played." I don't know where I go a mistake. Help me please, I will really appreciate it. Thanks.
Here is my code:
.....
private void dispatchTakeAudioIntent(int actionCode)
{
Intent takeAudioIntent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
File a = null;
try {
a = setUpAudioFile();
mCurrentAudioPath = a.getAbsolutePath();
takeAudioIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(a));
} catch (IOException e)
{
e.printStackTrace();
a = null;
mCurrentVideoPath = null;
}
startActivityForResult(takeAudioIntent, ACTION_TAKE_AUDIO);
}
private File setUpAudioFile() throws IOException {
File v = createAudioFile();
mCurrentVideoPath = v.getAbsolutePath();
return v;
}
private File createAudioFile() throws IOException
{
// Create an audio file name
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String audioFileName = AUDIO_FILE_PREFIX + timeStamp + "_";
File albumF = getAlbumDir();
File audioF = File.createTempFile(audioFileName, AUDIO_FILE_SUFFIX, albumF);
return audioF;
}
private void galleryAddAudio()
{
Intent mediaScanIntent = new Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE");
File f = new File(mCurrentAudioPath);
Uri contentUri = Uri.fromFile(f);
mediaScanIntent.setData(contentUri);
this.sendBroadcast(mediaScanIntent);
}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case ACTION_TAKE_PHOTO:
{
if (resultCode == RESULT_OK)
{
handleBigCameraPhoto();
dispatchTakePictureIntent(ACTION_TAKE_PHOTO);
}
break;
}
case ACTION_TAKE_AUDIO:
{
if (resultCode == RESULT_OK) {
//audioFileUri = data.getData();
handleAudio(data);
//galleryAddVideo();
}
break;
}
} // switch
}
private void handleAudio(Intent data) {
audioFileUri = data.getData();
if (mCurrentAudioPath != null)
{
//audioFileUri = data.getData();
galleryAddAudio();
mCurrentAudioPath = null;
}
}
........
There are some limitations regarding RECORD_SOUND_ACTION intent that it is not supported to specify a file path to save the audio recording. The application shall save the audio in default location. You cannot use MediaStore.EXTRA_OUTPUT as extra because in document of MediaStore it is written under constant EXTRA_OUTPUT that it only use for image and video.
The name of the Intent-extra used to indicate a content resolver Uri to be used to store the requested image or video.
A solution to this cause is bit tricky. You can let the application save the audio to default but after you can cut, paste and rename your audio to your required location. I found two answers who claims that they found a way to cut paste.
Solution A
Solution B
Accept this answer or +1 if you find it useful.