Commit 42cf9348 authored by Geoffrey Métais's avatar Geoffrey Métais
Browse files

Implement write access on external devices

parent 0b7880a4
......@@ -32,6 +32,7 @@ import org.videolan.vlc.util.Util;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
......@@ -216,9 +217,7 @@ public class MediaParsingService extends Service implements DevicesDiscoveryCb {
shouldInit |= initCode == Medialibrary.ML_INIT_DB_RESET;
if (initCode != Medialibrary.ML_INIT_FAILED) {
final List<String> devices = new ArrayList<>();
devices.addAll(AndroidDevices.getExternalStorageDirectories());
if (!devices.contains(AndroidDevices.EXTERNAL_PUBLIC_DIRECTORY))
devices.add(AndroidDevices.EXTERNAL_PUBLIC_DIRECTORY);
Collections.addAll(devices, AndroidDevices.getMediaDirectories());
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
for (final String device : devices) {
final boolean isMainStorage = TextUtils.equals(device, AndroidDevices.EXTERNAL_PUBLIC_DIRECTORY);
......@@ -273,7 +272,6 @@ public class MediaParsingService extends Service implements DevicesDiscoveryCb {
final Context ctx = VLCApplication.getAppContext();
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ctx);
final List<String> devices = AndroidDevices.getExternalStorageDirectories();
devices.remove(AndroidDevices.EXTERNAL_PUBLIC_DIRECTORY);
final String[] knownDevices = mMedialibrary.getDevices();
final List<String> missingDevices = Util.arrayToArrayList(knownDevices);
missingDevices.remove("file://"+AndroidDevices.EXTERNAL_PUBLIC_DIRECTORY);
......
......@@ -186,10 +186,4 @@ public class ContentActivity extends AudioPlayerContainerActivity implements Sea
((Filterable) current).restoreList();
}
}
public Runnable deleteAction;
public void onWriteAccessGranted() {
if (deleteAction != null) mActivityHandler.postDelayed(deleteAction, 500);
deleteAction = null;
}
}
......@@ -216,7 +216,7 @@ public class AudioBrowserFragment extends BaseAudioBrowser implements SwipeRefre
if (pos != MODE_SONG && pos != MODE_PLAYLIST)
menu.findItem(R.id.audio_list_browser_delete).setVisible(false);
else {
MenuItem item = menu.findItem(R.id.audio_list_browser_delete);
final MenuItem item = menu.findItem(R.id.audio_list_browser_delete);
AudioBrowserAdapter adapter = pos == MODE_SONG ? mSongsAdapter : mPlaylistAdapter;
MediaLibraryItem mediaItem = adapter.getItem(position);
if (pos == MODE_PLAYLIST )
......@@ -277,12 +277,13 @@ public class AudioBrowserFragment extends BaseAudioBrowser implements SwipeRefre
if (!checkWritePermission((MediaWrapper) mediaItem, new Runnable() {
@Override
public void run() {
UiTools.snackerWithCancel(getView(), message, action, cancel);
final View v = getView();
if (v != null) UiTools.snackerWithCancel(getView(), message, action, cancel);
}
})) return false;
} else
return false;
UiTools.snackerWithCancel(getView(), message, action, cancel);
} else return false;
final View v = getView();
if (v != null) UiTools.snackerWithCancel(getView(), message, action, cancel);
return true;
}
......
......@@ -73,7 +73,6 @@ import org.videolan.vlc.interfaces.IRefreshable;
import org.videolan.vlc.media.MediaDatabase;
import org.videolan.vlc.media.MediaUtils;
import org.videolan.vlc.util.AndroidDevices;
import org.videolan.vlc.util.FileUtils;
import org.videolan.vlc.util.Strings;
import org.videolan.vlc.util.Util;
import org.videolan.vlc.util.VLCInstance;
......@@ -494,7 +493,7 @@ public abstract class BaseBrowserFragment extends SortableFragment<BaseBrowserAd
final MediaWrapper mw = (MediaWrapper) mAdapter.getItem(position);
if (mw == null) return;
final int type = mw.getType();
boolean canWrite = this instanceof FileBrowserFragment && FileUtils.canWrite(mw.getUri().getPath());
boolean canWrite = this instanceof FileBrowserFragment;
if (type == MediaWrapper.TYPE_DIR) {
final boolean isEmpty = Util.isListEmpty(mFoldersContentLists.get(mw));
// if (canWrite) {
......@@ -537,7 +536,7 @@ public abstract class BaseBrowserFragment extends SortableFragment<BaseBrowserAd
int id = item.getItemId();
if (! (mAdapter.getItem(position) instanceof MediaWrapper))
return super.onContextItemSelected(item);
Uri uri = ((MediaWrapper) mAdapter.getItem(position)).getUri();
final Uri uri = ((MediaWrapper) mAdapter.getItem(position)).getUri();
MediaWrapper mwFromMl = "file".equals(uri.getScheme()) ? mMediaLibrary.getMedia(uri) : null;
final MediaWrapper mw = mwFromMl != null ? mwFromMl : (MediaWrapper) mAdapter.getItem(position);
switch (id){
......@@ -551,20 +550,12 @@ public abstract class BaseBrowserFragment extends SortableFragment<BaseBrowserAd
return true;
}
case R.id.directory_view_delete:
mAdapter.removeItem(mw);
final Runnable cancel = new Runnable() {
if (checkWritePermission(mw, new Runnable() {
@Override
public void run() {
mAdapter.addItem(mw, true);
removeMedia(mw);
}
};
final View v = getView();
if (v != null) UiTools.snackerWithCancel(v, getString(R.string.file_deleted), new Runnable() {
@Override
public void run() {
deleteMedia(mw, false, cancel);
}
}, cancel);
})) removeMedia(mw);
return true;
case R.id.directory_view_info:
showMediaInfo(mw);
......@@ -609,6 +600,23 @@ public abstract class BaseBrowserFragment extends SortableFragment<BaseBrowserAd
return false;
}
private void removeMedia(final MediaWrapper mw) {
mAdapter.removeItem(mw);
final Runnable cancel = new Runnable() {
@Override
public void run() {
mAdapter.addItem(mw, true);
}
};
final View v = getView();
if (v != null) UiTools.snackerWithCancel(v, getString(R.string.file_deleted), new Runnable() {
@Override
public void run() {
deleteMedia(mw, false, cancel);
}
}, cancel);
}
private void showMediaInfo(MediaWrapper mw) {
Intent i = new Intent(getActivity(), InfoActivity.class);
i.putExtra(InfoActivity.TAG_ITEM, mw);
......
......@@ -27,6 +27,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
......@@ -40,15 +41,16 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import org.videolan.libvlc.util.AndroidUtil;
import org.videolan.medialibrary.Medialibrary;
import org.videolan.medialibrary.media.MediaLibraryItem;
import org.videolan.medialibrary.media.MediaWrapper;
import org.videolan.vlc.R;
import org.videolan.vlc.VLCApplication;
import org.videolan.vlc.gui.ContentActivity;
import org.videolan.vlc.gui.InfoActivity;
import org.videolan.vlc.gui.PlaybackServiceFragment;
import org.videolan.vlc.gui.helpers.UiTools;
import org.videolan.vlc.gui.helpers.hf.WriteExternalDelegate;
import org.videolan.vlc.gui.view.ContextMenuRecyclerView;
import org.videolan.vlc.gui.view.SwipeRefreshLayout;
import org.videolan.vlc.util.AndroidDevices;
......@@ -169,7 +171,7 @@ public abstract class MediaBrowserFragment extends PlaybackServiceFragment imple
@Override
public boolean onContextItemSelected(MenuItem menu) {
if(!getUserVisibleHint()) return false;
if (!getUserVisibleHint()) return false;
final ContextMenuRecyclerView.RecyclerContextMenuInfo info = (ContextMenuRecyclerView.RecyclerContextMenuInfo) menu.getMenuInfo();
return info != null && handleContextItemSelected(menu, info.position);
}
......@@ -183,8 +185,10 @@ public abstract class MediaBrowserFragment extends PlaybackServiceFragment imple
for (MediaWrapper media : mw.getTracks()) {
final String path = media.getUri().getPath();
final String parentPath = FileUtils.getParent(path);
if (FileUtils.deleteFile(path) && media.getId() > 0L && !foldersToReload.contains(parentPath)) {
foldersToReload.add(parentPath);
if (FileUtils.deleteFile(media.getUri())) {
if (media.getId() > 0L && !foldersToReload.contains(parentPath)) {
foldersToReload.add(parentPath);
}
mediaPaths.add(media.getLocation());
} else onDeleteFailed(media);
}
......@@ -208,14 +212,21 @@ public abstract class MediaBrowserFragment extends PlaybackServiceFragment imple
}
protected boolean checkWritePermission(MediaWrapper media, Runnable callback) {
if (media.getUri().getPath().startsWith(AndroidDevices.EXTERNAL_PUBLIC_DIRECTORY) && !Permissions.canWriteStorage()) {
final ContentActivity activity = (ContentActivity) getActivity();
activity.deleteAction = callback;
Permissions.askWriteStoragePermission(getActivity(), false);
final Uri uri = media.getUri();
if (!"file".equals(uri.getScheme())) return false;
if (uri.getPath().startsWith(AndroidDevices.EXTERNAL_PUBLIC_DIRECTORY)) {
//Check write permission starting Oreo
if (AndroidUtil.isOOrLater && !Permissions.canWriteStorage()) {
Permissions.askWriteStoragePermission(getActivity(), false, callback);
return false;
}
} else if (AndroidUtil.isLolliPopOrLater && WriteExternalDelegate.Companion.needsWritePermission(uri)) {
WriteExternalDelegate.Companion.askForExtWrite(getActivity(), uri, callback);
return false;
}
return true;
}
private void onDeleteFailed(MediaWrapper media) {
final View v = getView();
if (v != null && isAdded()) UiTools.snacker(v, getString(R.string.msg_delete_failed, media.getTitle()));
......
/*
* *************************************************************************
* BaseHeadlessFragment.java
* BaseHeadlessFragment.kt
* **************************************************************************
* Copyright © 2017 VLC authors and VideoLAN
* Copyright © 2017-2018 VLC authors and VideoLAN
* Author: Geoffrey Métais
*
* This program is free software; you can redistribute it and/or modify
......@@ -21,38 +21,48 @@
* ***************************************************************************
*/
package org.videolan.vlc.gui.helpers.hf;
package org.videolan.vlc.gui.helpers.hf
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentActivity
public class BaseHeadlessFragment extends Fragment {
protected FragmentActivity mActivity;
open class BaseHeadlessFragment : Fragment() {
protected var mActivity: FragmentActivity? = null
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof FragmentActivity)
mActivity = (FragmentActivity) context;
override fun onAttach(context: Context?) {
super.onAttach(context)
if (context is FragmentActivity) mActivity = context
}
@Override
public void onDetach() {
super.onDetach();
mActivity = null;
override fun onDetach() {
super.onDetach()
mActivity = null
}
protected void exit() {
if (mActivity != null && !mActivity.isFinishing())
mActivity.getSupportFragmentManager().beginTransaction().remove(this).commitAllowingStateLoss();
protected fun exit() {
if (mActivity?.isFinishing == false) mActivity!!.supportFragmentManager.beginTransaction().remove(this).commitAllowingStateLoss()
}
companion object {
internal var callback: Runnable? = null
internal fun executeCallback() {
callback?.let {
try {
Handler().postDelayed(it, 500);
} catch (ignored: Exception) {
} finally {
callback = null
}
}
}
}
}
/*
* *************************************************************************
* StoragePermissionsDelegate.java
* StoragePermissionsDelegate.kt
* **************************************************************************
* Copyright © 2017 VLC authors and VideoLAN
* Copyright © 2017-2018 VLC authors and VideoLAN
* Author: Geoffrey Métais
*
* This program is free software; you can redistribute it and/or modify
......@@ -21,109 +21,104 @@
* ***************************************************************************
*/
package org.videolan.vlc.gui.helpers.hf;
package org.videolan.vlc.gui.helpers.hf
import android.Manifest;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import org.videolan.libvlc.util.AndroidUtil;
import org.videolan.vlc.MediaParsingService;
import org.videolan.vlc.VLCApplication;
import org.videolan.vlc.gui.ContentActivity;
import org.videolan.vlc.util.Constants;
import org.videolan.vlc.util.Permissions;
import static org.videolan.vlc.util.Permissions.canReadStorage;
import android.Manifest
import android.annotation.TargetApi
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentActivity
import org.videolan.libvlc.util.AndroidUtil
import org.videolan.vlc.MediaParsingService
import org.videolan.vlc.VLCApplication
import org.videolan.vlc.util.Constants
import org.videolan.vlc.util.Permissions
import org.videolan.vlc.util.Permissions.canReadStorage
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class StoragePermissionsDelegate extends BaseHeadlessFragment {
class StoragePermissionsDelegate : BaseHeadlessFragment() {
public interface CustomActionController {
void onStorageAccessGranted();
}
private var mFirstRun: Boolean = false
private var mUpgrade: Boolean = false
private var mWrite: Boolean = false
public final static String TAG = "VLC/StorageHF";
interface CustomActionController {
fun onStorageAccessGranted()
}
private boolean mFirstRun, mUpgrade, mWrite;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Intent intent = mActivity == null ? null : mActivity.getIntent();
if (intent != null && intent.getBooleanExtra(Constants.EXTRA_UPGRADE, false)) {
mUpgrade = true;
mFirstRun = intent.getBooleanExtra(Constants.EXTRA_FIRST_RUN, false);
intent.removeExtra(Constants.EXTRA_UPGRADE);
intent.removeExtra(Constants.EXTRA_FIRST_RUN);
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = if (mActivity == null) null else mActivity!!.intent
if (intent !== null && intent.getBooleanExtra(Constants.EXTRA_UPGRADE, false)) {
mUpgrade = true
mFirstRun = intent.getBooleanExtra(Constants.EXTRA_FIRST_RUN, false)
intent.removeExtra(Constants.EXTRA_UPGRADE)
intent.removeExtra(Constants.EXTRA_FIRST_RUN)
}
mWrite = getArguments().getBoolean("write");
if (AndroidUtil.isMarshMallowOrLater && !canReadStorage(getActivity())) {
mWrite = arguments.getBoolean("write")
if (AndroidUtil.isMarshMallowOrLater && !canReadStorage(activity)) {
if (shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE))
Permissions.showStoragePermissionDialog(mActivity, false, false);
Permissions.showStoragePermissionDialog(mActivity, false)
else
requestStorageAccess(false);
requestStorageAccess(false)
} else if (mWrite) {
if (shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE))
Permissions.showStoragePermissionDialog(mActivity, false, true);
Permissions.showStoragePermissionDialog(mActivity, false)
else
requestStorageAccess(true);
requestStorageAccess(true)
}
}
private void requestStorageAccess(boolean write) {
requestPermissions(new String[]{write ? Manifest.permission.WRITE_EXTERNAL_STORAGE : Manifest.permission.READ_EXTERNAL_STORAGE},
write ? Permissions.PERMISSION_WRITE_STORAGE_TAG : Permissions.PERMISSION_STORAGE_TAG);
private fun requestStorageAccess(write: Boolean) {
requestPermissions(arrayOf(if (write) Manifest.permission.WRITE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE),
if (write) Permissions.PERMISSION_WRITE_STORAGE_TAG else Permissions.PERMISSION_STORAGE_TAG)
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
switch (requestCode) {
case Permissions.PERMISSION_STORAGE_TAG:
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
when (requestCode) {
Permissions.PERMISSION_STORAGE_TAG -> {
// If request is cancelled, the result arrays are empty.
final Context ctx = VLCApplication.getAppContext();
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (mActivity instanceof CustomActionController) {
((CustomActionController) mActivity).onStorageAccessGranted();
val ctx = VLCApplication.getAppContext()
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (mActivity is CustomActionController) {
(mActivity as CustomActionController).onStorageAccessGranted()
} else {
final Intent serviceIntent = new Intent(Constants.ACTION_INIT, null, ctx, MediaParsingService.class);
serviceIntent.putExtra(Constants.EXTRA_FIRST_RUN, mFirstRun);
serviceIntent.putExtra(Constants.EXTRA_UPGRADE, mUpgrade);
ctx.startService(serviceIntent);
val serviceIntent = Intent(Constants.ACTION_INIT, null, ctx, MediaParsingService::class.java)
serviceIntent.putExtra(Constants.EXTRA_FIRST_RUN, mFirstRun)
serviceIntent.putExtra(Constants.EXTRA_UPGRADE, mUpgrade)
ctx.startService(serviceIntent)
}
exit();
exit()
} else if (mActivity != null) {
Permissions.showStoragePermissionDialog(mActivity, false, mWrite);
Permissions.showStoragePermissionDialog(mActivity, false)
if (!shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE))
exit();
exit()
}
break;
case Permissions.PERMISSION_WRITE_STORAGE_TAG:
if (mActivity instanceof ContentActivity) ((ContentActivity) mActivity).onWriteAccessGranted();
break;
}
Permissions.PERMISSION_WRITE_STORAGE_TAG -> executeCallback()
}
}
public static void askStoragePermission(@NonNull FragmentActivity activity, boolean write) {
if (activity.isFinishing()) return;
final FragmentManager fm = activity.getSupportFragmentManager();
Fragment fragment = fm.findFragmentByTag(TAG);
if (fragment == null) {
final Bundle args = new Bundle();
args.putBoolean("write", write);
fragment = new StoragePermissionsDelegate();
fragment.setArguments(args);
fm.beginTransaction().add(fragment, TAG).commitAllowingStateLoss();
} else
((StoragePermissionsDelegate)fragment).requestStorageAccess(write);
companion object {
const val TAG = "VLC/StorageHF"
fun askStoragePermission(activity: FragmentActivity, write: Boolean, cb: Runnable?) {
if (activity.isFinishing) return
val fm = activity.supportFragmentManager
var fragment: Fragment? = fm.findFragmentByTag(TAG)
callback = cb
if (fragment == null) {
val args = Bundle()
args.putBoolean("write", write)
fragment = StoragePermissionsDelegate()
fragment.arguments = args
fm.beginTransaction().add(fragment, TAG).commitAllowingStateLoss()
} else
(fragment as StoragePermissionsDelegate).requestStorageAccess(write)
}
}
}
package org.videolan.vlc.gui.helpers.hf
import android.annotation.TargetApi
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.DocumentsContract
import android.support.v4.app.FragmentActivity
import android.support.v4.provider.DocumentFile
import android.support.v7.preference.PreferenceManager
import android.text.TextUtils
import org.videolan.libvlc.util.AndroidUtil
import org.videolan.vlc.VLCApplication
import org.videolan.vlc.util.AndroidDevices
import org.videolan.vlc.util.FileUtils
class WriteExternalDelegate : BaseHeadlessFragment() {
@TargetApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
arguments?.getString(KEY_STORAGE_PATH)?.let { intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.parse(it)) }
startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCES)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (data !== null && requestCode == REQUEST_CODE_STORAGE_ACCES) {
if (resultCode == Activity.RESULT_OK) {
val context = context
val treeUri = data.data
PreferenceManager.getDefaultSharedPreferences(VLCApplication.getAppContext()).edit()
.putString("tree_uri_"+ storage, treeUri.toString()).apply()
val treeFile = DocumentFile.fromTreeUri(context, treeUri)
val contentResolver = context.contentResolver
// revoke access if a permission already exists
val persistedUriPermissions = contentResolver.persistedUriPermissions
for (uriPermission in persistedUriPermissions) {
val file = DocumentFile.fromTreeUri(context, uriPermission.uri)
if (treeFile.name == file.name) {
contentResolver.releasePersistableUriPermission(uriPermission.uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
return
}
}
// else set permission
contentResolver.takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
permissions = contentResolver.persistedUriPermissions
executeCallback()
return
}
}
callback = null
}
companion object {
internal const val TAG = "VLC/WriteExternal"
internal const val KEY_STORAGE_PATH = "VLC/storage_path"
private const val REQUEST_CODE_STORAGE_ACCES = 42
private var permissions = VLCApplication.getAppContext().contentResolver.persistedUriPermissions
private lateinit var storage: String