From 525f99b056ff7ae1e2f134bd6243a9c4f9874a89 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 11 Sep 2018 10:30:00 +0200 Subject: [PATCH] implement mirror/repos on USB OTG via Storage Access Framework * https://developer.android.com/training/articles/scoped-directory-access One potential future direction, if this proves too limiting: https://github.com/magnusja/libaums --- .../nearby/TreeUriScannerIntentService.java | 32 +++++++ .../nearby/TreeUriScannerIntentService.java | 29 +++++- .../fdroid/fdroid/nearby/TreeUriUtils.java | 96 +++++++++++++++++++ .../fdroid/views/main/NearbyViewBinder.java | 35 +++++++ app/src/full/res/layout/main_tab_swap.xml | 36 ++++++- .../fdroid/fdroid/net/TreeUriDownloader.java | 2 +- .../fdroid/views/main/MainActivity.java | 12 ++- app/src/main/res/values/strings.xml | 3 +- 8 files changed, 239 insertions(+), 6 deletions(-) create mode 100644 app/src/basic/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java create mode 100644 app/src/full/java/org/fdroid/fdroid/nearby/TreeUriUtils.java diff --git a/app/src/basic/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java b/app/src/basic/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java new file mode 100644 index 000000000..979187247 --- /dev/null +++ b/app/src/basic/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 Senecto Limited + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +package org.fdroid.fdroid.nearby; + +import android.app.Activity; +import android.content.Intent; + +/** + * Dummy version for basic app flavor. + */ +public class TreeUriScannerIntentService { + public static void onActivityResult(Activity activity, Intent intent) { + throw new IllegalStateException("unimplemented"); + } +} diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java b/app/src/full/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java index b27c57256..e3f8679ad 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java @@ -20,19 +20,24 @@ package org.fdroid.fdroid.nearby; import android.annotation.TargetApi; +import android.app.Activity; import android.app.IntentService; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.os.Build; import android.os.Process; import android.support.v4.provider.DocumentFile; import android.util.Log; +import android.widget.Toast; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.fdroid.fdroid.AddRepoIntentService; import org.fdroid.fdroid.IndexUpdater; import org.fdroid.fdroid.IndexV1Updater; import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; @@ -57,8 +62,11 @@ import java.util.jar.JarInputStream; * {@link android.os.Build.VERSION_CODES#KITKAT android-19}, this approach is only * workable if {@link android.content.Intent#ACTION_OPEN_DOCUMENT_TREE} is available. * It was added in {@link android.os.Build.VERSION_CODES#LOLLIPOP android-21}. + * {@link android.os.storage.StorageVolume#createAccessIntent(String)} is also + * necessary to do this with any kind of rational UX. * - * @see The Storage Situation: Removable Storage + * @see The Storage Situation: Removable Storage + * @see Be Careful with Scoped Directory Access * @see Using Scoped Directory Access * @see Open Files using Storage Access Framework */ @@ -81,6 +89,25 @@ public class TreeUriScannerIntentService extends IntentService { } } + /** + * Now determine if it is External Storage that must be handled by the + * {@link TreeUriScannerIntentService} or whether it is External Storage + * like an SD Card that can be directly accessed via the file system. + */ + public static void onActivityResult(Activity activity, Intent intent) { + Uri uri = intent.getData(); + if (uri != null) { + if (Build.VERSION.SDK_INT >= 19) { + ContentResolver contentResolver = activity.getContentResolver(); + int perms = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; + contentResolver.takePersistableUriPermission(uri, perms); + } + String msg = String.format(activity.getString(R.string.swap_toast_using_path), uri.toString()); + Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show(); + scan(activity, uri); + } + } + @Override protected void onHandleIntent(Intent intent) { if (intent == null || !ACTION_SCAN_TREE_URI.equals(intent.getAction())) { diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/TreeUriUtils.java b/app/src/full/java/org/fdroid/fdroid/nearby/TreeUriUtils.java new file mode 100644 index 000000000..3098a7a34 --- /dev/null +++ b/app/src/full/java/org/fdroid/fdroid/nearby/TreeUriUtils.java @@ -0,0 +1,96 @@ +package org.fdroid.fdroid.nearby; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import android.os.storage.StorageManager; +import android.provider.DocumentsContract; +import android.support.annotation.Nullable; + +import java.io.File; +import java.lang.reflect.Array; +import java.lang.reflect.Method; + + +/** + * @see Android 5.0 DocumentFile from tree URI + */ +public final class TreeUriUtils { + public static final String TAG = "TreeUriUtils"; + + private static final String PRIMARY_VOLUME_NAME = "primary"; + + @Nullable + public static String getFullPathFromTreeUri(Context context, @Nullable final Uri treeUri) { + if (treeUri == null) return null; + String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri), context); + if (volumePath == null) return File.separator; + if (volumePath.endsWith(File.separator)) + volumePath = volumePath.substring(0, volumePath.length() - 1); + + String documentPath = getDocumentPathFromTreeUri(treeUri); + if (documentPath.endsWith(File.separator)) + documentPath = documentPath.substring(0, documentPath.length() - 1); + + if (documentPath.length() > 0) { + if (documentPath.startsWith(File.separator)) + return volumePath + documentPath; + else + return volumePath + File.separator + documentPath; + } else return volumePath; + } + + + @SuppressLint("ObsoleteSdkInt") + private static String getVolumePath(final String volumeId, Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null; + try { + StorageManager mStorageManager = + (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); + Class storageVolumeClazz = Class.forName("android.os.storage.StorageVolume"); + Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList"); + Method getUuid = storageVolumeClazz.getMethod("getUuid"); + Method getPath = storageVolumeClazz.getMethod("getPath"); + Method isPrimary = storageVolumeClazz.getMethod("isPrimary"); + Object result = getVolumeList.invoke(mStorageManager); + + final int length = Array.getLength(result); + for (int i = 0; i < length; i++) { + Object storageVolumeElement = Array.get(result, i); + String uuid = (String) getUuid.invoke(storageVolumeElement); + Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement); + + // primary volume? + if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) + return (String) getPath.invoke(storageVolumeElement); + + // other volumes? + if (uuid != null && uuid.equals(volumeId)) + return (String) getPath.invoke(storageVolumeElement); + } + // not found. + return null; + } catch (Exception ex) { + return null; + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static String getVolumeIdFromTreeUri(final Uri treeUri) { + final String docId = DocumentsContract.getTreeDocumentId(treeUri); + final String[] split = docId.split(":"); + if (split.length > 0) return split[0]; + else return null; + } + + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static String getDocumentPathFromTreeUri(final Uri treeUri) { + final String docId = DocumentsContract.getTreeDocumentId(treeUri); + final String[] split = docId.split(":"); + if ((split.length >= 2) && (split[1] != null)) return split[1]; + else return File.separator; + } +} \ No newline at end of file diff --git a/app/src/full/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java b/app/src/full/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java index 88a00c973..74996bfe0 100644 --- a/app/src/full/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java +++ b/app/src/full/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java @@ -2,13 +2,20 @@ package org.fdroid.fdroid.views.main; import android.Manifest; import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; import android.content.Intent; +import android.content.UriPermission; import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Build; import android.os.Environment; +import android.os.storage.StorageManager; +import android.os.storage.StorageVolume; import android.support.annotation.RequiresApi; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; +import android.support.v4.provider.DocumentFile; import android.util.Log; import android.view.View; import android.widget.Button; @@ -17,6 +24,8 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.nearby.TreeUriUtils; import org.fdroid.fdroid.nearby.SDCardScannerService; import org.fdroid.fdroid.nearby.SwapService; import org.fdroid.fdroid.nearby.TreeUriScannerIntentService; @@ -131,5 +140,31 @@ class NearbyViewBinder { } }); } + + if (Build.VERSION.SDK_INT < 24) { + return; + } + StorageManager storageManager = (StorageManager) activity.getSystemService(Context.STORAGE_SERVICE); + for (StorageVolume storageVolume : storageManager.getStorageVolumes()) { + if (storageVolume.isRemovable() && !storageVolume.isPrimary()) { + Log.i(TAG, "StorageVolume: " + storageVolume); + final Intent intent = storageVolume.createAccessIntent(null); + if (intent == null) { + Utils.debugLog(TAG, "Got null Storage Volume access Intent"); + return; + } + TextView storageVolumeText = swapView.findViewById(R.id.storage_volume_text); + storageVolumeText.setVisibility(View.VISIBLE); + Button requestStorageVolume = swapView.findViewById(R.id.request_storage_volume_button); + requestStorageVolume.setText(storageVolume.getDescription(activity)); + requestStorageVolume.setVisibility(View.VISIBLE); + requestStorageVolume.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + activity.startActivityForResult(intent, MainActivity.REQUEST_STORAGE_ACCESS); + } + }); + } + } } } diff --git a/app/src/full/res/layout/main_tab_swap.xml b/app/src/full/res/layout/main_tab_swap.xml index 3db7d5eee..dff52eb34 100644 --- a/app/src/full/res/layout/main_tab_swap.xml +++ b/app/src/full/res/layout/main_tab_swap.xml @@ -72,11 +72,12 @@ android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/nearby_splash__read_external_storage" - android:textSize="20sp" + android:textSize="17sp" android:textColor="?attr/lightGrayTextColor" android:textAlignment="center" android:gravity="center" - android:layout_marginEnd="24dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginRight="24dp" + android:layout_marginEnd="24dp" app:layout_constraintEnd_toEndOf="parent" + android:layout_marginRight="24dp" android:layout_marginStart="24dp" app:layout_constraintStart_toStartOf="parent" android:layout_marginLeft="24dp" android:layout_marginTop="48dp" app:layout_constraintTop_toBottomOf="@+id/both_parties_need_fdroid_text" @@ -96,4 +97,35 @@ app:layout_constraintTop_toBottomOf="@+id/read_external_storage_text" android:visibility="gone" tools:visibility="visible"/> + + +