0

I created a test Android app for being informed about a folder created with Storage Access Framework being updated.

The app creates the folder by means of the SAF picker.

When the user has created the folder the uri is used for creating a DocumentFile object.

Then every 2 seconds the lastModified() method is asked for a long integer that is a timestamp.

That number is logged so I can see when the folder is modified.

When I create a folder on the device storage the app correctly logs events of modification, like creating a subfolder, copying a file into the main folder and so on.

This works only when something happens inside the folder, not subfolders. But I am not interested in that level of modifications. I just need to know when something happens inside the main folder.

But when I perform the same operations on a SAF folder created on the cloud storage (the user can access a cloud root and then create the folder over there) modifications are not logged.

That is, the lastModified() method doesn't yield an updated value. I get the same original value at every time.

What's wrong with my code? Do I have to refresh something or call some other method first?

This is the app code:

package com.example.safevents;

import android.app.Activity;
...
... //other imports

public class MainActivity extends AppCompatActivity {
Activity activity;
private ScheduledExecutorService scheduleTaskExecutor;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Toolbar toolbar = findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    activity=this;
    FloatingActionButton fab = findViewById(R.id.fab);
    fab.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                    .setAction("Action", null).show();
            openPickerForFolderCreation(activity,0);
        }
    });
}

@Override
protected void onDestroy()
{
    super.onDestroy();

}

@Override
protected void onActivityResult(int requestCode, int resultCode,Intent returnedIntent)
{
Log.d("SAF","picker returned "+resultCode+" "+requestCode);
if (returnedIntent!=null) {
Log.d("SAF", "intent=" + returnedIntent.toString());
if (returnedIntent.getData() != null)
    Log.d("SAF", "uri=" + returnedIntent.getData().toString());
if (resultCode == -1)
    if (requestCode == 0) {

        Uri uri = takePermanentReadWritePermissions(activity, returnedIntent.getData(), returnedIntent.getFlags());

        Log.d("SAF", "uri=" + uri.toString());
        Log.d("SAF", "read/write permissions=" + arePermissionsGranted(activity, uri.toString()));



        final DocumentFile df = DocumentFile.fromSingleUri(activity, uri);
        SAFEvents.getInstance().mainFolder = df;


        scheduleTaskExecutor = Executors.newScheduledThreadPool(5);


        scheduleTaskExecutor.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {


                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {

                        Log.d("folder modified", String.valueOf(df.lastModified()));
                    }
                });

            }
        }, 0, 2, TimeUnit.SECONDS);

    }
 }
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();

    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
        return true;
    }

    return super.onOptionsItemSelected(item);
 }
}

here are some methods:

public void openPickerForFolderCreation(Activity activity, int requestCode) {

    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
    intent.setType("vnd.android.document/directory");
    intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    activity.startActivityForResult(intent, requestCode);
}

    public boolean arePermissionsGranted(Activity activity, String uriString) {

    Uri uri = Uri.parse(uriString);


    ContentResolver resolver = activity.getContentResolver();
    List<UriPermission> list = resolver.getPersistedUriPermissions();
    for (int i = 0; i < list.size(); i++) {
    Log.d("SAF","checking permissions of "+list.get(i).getUri().toString());
        if (((Uri.decode(list.get(i).getUri().toString())).equals(Uri.decode(uriString))) && list.get(i).isWritePermission() && list.get(i).isReadPermission()) {
            return true;
        }


    }

    return false;
}

public Uri takePermanentReadWritePermissions(Activity activity, Uri uri, int flags) {
    int takeFlags = flags
            &
            (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            );




    ContentResolver resolver = activity.getContentResolver();
    resolver.takePersistableUriPermission(uri, takeFlags);

    return uri;

}

a singleton is for keeping the reference, if it is necessary

package com.example.safevents;

import androidx.documentfile.provider.DocumentFile;

