mirror of https://github.com/kurisufriend/Clover
parent
97a15fdb85
commit
5e09cd398b
@ -0,0 +1,38 @@ |
|||||||
|
package org.floens.chan.core.storage; |
||||||
|
|
||||||
|
import android.content.Context; |
||||||
|
import android.content.Intent; |
||||||
|
import android.net.Uri; |
||||||
|
|
||||||
|
public class BaseStorageImpl implements StorageImpl { |
||||||
|
protected Context applicationContext; |
||||||
|
|
||||||
|
public BaseStorageImpl(Context applicationContext) { |
||||||
|
this.applicationContext = applicationContext; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean supportsExternalStorage() { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Intent getOpenTreeIntent() { |
||||||
|
throw new UnsupportedOperationException(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleOpenTreeIntent(Uri uri) { |
||||||
|
throw new UnsupportedOperationException(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public StorageFile obtainStorageFileForName(String name) { |
||||||
|
throw new UnsupportedOperationException(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String currentStorageName() { |
||||||
|
throw new UnsupportedOperationException(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,106 @@ |
|||||||
|
package org.floens.chan.core.storage; |
||||||
|
|
||||||
|
import android.content.ContentResolver; |
||||||
|
import android.content.Context; |
||||||
|
import android.content.Intent; |
||||||
|
import android.database.Cursor; |
||||||
|
import android.net.Uri; |
||||||
|
import android.os.Build; |
||||||
|
import android.provider.DocumentsContract; |
||||||
|
import android.support.annotation.RequiresApi; |
||||||
|
|
||||||
|
import org.floens.chan.core.settings.ChanSettings; |
||||||
|
import org.floens.chan.utils.IOUtils; |
||||||
|
import org.floens.chan.utils.Logger; |
||||||
|
|
||||||
|
import java.io.FileNotFoundException; |
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) |
||||||
|
public class LollipopStorageImpl extends BaseStorageImpl { |
||||||
|
private static final String TAG = "LollipopStorageImpl"; |
||||||
|
|
||||||
|
public LollipopStorageImpl(Context applicationContext) { |
||||||
|
super(applicationContext); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean supportsExternalStorage() { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Intent getOpenTreeIntent() { |
||||||
|
return new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void handleOpenTreeIntent(Uri uri) { |
||||||
|
String documentId = DocumentsContract.getTreeDocumentId(uri); |
||||||
|
Uri treeDocumentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId); |
||||||
|
|
||||||
|
ChanSettings.saveLocationTreeUri.set(treeDocumentUri.toString()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public StorageFile obtainStorageFileForName(String name) { |
||||||
|
String uriString = ChanSettings.saveLocationTreeUri.get(); |
||||||
|
if (uriString.isEmpty()) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
Uri treeUri = Uri.parse(uriString); |
||||||
|
|
||||||
|
ContentResolver contentResolver = applicationContext.getContentResolver(); |
||||||
|
|
||||||
|
String documentId = DocumentsContract.getTreeDocumentId(treeUri); |
||||||
|
Uri treeDocumentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId); |
||||||
|
|
||||||
|
Uri docUri; |
||||||
|
try { |
||||||
|
docUri = DocumentsContract.createDocument(contentResolver, treeDocumentUri, |
||||||
|
"text", name); |
||||||
|
} catch (FileNotFoundException e) { |
||||||
|
Logger.e(TAG, "obtainStorageFileForName createDocument", e); |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
return StorageFile.fromUri(contentResolver, docUri); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String currentStorageName() { |
||||||
|
String uriString = ChanSettings.saveLocationTreeUri.get(); |
||||||
|
if (uriString.isEmpty()) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
Uri treeUri = Uri.parse(uriString); |
||||||
|
return queryTreeName(treeUri); |
||||||
|
} |
||||||
|
|
||||||
|
private String queryTreeName(Uri uri) { |
||||||
|
ContentResolver contentResolver = applicationContext.getContentResolver(); |
||||||
|
|
||||||
|
Cursor c = null; |
||||||
|
String name = null; |
||||||
|
try { |
||||||
|
c = contentResolver.query(uri, new String[]{ |
||||||
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME, |
||||||
|
DocumentsContract.Document.COLUMN_MIME_TYPE |
||||||
|
}, null, null, null); |
||||||
|
|
||||||
|
if (c != null && c.moveToNext()) { |
||||||
|
name = c.getString(0); |
||||||
|
// mime = c.getString(1);
|
||||||
|
} |
||||||
|
|
||||||
|
return name; |
||||||
|
} catch (Exception e) { |
||||||
|
Logger.e(TAG, "queryTreeName", e); |
||||||
|
} finally { |
||||||
|
IOUtils.closeQuietly(c); |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
@ -1,66 +1,58 @@ |
|||||||
package org.floens.chan.core.storage; |
package org.floens.chan.core.storage; |
||||||
|
|
||||||
import android.annotation.TargetApi; |
|
||||||
import android.content.Context; |
import android.content.Context; |
||||||
import android.content.Intent; |
import android.content.Intent; |
||||||
|
import android.net.Uri; |
||||||
import android.os.Build; |
import android.os.Build; |
||||||
import android.os.storage.StorageManager; |
|
||||||
import android.os.storage.StorageVolume; |
|
||||||
|
|
||||||
import java.util.Objects; |
|
||||||
|
|
||||||
|
import javax.inject.Inject; |
||||||
|
import javax.inject.Singleton; |
||||||
|
|
||||||
|
/** |
||||||
|
* Abstraction of the storage APIs available in Android. |
||||||
|
* <p> |
||||||
|
* This is used primarily for saving images, especially on removable storage. |
||||||
|
* <p> |
||||||
|
* First, a good read: |
||||||
|
* https://commonsware.com/blog/2017/11/13/storage-situation-internal-storage.html
|
||||||
|
* https://commonsware.com/blog/2017/11/14/storage-situation-external-storage.html
|
||||||
|
* https://commonsware.com/blog/2017/11/15/storage-situation-removable-storage.html
|
||||||
|
* <p> |
||||||
|
* The Android Storage Access Framework can be used from Android 5.0 and higher. Since Android 5.0 |
||||||
|
* it has support for granting permissions for a directory, which we want to save our files to. |
||||||
|
* <p> |
||||||
|
* Otherwise a fallback is provided for only saving on the primary volume with the older APIs. |
||||||
|
*/ |
||||||
|
@Singleton |
||||||
public class Storage { |
public class Storage { |
||||||
private static final Storage instance; |
private StorageImpl impl; |
||||||
|
|
||||||
static { |
@Inject |
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { |
public Storage(Context applicationContext) { |
||||||
instance = new Storage(new BaseStorageImpl()); |
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { |
||||||
|
impl = new BaseStorageImpl(applicationContext); |
||||||
} else { |
} else { |
||||||
instance = new Storage(new NougatStorageImpl()); |
impl = new LollipopStorageImpl(applicationContext); |
||||||
} |
} |
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
public static Storage getInstance() { |
|
||||||
return instance; |
|
||||||
} |
} |
||||||
|
|
||||||
private StorageImpl impl; |
public boolean supportsExternalStorage() { |
||||||
|
return impl.supportsExternalStorage(); |
||||||
public Storage(StorageImpl impl) { |
|
||||||
this.impl = impl; |
|
||||||
} |
} |
||||||
|
|
||||||
public Intent requestExternalPermission(Context applicationContext) { |
public Intent getOpenTreeIntent() { |
||||||
return impl.requestExternalPermission(applicationContext); |
return impl.getOpenTreeIntent(); |
||||||
} |
} |
||||||
|
|
||||||
public interface StorageImpl { |
public void handleOpenTreeIntent(Uri uri) { |
||||||
Intent requestExternalPermission(Context applicationContext); |
impl.handleOpenTreeIntent(uri); |
||||||
} |
} |
||||||
|
|
||||||
public static class BaseStorageImpl implements StorageImpl { |
public StorageFile obtainStorageFileForName(String name) { |
||||||
@Override |
return impl.obtainStorageFileForName(name); |
||||||
public Intent requestExternalPermission(Context applicationContext) { |
|
||||||
throw new UnsupportedOperationException(); |
|
||||||
} |
|
||||||
} |
} |
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.N) |
public String currentStorageName() { |
||||||
public static class NougatStorageImpl extends BaseStorageImpl { |
return impl.currentStorageName(); |
||||||
@Override |
|
||||||
public Intent requestExternalPermission(Context applicationContext) { |
|
||||||
StorageManager sm = (StorageManager) |
|
||||||
applicationContext.getSystemService(Context.STORAGE_SERVICE); |
|
||||||
Objects.requireNonNull(sm); |
|
||||||
for (StorageVolume storageVolume : sm.getStorageVolumes()) { |
|
||||||
if (!storageVolume.isPrimary()) { |
|
||||||
Intent accessIntent = storageVolume.createAccessIntent(null); |
|
||||||
return accessIntent; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return null; |
|
||||||
} |
|
||||||
} |
} |
||||||
} |
} |
||||||
|
@ -0,0 +1,93 @@ |
|||||||
|
package org.floens.chan.core.storage; |
||||||
|
|
||||||
|
import android.content.ContentResolver; |
||||||
|
import android.net.Uri; |
||||||
|
|
||||||
|
import org.floens.chan.utils.IOUtils; |
||||||
|
|
||||||
|
import java.io.BufferedInputStream; |
||||||
|
import java.io.BufferedOutputStream; |
||||||
|
import java.io.File; |
||||||
|
import java.io.FileInputStream; |
||||||
|
import java.io.FileOutputStream; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStream; |
||||||
|
import java.io.OutputStream; |
||||||
|
|
||||||
|
public class StorageFile { |
||||||
|
private final ContentResolver contentResolver; |
||||||
|
private final Uri uriOpenableByContentResolvers; |
||||||
|
private final File file; |
||||||
|
|
||||||
|
public static StorageFile fromFile(File file) { |
||||||
|
return new StorageFile(file); |
||||||
|
} |
||||||
|
|
||||||
|
public static StorageFile fromUri(ContentResolver contentResolver, Uri uriOpenableByContentResolvers) { |
||||||
|
return new StorageFile(contentResolver, uriOpenableByContentResolvers); |
||||||
|
} |
||||||
|
|
||||||
|
private StorageFile(ContentResolver contentResolver, Uri uriOpenableByContentResolvers) { |
||||||
|
this.contentResolver = contentResolver; |
||||||
|
this.uriOpenableByContentResolvers = uriOpenableByContentResolvers; |
||||||
|
this.file = null; |
||||||
|
} |
||||||
|
|
||||||
|
private StorageFile(File file) { |
||||||
|
this.contentResolver = null; |
||||||
|
this.uriOpenableByContentResolvers = null; |
||||||
|
this.file = file; |
||||||
|
} |
||||||
|
|
||||||
|
public InputStream inputStream() throws IOException { |
||||||
|
if (file != null) { |
||||||
|
return new FileInputStream(file); |
||||||
|
} else { |
||||||
|
return contentResolver.openInputStream(uriOpenableByContentResolvers); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public OutputStream outputStream() throws IOException { |
||||||
|
if (file != null) { |
||||||
|
File parent = file.getParentFile(); |
||||||
|
if (!parent.mkdirs() && !parent.isDirectory()) { |
||||||
|
throw new IOException("Could not create parent directory"); |
||||||
|
} |
||||||
|
|
||||||
|
if (file.isDirectory()) { |
||||||
|
throw new IOException("Destination not a file"); |
||||||
|
} |
||||||
|
|
||||||
|
return new FileOutputStream(file); |
||||||
|
} else { |
||||||
|
return contentResolver.openOutputStream(uriOpenableByContentResolvers); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public String name() { |
||||||
|
return "dummy name"; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean exists() { |
||||||
|
// TODO
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean delete() { |
||||||
|
// TODO
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
public void copyFrom(File source) throws IOException { |
||||||
|
InputStream is = null; |
||||||
|
OutputStream os = null; |
||||||
|
try { |
||||||
|
is = new BufferedInputStream(new FileInputStream(source)); |
||||||
|
os = new BufferedOutputStream(outputStream()); |
||||||
|
IOUtils.copy(is, os); |
||||||
|
} finally { |
||||||
|
IOUtils.closeQuietly(is); |
||||||
|
IOUtils.closeQuietly(os); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package org.floens.chan.core.storage; |
||||||
|
|
||||||
|
import android.content.Intent; |
||||||
|
import android.net.Uri; |
||||||
|
|
||||||
|
public interface StorageImpl { |
||||||
|
boolean supportsExternalStorage(); |
||||||
|
|
||||||
|
Intent getOpenTreeIntent(); |
||||||
|
|
||||||
|
void handleOpenTreeIntent(Uri uri); |
||||||
|
|
||||||
|
StorageFile obtainStorageFileForName(String name); |
||||||
|
|
||||||
|
String currentStorageName(); |
||||||
|
} |
@ -0,0 +1,79 @@ |
|||||||
|
package org.floens.chan.ui.controller; |
||||||
|
|
||||||
|
import android.app.Activity; |
||||||
|
import android.content.Context; |
||||||
|
import android.content.Intent; |
||||||
|
import android.view.View; |
||||||
|
import android.widget.Button; |
||||||
|
import android.widget.TextView; |
||||||
|
|
||||||
|
import org.floens.chan.R; |
||||||
|
import org.floens.chan.controller.Controller; |
||||||
|
import org.floens.chan.core.storage.Storage; |
||||||
|
|
||||||
|
import javax.inject.Inject; |
||||||
|
|
||||||
|
import static org.floens.chan.Chan.inject; |
||||||
|
|
||||||
|
public class StorageSetupController extends Controller implements View.OnClickListener { |
||||||
|
private static final int OPEN_TREE_INTENT_RESULT_ID = 101; |
||||||
|
|
||||||
|
private static final String TAG = "StorageSetupController"; |
||||||
|
|
||||||
|
@Inject |
||||||
|
private Storage storage; |
||||||
|
|
||||||
|
private TextView text; |
||||||
|
private Button button; |
||||||
|
|
||||||
|
public StorageSetupController(Context context) { |
||||||
|
super(context); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onCreate() { |
||||||
|
super.onCreate(); |
||||||
|
|
||||||
|
inject(this); |
||||||
|
|
||||||
|
// Navigation
|
||||||
|
navigation.setTitle(R.string.storage_setup_screen); |
||||||
|
|
||||||
|
// View inflation
|
||||||
|
view = inflateRes(R.layout.controller_storage_setup); |
||||||
|
|
||||||
|
// View binding
|
||||||
|
text = view.findViewById(R.id.text); |
||||||
|
button = view.findViewById(R.id.button); |
||||||
|
|
||||||
|
// View setup
|
||||||
|
button.setOnClickListener(this); |
||||||
|
|
||||||
|
updateName(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onClick(View v) { |
||||||
|
if (v == button) { |
||||||
|
requestTree(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void requestTree() { |
||||||
|
Intent i = storage.getOpenTreeIntent(); |
||||||
|
((Activity) context).startActivityForResult(i, OPEN_TREE_INTENT_RESULT_ID); |
||||||
|
updateName(); |
||||||
|
} |
||||||
|
|
||||||
|
private void updateName() { |
||||||
|
text.setText(storage.currentStorageName()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onActivityResult(int requestCode, int resultCode, Intent data) { |
||||||
|
if (requestCode == OPEN_TREE_INTENT_RESULT_ID && resultCode == Activity.RESULT_OK) { |
||||||
|
storage.handleOpenTreeIntent(data.getData()); |
||||||
|
updateName(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="match_parent"> |
||||||
|
|
||||||
|
<LinearLayout |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:orientation="vertical" |
||||||
|
android:padding="16dp"> |
||||||
|
|
||||||
|
<TextView |
||||||
|
android:id="@+id/text" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="wrap_content" /> |
||||||
|
|
||||||
|
<Button |
||||||
|
android:id="@+id/button" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:text="open" /> |
||||||
|
|
||||||
|
</LinearLayout> |
||||||
|
|
||||||
|
</ScrollView> |
Loading…
Reference in new issue