From ac1a5e0ad8bed1c658885c8f6142367a3812815d Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 17 Dec 2018 23:00:38 +0100 Subject: [PATCH 1/9] ensure the canonical repo URL is always included in mirrors list The mirror logic assumes that it has a mirrors list with at least once valid entry in it. In the index format as defined by `fdroid update`, there is always at least one valid URL: the canonical URL. That also means if there is only one item in the mirrors list, there are no other URLs to try. The initial state of the repos in the database also include the canonical URL in the mirrors list so the mirror logic works on the first index update. That makes it possible to do the first index update via SD Card or USB OTG drive. --- .../org/fdroid/fdroid/IndexV1Updater.java | 18 ++++++--- .../java/org/fdroid/fdroid/data/DBHelper.java | 9 +++++ .../java/org/fdroid/fdroid/data/Repo.java | 39 +++++++++++++------ 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java b/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java index 08f16430b..63ee68901 100644 --- a/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java +++ b/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java @@ -62,8 +62,10 @@ import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.jar.JarEntry; @@ -284,7 +286,14 @@ public class IndexV1Updater extends IndexUpdater { repo.name = getStringRepoValue(repoMap, "name"); repo.icon = getStringRepoValue(repoMap, "icon"); repo.description = getStringRepoValue(repoMap, "description"); - repo.mirrors = getStringArrayRepoValue(repoMap, "mirrors"); + + // ensure the canonical URL is included in the "mirrors" list + List mirrorsList = getStringListRepoValue(repoMap, "mirrors"); + HashSet mirrors = new HashSet<>(mirrorsList.size() + 1); + mirrors.addAll(mirrorsList); + mirrors.add(repo.address); + repo.mirrors = mirrors.toArray(new String[mirrors.size()]); + // below are optional, can be default value repo.maxage = getIntRepoValue(repoMap, "maxage"); repo.version = getIntRepoValue(repoMap, "version"); @@ -372,13 +381,12 @@ public class IndexV1Updater extends IndexUpdater { } @SuppressWarnings("unchecked") - private String[] getStringArrayRepoValue(Map repoMap, String key) { + private List getStringListRepoValue(Map repoMap, String key) { Object value = repoMap.get(key); if (value != null && value instanceof ArrayList) { - ArrayList list = (ArrayList) value; - return list.toArray(new String[list.size()]); + return (List) value; } - return null; + return Collections.emptyList(); } private HashMap parseRepo(ObjectMapper mapper, JsonParser parser) throws IOException { diff --git a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java index 9eaad2d44..e56a0d139 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java @@ -1394,6 +1394,12 @@ public class DBHelper extends SQLiteOpenHelper { return exists; } + /** + * Insert a new repo into the database. This also initializes the list of + * "mirror" URLs. There should always be at least one URL there, since the + * logic in {@link org.fdroid.fdroid.FDroidApp#getMirror(String, Repo)} + * expects at least one entry in the mirrors list. + */ private void insertRepo(SQLiteDatabase db, String name, String address, String description, String version, String enabled, String priority, String pushRequests, String pubKey) { @@ -1410,6 +1416,9 @@ public class DBHelper extends SQLiteOpenHelper { values.put(RepoTable.Cols.LAST_ETAG, (String) null); values.put(RepoTable.Cols.TIMESTAMP, 0); + String[] initializeMirrors = {address}; + values.put(Schema.RepoTable.Cols.MIRRORS, Utils.serializeCommaSeparatedString(initializeMirrors)); + switch (pushRequests) { case "ignore": values.put(RepoTable.Cols.PUSH_REQUESTS, Repo.PUSH_REQUEST_IGNORE); diff --git a/app/src/main/java/org/fdroid/fdroid/data/Repo.java b/app/src/main/java/org/fdroid/fdroid/data/Repo.java index b3b35f4b7..1a9929789 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Repo.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Repo.java @@ -37,6 +37,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; @@ -338,20 +339,29 @@ public class Repo extends ValueObject { } } + /** + * The main repo URL is included in the mirror list, so it only makes + * sense to activate this logic if there are more than one entry in the + * mirror list. + */ public boolean hasMirrors() { return (mirrors != null && mirrors.length > 1) || (userMirrors != null && userMirrors.length > 0); } + /** + * @return {@link List} of valid URLs to reach this repo, including the canonical URL + */ public List getMirrorList() { - final ArrayList allMirrors = new ArrayList(); + final HashSet allMirrors = new HashSet<>(); if (userMirrors != null) { allMirrors.addAll(Arrays.asList(userMirrors)); } if (mirrors != null) { allMirrors.addAll(Arrays.asList(mirrors)); } - return allMirrors; + allMirrors.add(address); + return new ArrayList<>(allMirrors); } /** @@ -360,19 +370,26 @@ public class Repo extends ValueObject { public int getMirrorCount() { int count = 0; for (String m : getMirrorList()) { - if (!m.equals(address)) { - if (FDroidApp.isUsingTor()) { - count++; - } else { - if (!m.contains(".onion")) { - count++; - } - } + if (FDroidApp.isUsingTor()) { + count++; + } else if (!m.contains(".onion")) { + count++; } } return count; } + /** + * The mirror logic assumes that it has a mirrors list with at least once + * valid entry in it. In the index format as defined by {@code fdroid update}, + * there is always at least one valid URL: the canonical URL. That also means + * if there is only one item in the mirrors list, there are no other URLs to try. + *

+ * The initial state of the repos in the database also include the canonical + * URL in the mirrors list so the mirror logic works on the first index + * update. That makes it possible to do the first index update via SD Card + * or USB OTG drive. + */ public String getMirror(String lastWorkingMirror) { if (TextUtils.isEmpty(lastWorkingMirror)) { lastWorkingMirror = address; @@ -382,7 +399,7 @@ public class Repo extends ValueObject { if (shuffledMirrors.size() > 1) { for (String m : shuffledMirrors) { // Return a non default, and not last used mirror - if (!m.equals(address) && !m.equals(lastWorkingMirror)) { + if (!m.equals(lastWorkingMirror)) { if (FDroidApp.isUsingTor()) { return m; } else { From 1571e28f68090b148b61296456dfceef0330bcf2 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 21 Dec 2018 00:03:39 +0100 Subject: [PATCH 2/9] support swapping with removable storage on android-21+ This uses the new Storage Access Framework, which was required for accessing files on the SD Card starting in android-19. But the API was really limited until android-21, and not really complete until android-23 or even android-26. So the levels of usability will vary a lot based on how new the version of Android is. --- .../fdroid/MainActivityEspressoTest.java | 10 + .../fdroid/views/main/NearbyViewBinder.java | 10 + app/src/full/AndroidManifest.xml | 3 + .../TreeUriScannerIntentService.java | 171 ++++++++++++++++++ .../fdroid/views/main/MainViewController.java | 28 +-- .../fdroid/views/main/NearbyViewBinder.java | 124 +++++++++++++ app/src/full/res/layout/main_tab_swap.xml | 31 ++++ .../java/org/fdroid/fdroid/IndexUpdater.java | 7 +- .../org/fdroid/fdroid/IndexV1Updater.java | 11 +- .../org/fdroid/fdroid/data/NewRepoConfig.java | 2 +- .../fdroid/fdroid/net/DownloaderFactory.java | 2 + .../fdroid/fdroid/net/ImageLoaderForUIL.java | 5 + .../fdroid/fdroid/net/TreeUriDownloader.java | 105 +++++++++++ .../fdroid/views/ManageReposActivity.java | 6 + .../fdroid/views/main/MainActivity.java | 4 +- app/src/main/res/values/strings.xml | 6 + config/checkstyle/checkstyle.xml | 2 +- 17 files changed, 494 insertions(+), 33 deletions(-) create mode 100644 app/src/basic/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java create mode 100644 app/src/full/java/org/fdroid/fdroid/localrepo/TreeUriScannerIntentService.java create mode 100644 app/src/full/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java create mode 100644 app/src/main/java/org/fdroid/fdroid/net/TreeUriDownloader.java diff --git a/app/src/androidTest/java/org/fdroid/fdroid/MainActivityEspressoTest.java b/app/src/androidTest/java/org/fdroid/fdroid/MainActivityEspressoTest.java index 29ef5621a..346a65e78 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/MainActivityEspressoTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/MainActivityEspressoTest.java @@ -1,5 +1,6 @@ package org.fdroid.fdroid; +import android.Manifest; import android.app.Instrumentation; import android.os.Build; import android.support.test.InstrumentationRegistry; @@ -7,6 +8,7 @@ import android.support.test.espresso.IdlingPolicies; import android.support.test.espresso.ViewInteraction; import android.support.test.filters.LargeTest; import android.support.test.rule.ActivityTestRule; +import android.support.test.rule.GrantPermissionRule; import android.support.test.runner.AndroidJUnit4; import android.support.test.uiautomator.UiDevice; import android.support.test.uiautomator.UiObject; @@ -120,6 +122,14 @@ public class MainActivityEspressoTest { public ActivityTestRule activityTestRule = new ActivityTestRule<>(MainActivity.class); + @Rule + public GrantPermissionRule accessCoarseLocationPermissionRule = GrantPermissionRule.grant( + Manifest.permission.ACCESS_COARSE_LOCATION); + + @Rule + public GrantPermissionRule writeExternalStoragePermissionRule = GrantPermissionRule.grant( + Manifest.permission.WRITE_EXTERNAL_STORAGE); + @Test public void bottomNavFlavorCheck() { onView(withText(R.string.updates)).check(matches(isDisplayed())); diff --git a/app/src/basic/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java b/app/src/basic/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java new file mode 100644 index 000000000..f3e21451a --- /dev/null +++ b/app/src/basic/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java @@ -0,0 +1,10 @@ +package org.fdroid.fdroid.views.main; + +import android.app.Activity; +import android.content.Intent; + +class NearbyViewBinder { + static void onActivityResult(Activity activity, Intent data) { + throw new IllegalStateException("unimplemented"); + } +} diff --git a/app/src/full/AndroidManifest.xml b/app/src/full/AndroidManifest.xml index 0cee2ee2c..5bdd57aec 100644 --- a/app/src/full/AndroidManifest.xml +++ b/app/src/full/AndroidManifest.xml @@ -77,6 +77,9 @@ + + * + * 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.localrepo; + +import android.annotation.TargetApi; +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Process; +import android.support.v4.provider.DocumentFile; +import android.util.Log; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.fdroid.fdroid.IndexV1Updater; +import org.fdroid.fdroid.IndexUpdater; +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; +import org.fdroid.fdroid.views.main.MainActivity; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.security.cert.Certificate; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; + +/** + * An {@link IntentService} subclass for handling asynchronous scanning of a + * removable storage device like an SD Card or USB OTG thumb drive using the + * Storage Access Framework. Permission must first be granted by the user + * {@link android.content.Intent#ACTION_OPEN_DOCUMENT_TREE} or + * {@link android.os.storage.StorageVolume#createAccessIntent(String)}request, + * then F-Droid will have permanent access to that{@link Uri}. + *

+ * Even though the Storage Access Framework was introduced in + * {@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}. + * + * @see The Storage Situation: Removable Storage + * @see Using Scoped Directory Access + * @see Open Files using Storage Access Framework + */ +@TargetApi(21) +public class TreeUriScannerIntentService extends IntentService { + public static final String TAG = "TreeUriScannerIntentSer"; + + private static final String ACTION_SCAN_TREE_URI = "org.fdroid.fdroid.localrepo.action.SCAN_TREE_URI"; + + public TreeUriScannerIntentService() { + super("TreeUriScannerIntentService"); + } + + public static void scan(Context context, Uri data) { + Intent intent = new Intent(context, TreeUriScannerIntentService.class); + intent.setAction(ACTION_SCAN_TREE_URI); + intent.setData(data); + context.startService(intent); + } + + @Override + protected void onHandleIntent(Intent intent) { + if (intent == null || !ACTION_SCAN_TREE_URI.equals(intent.getAction())) { + return; + } + Uri treeUri = intent.getData(); + if (treeUri == null) { + return; + } + Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); + DocumentFile treeFile = DocumentFile.fromTreeUri(this, treeUri); + searchDirectory(treeFile); + } + + private void searchDirectory(DocumentFile documentFileDir) { + DocumentFile[] documentFiles = documentFileDir.listFiles(); + if (documentFiles == null) { + return; + } + for (DocumentFile documentFile : documentFiles) { + if (documentFile.isDirectory()) { + searchDirectory(documentFile); + } else { + if (IndexV1Updater.SIGNED_FILE_NAME.equals(documentFile.getName())) { + registerRepo(documentFile); + } + } + } + } + + /** + * For all files called {@link IndexV1Updater#SIGNED_FILE_NAME} found, check + * the JAR signature and read the fingerprint of the signing certificate. + * The fingerprint is then used to find whether this local repo is a mirror + * of an existing repo, or a totally new repo. In order to verify the + * signatures in the JAR, the whole file needs to be read in first. + * + * @see JarInputStream#JarInputStream(InputStream, boolean) + */ + private void registerRepo(DocumentFile index) { + InputStream inputStream = null; + try { + Log.i(TAG, "FOUND: " + index.getUri()); + inputStream = getContentResolver().openInputStream(index.getUri()); + Log.i(TAG, "repo URL: " + index.getParentFile().getUri()); + registerRepo(this, inputStream, index.getParentFile().getUri()); + } catch (IOException | IndexUpdater.SigningException e) { + e.printStackTrace(); + } finally { + Utils.closeQuietly(inputStream); + } + } + + public static void registerRepo(Context context, InputStream inputStream, Uri repoUri) + throws IOException, IndexUpdater.SigningException { + if (inputStream == null) { + return; + } + File destFile = File.createTempFile("dl-", IndexV1Updater.SIGNED_FILE_NAME, context.getCacheDir()); + FileUtils.copyInputStreamToFile(inputStream, destFile); + JarFile jarFile = new JarFile(destFile, true); + JarEntry indexEntry = (JarEntry) jarFile.getEntry(IndexV1Updater.DATA_FILE_NAME); + IOUtils.readLines(jarFile.getInputStream(indexEntry)); + Certificate certificate = IndexUpdater.getSigningCertFromJar(indexEntry); + Log.i(TAG, "Got certificate: " + certificate); + String fingerprint = Utils.calcFingerprint(certificate); + Log.i(TAG, "Got fingerprint: " + fingerprint); + destFile.delete(); + + Log.i(TAG, "Found a valid, signed index-v1.json"); + for (Repo repo : RepoProvider.Helper.all(context)) { + if (fingerprint.equals(repo.fingerprint)) { + Log.i(TAG, repo.address + " has the SAME fingerprint: " + fingerprint); + } else { + Log.i(TAG, repo.address + " different fingerprint"); + } + } + + Intent intent = new Intent(context, MainActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setData(repoUri.buildUpon() + .appendQueryParameter("fingerprint", fingerprint) + .build()); + context.startActivity(intent); + // TODO parse repo URL/mirrors/fingerprint using Jackson + // https://stackoverflow.com/questions/24835431/use-jackson-to-stream-parse-an-array-of-json-objects# + // TODO rework IndexUpdater.getSigningCertFromJar to work for here + // TODO check whether fingerprint is already in the database + } +} diff --git a/app/src/full/java/org/fdroid/fdroid/views/main/MainViewController.java b/app/src/full/java/org/fdroid/fdroid/views/main/MainViewController.java index 849a15a4b..447ec21f1 100644 --- a/app/src/full/java/org/fdroid/fdroid/views/main/MainViewController.java +++ b/app/src/full/java/org/fdroid/fdroid/views/main/MainViewController.java @@ -1,17 +1,12 @@ package org.fdroid.fdroid.views.main; -import android.content.Intent; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.RecyclerView; -import android.view.View; -import android.widget.Button; import android.widget.FrameLayout; -import android.widget.TextView; import org.fdroid.fdroid.R; import org.fdroid.fdroid.views.PreferencesFragment; -import org.fdroid.fdroid.views.swap.SwapWorkflowActivity; import org.fdroid.fdroid.views.updates.UpdatesViewBinder; /** @@ -65,29 +60,8 @@ class MainViewController extends RecyclerView.ViewHolder { new CategoriesViewBinder(activity, frame); } - /** - * A splash screen encouraging people to start the swap process. - * The swap process is quite heavy duty in that it fires up Bluetooth and/or WiFi in - * order to scan for peers. As such, it is quite convenient to have a more lightweight view to show - * in the main navigation that doesn't automatically start doing things when the user touches the - * navigation menu in the bottom navigation. - */ public void bindSwapView() { - View swapView = activity.getLayoutInflater().inflate(R.layout.main_tab_swap, frame, true); - - // To allow for whitelabel versions of F-Droid, make sure not to hardcode "F-Droid" into our - // translation here. - TextView subtext = (TextView) swapView.findViewById(R.id.text2); - subtext.setText(activity.getString(R.string.nearby_splash__both_parties_need_fdroid, - activity.getString(R.string.app_name))); - - Button startButton = (Button) swapView.findViewById(R.id.button); - startButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - activity.startActivity(new Intent(activity, SwapWorkflowActivity.class)); - } - }); + new NearbyViewBinder(activity, frame); } /** 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 new file mode 100644 index 000000000..8840b915c --- /dev/null +++ b/app/src/full/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java @@ -0,0 +1,124 @@ +package org.fdroid.fdroid.views.main; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Environment; +import android.support.annotation.RequiresApi; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.localrepo.TreeUriScannerIntentService; +import org.fdroid.fdroid.views.swap.SwapWorkflowActivity; + +import java.io.File; + +/** + * A splash screen encouraging people to start the swap process. The swap + * process is quite heavy duty in that it fires up Bluetooth and/or WiFi + * in order to scan for peers. As such, it is quite convenient to have a + * more lightweight view to show in the main navigation that doesn't + * automatically start doing things when the user touches the navigation + * menu in the bottom navigation. + *

+ * Lots of pieces of the nearby/swap functionality require that the user grant + * F-Droid permissions at runtime on {@code android-23} and higher. On devices + * that have a removable SD Card that is currently mounted, this will request + * permission to read it, so that F-Droid can look for repos on the SD Card. + *

+ * Once {@link Manifest.permission#READ_EXTERNAL_STORAGE} or + * {@link Manifest.permission#WRITE_EXTERNAL_STORAGE} is granted for F-Droid, + * then it can read any file on an SD Card and no more prompts are needed. For + * USB OTG drives, the only way to get read permissions is to prompt the user + * via {@link Intent#ACTION_OPEN_DOCUMENT_TREE}. + *

+ * For write permissions, {@code android-19} and {@code android-20} devices are + * basically screwed here. {@link Intent#ACTION_OPEN_DOCUMENT_TREE} was added + * in {@code android-21}, and there does not seem to be any other way to get + * write access to the the removable storage. + * + * @see TreeUriScannerIntentService + */ +class NearbyViewBinder { + public static final String TAG = "NearbyViewBinder"; + + static File externalStorage = null; + + NearbyViewBinder(final Activity activity, FrameLayout parent) { + View swapView = activity.getLayoutInflater().inflate(R.layout.main_tab_swap, parent, true); + + TextView subtext = swapView.findViewById(R.id.text2); + subtext.setText(activity.getString(R.string.nearby_splash__both_parties_need_fdroid, + activity.getString(R.string.app_name))); + + ImageView nearbySplash = swapView.findViewById(R.id.image); + + Button startButton = swapView.findViewById(R.id.button); + startButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + activity.startActivity(new Intent(activity, SwapWorkflowActivity.class)); + } + }); + + if (Build.VERSION.SDK_INT >= 21) { + Log.i(TAG, "Environment.isExternalStorageRemovable(activity.getExternalFilesDir(\"\")) " + + Environment.isExternalStorageRemovable(activity.getExternalFilesDir(""))); + File[] dirs = activity.getExternalFilesDirs(""); + if (dirs != null) { + for (File f : dirs) { + if (f != null && Environment.isExternalStorageRemovable(f)) { + // remove Android/data/org.fdroid.fdroid/files to get root + externalStorage = f.getParentFile().getParentFile().getParentFile().getParentFile(); + break; + } + } + } + } else if (Environment.isExternalStorageRemovable() && + (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED_READ_ONLY) + || Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))) { + Log.i(TAG, "<21 isExternalStorageRemovable MEDIA_MOUNTED"); + externalStorage = Environment.getExternalStorageDirectory(); + } + + if (externalStorage != null) { + nearbySplash.setVisibility(View.GONE); + View readExternalStorage = swapView.findViewById(R.id.readExternalStorage); + readExternalStorage.setVisibility(View.VISIBLE); + Button requestReadExternalStorage = swapView.findViewById(R.id.requestReadExternalStorage); + requestReadExternalStorage.setOnClickListener(new View.OnClickListener() { + @RequiresApi(api = 21) + @Override + public void onClick(View v) { + File storage = externalStorage.getParentFile(); + File[] files = storage.listFiles(); + String msg = ""; + if (files != null) for (File f : files) { + msg += "|" + f.getName(); + } + Toast.makeText(activity, msg, Toast.LENGTH_LONG).show(); + final String writeExternalStorage = Manifest.permission.WRITE_EXTERNAL_STORAGE; + if (Build.VERSION.SDK_INT >= 23 + && !externalStorage.canRead() + && PackageManager.PERMISSION_GRANTED + != ContextCompat.checkSelfPermission(activity, writeExternalStorage)) { + ActivityCompat.requestPermissions(activity, new String[]{writeExternalStorage}, + MainActivity.REQUEST_STORAGE_PERMISSIONS); + } else { + // TODO do something + } + } + }); + + } + } +} diff --git a/app/src/full/res/layout/main_tab_swap.xml b/app/src/full/res/layout/main_tab_swap.xml index 020fb9140..a187febb2 100644 --- a/app/src/full/res/layout/main_tab_swap.xml +++ b/app/src/full/res/layout/main_tab_swap.xml @@ -54,6 +54,37 @@ android:layout_marginStart="48dp" android:layout_marginLeft="48dp" /> + + +