public class SAFEvents {
private static final SAFEvents ourInstance = new SAFEvents();

DocumentFile mainFolder;

public static SAFEvents getInstance() {
    return ourInstance;
}

private SAFEvents() {
}
}
P5music
  • 3,197
  • 2
  • 32
  • 81
  • `public void openPickerForFolderCreation(Activity activity, int requestCode)` Congratulations! You use `ACTION_CREATE_DOCUMENT` to create a directory. Never seen such. A great hack. Of course it is useless to add those flags as you cannot grant anything there. Further I advise you to do it in the normal way and use `ACTION_OPEN_DOCUMENT_TREE` to let the user select a directory. With that action the user can add a new folder in the normal way. – blackapps Nov 13 '19 at 11:12
  • @blackapps ACTION_OPEN_DOCUMENT_TREE does not fully work yet in GDrive (the only provider I know that has SAF features). There is a bugfix request opened about it: cloud roots do not appear. However the mimetype is correct to create folders, because folders are nothing but toplevel documents of a tree. Furthermore my code correctly works on device storage. – P5music Nov 13 '19 at 11:24
  • Not always. Try it on Emulator Pixel 2 API 29 (Android Q) in the Downloads directly and see it gives an extra subfolder named `(invalid)`. And remove the flags. They are useless. – blackapps Nov 13 '19 at 11:27
  • `final DocumentFile df = DocumentFile.fromSingleUri(activity, uri);` You keep the DocumentFile instance. Put that statement in run(). Not final. Just try. – blackapps Nov 13 '19 at 12:35
  • You should also check if in the drive app the lastmodified date changes. Please tell. – blackapps Nov 13 '19 at 12:40
  • @blackapps I already put the DocumentFile creation inside the run method but nothing changes. As to the Drive app I can look at a list of recent operations on the folder, I have to say that the list is not immediately updated, but I do not know if it is some UI issue instead of a real delay (after some attempts the last operation appears in the list and it is correctly timestamped). However it is clear that it's the Drive app that should have to update the timestamp value from the lastModified() call. – P5music Nov 13 '19 at 13:37
  • What you do is pretty hacky. Indeed a folder is created and one can put files in it i tested. But the content scheme obtained in onActivityResult is one for a file thats why you use `DocumentFile.fromSingleUri(activity, uri);` Normally if a dir is choosen with `ACTION_OPEN_DOCUMENT_TREE` one would use `DocumentFile.fromTreeUri(activity, uri);` You should try to list the files in it with `df.listFiles()` That will not go. Great exceptions. The obtained uri is useless. – blackapps Nov 13 '19 at 20:17
  • I tried your hack with ACTON_OPEN_DOCUMENT too hoping a folder from a gdrive would be selectable. But not. – blackapps Nov 13 '19 at 20:21
  • @blackapps ACTION_OPEN_DOCUMENT seems to be working on cloud too, try to upload a txt document on a certain GDrive cloud folder, then run the application and select that file, then try renaming it. I just tried this. – P5music Nov 14 '19 at 10:01
  • You are not getting me. I tried your hack on ACTION_OPEN_DOCUMENT trying to select a directory. This was not possible. Of course i know that a file can be selected with it. – blackapps Nov 14 '19 at 10:10
  • @blackapps One could create a folder with a special mimetype for folders, that is, a customized one, so the picker navigates through normal folders but could in principle let you select that folder with special mimetype you created. This thread could be a good starting point https://stackoverflow.com/questions/4749593/mime-type-for-directories-in-android – P5music Nov 14 '19 at 10:24
  • You have a great fantasy that one could create its own folder type and then be able to select one with ACTION_OPEN_DOCUMENT where the type was set to that folder type. The example there can browse but nor files nor folders can be selected. – blackapps Nov 14 '19 at 10:51
  • @blackapps I do not know if it would work, it is just a reasoning, but I meant also that you have to put the mimetype in the intent when calling the picker, otherwise that customized type is not selectable. This is mandatory: intent.setType(mimetype); – P5music Nov 15 '19 at 10:31
  • @blackapps I refer to the sub-type, however I do not know if it makes sense, it was just an idea https://stackoverflow.com/a/7563037/930835 – P5music Nov 15 '19 at 11:45

0 Answers0