Merge branch 'usb-otg-support' into 'master'
USB OTG support See merge request fdroid/fdroidclient!830
This commit is contained in:
		
						commit
						4260539374
					
				@ -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");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,10 +1,9 @@
 | 
				
			|||||||
package org.fdroid.fdroid.views.main;
 | 
					package org.fdroid.fdroid.views.main;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.app.Activity;
 | 
					import android.app.Activity;
 | 
				
			||||||
import android.content.Intent;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NearbyViewBinder {
 | 
					class NearbyViewBinder {
 | 
				
			||||||
    static void onActivityResult(Activity activity, Intent data) {
 | 
					    public static void updateUsbOtg(final Activity activity) {
 | 
				
			||||||
        throw new IllegalStateException("unimplemented");
 | 
					        throw new IllegalStateException("unimplemented");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -28,6 +28,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    <uses-feature android:name="android.hardware.nfc" android:required="false"/>
 | 
					    <uses-feature android:name="android.hardware.nfc" android:required="false"/>
 | 
				
			||||||
    <uses-feature android:name="android.hardware.bluetooth" android:required="false"/>
 | 
					    <uses-feature android:name="android.hardware.bluetooth" android:required="false"/>
 | 
				
			||||||
 | 
					    <uses-feature android:name="android.hardware.usb.host" android:required="false"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <uses-permission android:name="android.permission.INTERNET"/>
 | 
					    <uses-permission android:name="android.permission.INTERNET"/>
 | 
				
			||||||
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
 | 
					    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
 | 
				
			||||||
@ -42,6 +43,8 @@
 | 
				
			|||||||
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 | 
					    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 | 
				
			||||||
    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
 | 
					    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
 | 
				
			||||||
    <uses-permission android:name="android.permission.NFC"/>
 | 
					    <uses-permission android:name="android.permission.NFC"/>
 | 
				
			||||||
 | 
					    <uses-permission android:name="android.permission.USB_PERMISSION"
 | 
				
			||||||
 | 
					                     android:maxSdkVersion="22"/> <!-- maybe unnecessary -->
 | 
				
			||||||
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
 | 
					    <uses-permission android:name="android.permission.WAKE_LOCK"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
 | 
					    <uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
 | 
				
			||||||
@ -78,7 +81,6 @@
 | 
				
			|||||||
                android:exported="false"/>
 | 
					                android:exported="false"/>
 | 
				
			||||||
        <service android:name=".nearby.SwapService"/>
 | 
					        <service android:name=".nearby.SwapService"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <service android:name=".nearby.LocalHTTPDManager"/>
 | 
					 | 
				
			||||||
        <service
 | 
					        <service
 | 
				
			||||||
                android:name=".nearby.LocalRepoService"
 | 
					                android:name=".nearby.LocalRepoService"
 | 
				
			||||||
                android:exported="false"/>
 | 
					                android:exported="false"/>
 | 
				
			||||||
@ -89,6 +91,17 @@
 | 
				
			|||||||
                android:name=".nearby.SDCardScannerService"
 | 
					                android:name=".nearby.SDCardScannerService"
 | 
				
			||||||
                android:exported="false"/>
 | 
					                android:exported="false"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					                android:name=".nearby.UsbDeviceAttachedActivity"
 | 
				
			||||||
 | 
					                android:noHistory="true">
 | 
				
			||||||
 | 
					            <intent-filter>
 | 
				
			||||||
 | 
					                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"/>
 | 
				
			||||||
 | 
					            </intent-filter>
 | 
				
			||||||
 | 
					            <meta-data
 | 
				
			||||||
 | 
					                    android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
 | 
				
			||||||
 | 
					                    android:resource="@xml/device_filter"/>
 | 
				
			||||||
 | 
					        </activity>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <activity
 | 
					        <activity
 | 
				
			||||||
                android:name=".panic.PanicPreferencesActivity"
 | 
					                android:name=".panic.PanicPreferencesActivity"
 | 
				
			||||||
                android:label="@string/panic_settings"
 | 
					                android:label="@string/panic_settings"
 | 
				
			||||||
 | 
				
			|||||||
@ -20,19 +20,24 @@
 | 
				
			|||||||
package org.fdroid.fdroid.nearby;
 | 
					package org.fdroid.fdroid.nearby;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.annotation.TargetApi;
 | 
					import android.annotation.TargetApi;
 | 
				
			||||||
 | 
					import android.app.Activity;
 | 
				
			||||||
import android.app.IntentService;
 | 
					import android.app.IntentService;
 | 
				
			||||||
 | 
					import android.content.ContentResolver;
 | 
				
			||||||
import android.content.Context;
 | 
					import android.content.Context;
 | 
				
			||||||
import android.content.Intent;
 | 
					import android.content.Intent;
 | 
				
			||||||
import android.net.Uri;
 | 
					import android.net.Uri;
 | 
				
			||||||
 | 
					import android.os.Build;
 | 
				
			||||||
import android.os.Process;
 | 
					import android.os.Process;
 | 
				
			||||||
import android.support.v4.provider.DocumentFile;
 | 
					import android.support.v4.provider.DocumentFile;
 | 
				
			||||||
import android.util.Log;
 | 
					import android.util.Log;
 | 
				
			||||||
 | 
					import android.widget.Toast;
 | 
				
			||||||
import org.apache.commons.io.FileUtils;
 | 
					import org.apache.commons.io.FileUtils;
 | 
				
			||||||
import org.apache.commons.io.IOUtils;
 | 
					import org.apache.commons.io.IOUtils;
 | 
				
			||||||
import org.fdroid.fdroid.AddRepoIntentService;
 | 
					import org.fdroid.fdroid.AddRepoIntentService;
 | 
				
			||||||
import org.fdroid.fdroid.IndexUpdater;
 | 
					import org.fdroid.fdroid.IndexUpdater;
 | 
				
			||||||
import org.fdroid.fdroid.IndexV1Updater;
 | 
					import org.fdroid.fdroid.IndexV1Updater;
 | 
				
			||||||
import org.fdroid.fdroid.Preferences;
 | 
					import org.fdroid.fdroid.Preferences;
 | 
				
			||||||
 | 
					import org.fdroid.fdroid.R;
 | 
				
			||||||
import org.fdroid.fdroid.Utils;
 | 
					import org.fdroid.fdroid.Utils;
 | 
				
			||||||
import org.fdroid.fdroid.data.Repo;
 | 
					import org.fdroid.fdroid.data.Repo;
 | 
				
			||||||
import org.fdroid.fdroid.data.RepoProvider;
 | 
					import org.fdroid.fdroid.data.RepoProvider;
 | 
				
			||||||
@ -41,6 +46,7 @@ import java.io.File;
 | 
				
			|||||||
import java.io.IOException;
 | 
					import java.io.IOException;
 | 
				
			||||||
import java.io.InputStream;
 | 
					import java.io.InputStream;
 | 
				
			||||||
import java.security.cert.Certificate;
 | 
					import java.security.cert.Certificate;
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
import java.util.jar.JarEntry;
 | 
					import java.util.jar.JarEntry;
 | 
				
			||||||
import java.util.jar.JarFile;
 | 
					import java.util.jar.JarFile;
 | 
				
			||||||
import java.util.jar.JarInputStream;
 | 
					import java.util.jar.JarInputStream;
 | 
				
			||||||
@ -57,8 +63,11 @@ import java.util.jar.JarInputStream;
 | 
				
			|||||||
 * {@link android.os.Build.VERSION_CODES#KITKAT android-19}, this approach is only
 | 
					 * {@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.
 | 
					 * 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}.
 | 
					 * 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 <a href="https://commonsware.com/blog/2017/11/15/storage-situation-removable-storage.html"> The Storage Situation: Removable Storage </a>
 | 
					 * @see <a href="https://commonsware.com/blog/2017/11/15/storage-situation-removable-storage.html">The Storage Situation: Removable Storage </a>
 | 
				
			||||||
 | 
					 * @see <a href="https://commonsware.com/blog/2016/11/18/be-careful-scoped-directory-access.html">Be Careful with Scoped Directory Access</a>
 | 
				
			||||||
 * @see <a href="https://developer.android.com/training/articles/scoped-directory-access.html">Using Scoped Directory Access</a>
 | 
					 * @see <a href="https://developer.android.com/training/articles/scoped-directory-access.html">Using Scoped Directory Access</a>
 | 
				
			||||||
 * @see <a href="https://developer.android.com/guide/topics/providers/document-provider.html">Open Files using Storage Access Framework</a>
 | 
					 * @see <a href="https://developer.android.com/guide/topics/providers/document-provider.html">Open Files using Storage Access Framework</a>
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@ -81,6 +90,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
 | 
					    @Override
 | 
				
			||||||
    protected void onHandleIntent(Intent intent) {
 | 
					    protected void onHandleIntent(Intent intent) {
 | 
				
			||||||
        if (intent == null || !ACTION_SCAN_TREE_URI.equals(intent.getAction())) {
 | 
					        if (intent == null || !ACTION_SCAN_TREE_URI.equals(intent.getAction())) {
 | 
				
			||||||
@ -95,20 +123,34 @@ public class TreeUriScannerIntentService extends IntentService {
 | 
				
			|||||||
        searchDirectory(treeFile);
 | 
					        searchDirectory(treeFile);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Recursively search for {@link IndexV1Updater#SIGNED_FILE_NAME} starting
 | 
				
			||||||
 | 
					     * from the given directory, looking at files first before recursing into
 | 
				
			||||||
 | 
					     * directories.  This is "depth last" since the index file is much more
 | 
				
			||||||
 | 
					     * likely to be shallow than deep, and there can be a lot of files to
 | 
				
			||||||
 | 
					     * search through starting at 4 or more levels deep, like the fdroid
 | 
				
			||||||
 | 
					     * icons dirs and the per-app "external storage" dirs.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    private void searchDirectory(DocumentFile documentFileDir) {
 | 
					    private void searchDirectory(DocumentFile documentFileDir) {
 | 
				
			||||||
        DocumentFile[] documentFiles = documentFileDir.listFiles();
 | 
					        DocumentFile[] documentFiles = documentFileDir.listFiles();
 | 
				
			||||||
        if (documentFiles == null) {
 | 
					        if (documentFiles == null) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        boolean foundIndex = false;
 | 
				
			||||||
 | 
					        ArrayList<DocumentFile> dirs = new ArrayList<>();
 | 
				
			||||||
        for (DocumentFile documentFile : documentFiles) {
 | 
					        for (DocumentFile documentFile : documentFiles) {
 | 
				
			||||||
            if (documentFile.isDirectory()) {
 | 
					            if (documentFile.isDirectory()) {
 | 
				
			||||||
                searchDirectory(documentFile);
 | 
					                dirs.add(documentFile);
 | 
				
			||||||
            } else {
 | 
					            } else if (!foundIndex) {
 | 
				
			||||||
                if (IndexV1Updater.SIGNED_FILE_NAME.equals(documentFile.getName())) {
 | 
					                if (IndexV1Updater.SIGNED_FILE_NAME.equals(documentFile.getName())) {
 | 
				
			||||||
                    registerRepo(documentFile);
 | 
					                    registerRepo(documentFile);
 | 
				
			||||||
 | 
					                    foundIndex = true;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        for (DocumentFile dir : dirs) {
 | 
				
			||||||
 | 
					            searchDirectory(dir);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -123,9 +165,7 @@ public class TreeUriScannerIntentService extends IntentService {
 | 
				
			|||||||
    private void registerRepo(DocumentFile index) {
 | 
					    private void registerRepo(DocumentFile index) {
 | 
				
			||||||
        InputStream inputStream = null;
 | 
					        InputStream inputStream = null;
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            Log.i(TAG, "FOUND: " + index.getUri());
 | 
					 | 
				
			||||||
            inputStream = getContentResolver().openInputStream(index.getUri());
 | 
					            inputStream = getContentResolver().openInputStream(index.getUri());
 | 
				
			||||||
            Log.i(TAG, "repo URL: " + index.getParentFile().getUri());
 | 
					 | 
				
			||||||
            registerRepo(this, inputStream, index.getParentFile().getUri());
 | 
					            registerRepo(this, inputStream, index.getParentFile().getUri());
 | 
				
			||||||
        } catch (IOException | IndexUpdater.SigningException e) {
 | 
					        } catch (IOException | IndexUpdater.SigningException e) {
 | 
				
			||||||
            e.printStackTrace();
 | 
					            e.printStackTrace();
 | 
				
			||||||
@ -145,7 +185,6 @@ public class TreeUriScannerIntentService extends IntentService {
 | 
				
			|||||||
        JarEntry indexEntry = (JarEntry) jarFile.getEntry(IndexV1Updater.DATA_FILE_NAME);
 | 
					        JarEntry indexEntry = (JarEntry) jarFile.getEntry(IndexV1Updater.DATA_FILE_NAME);
 | 
				
			||||||
        IOUtils.readLines(jarFile.getInputStream(indexEntry));
 | 
					        IOUtils.readLines(jarFile.getInputStream(indexEntry));
 | 
				
			||||||
        Certificate certificate = IndexUpdater.getSigningCertFromJar(indexEntry);
 | 
					        Certificate certificate = IndexUpdater.getSigningCertFromJar(indexEntry);
 | 
				
			||||||
        Log.i(TAG, "Got certificate: " + certificate);
 | 
					 | 
				
			||||||
        String fingerprint = Utils.calcFingerprint(certificate);
 | 
					        String fingerprint = Utils.calcFingerprint(certificate);
 | 
				
			||||||
        Log.i(TAG, "Got fingerprint: " + fingerprint);
 | 
					        Log.i(TAG, "Got fingerprint: " + fingerprint);
 | 
				
			||||||
        destFile.delete();
 | 
					        destFile.delete();
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										96
									
								
								app/src/full/java/org/fdroid/fdroid/nearby/TreeUriUtils.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								app/src/full/java/org/fdroid/fdroid/nearby/TreeUriUtils.java
									
									
									
									
									
										Normal file
									
								
							@ -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 <a href="https://stackoverflow.com/a/36162691">Android 5.0 DocumentFile from tree URI</a>
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,112 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2018-2019 Hans-Christoph Steiner <hans@eds.org>
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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.BroadcastReceiver;
 | 
				
			||||||
 | 
					import android.content.ComponentName;
 | 
				
			||||||
 | 
					import android.content.ContentResolver;
 | 
				
			||||||
 | 
					import android.content.Context;
 | 
				
			||||||
 | 
					import android.content.Intent;
 | 
				
			||||||
 | 
					import android.content.IntentFilter;
 | 
				
			||||||
 | 
					import android.content.UriPermission;
 | 
				
			||||||
 | 
					import android.database.ContentObserver;
 | 
				
			||||||
 | 
					import android.hardware.usb.UsbManager;
 | 
				
			||||||
 | 
					import android.net.Uri;
 | 
				
			||||||
 | 
					import android.os.Build;
 | 
				
			||||||
 | 
					import android.os.Bundle;
 | 
				
			||||||
 | 
					import android.os.Handler;
 | 
				
			||||||
 | 
					import android.support.annotation.Nullable;
 | 
				
			||||||
 | 
					import android.support.annotation.RequiresApi;
 | 
				
			||||||
 | 
					import android.text.TextUtils;
 | 
				
			||||||
 | 
					import android.util.Log;
 | 
				
			||||||
 | 
					import org.fdroid.fdroid.views.main.MainActivity;
 | 
				
			||||||
 | 
					import org.fdroid.fdroid.views.main.NearbyViewBinder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This is just a shim to receive {@link UsbManager#ACTION_USB_ACCESSORY_ATTACHED}
 | 
				
			||||||
 | 
					 * events then open up the right screen in {@link MainActivity}.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class UsbDeviceAttachedActivity extends Activity {
 | 
				
			||||||
 | 
					    public static final String TAG = "UsbDeviceAttachedActivi";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final HashMap<Uri, ContentObserver> contentObservers = new HashMap<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @RequiresApi(api = 19)
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onCreate(@Nullable Bundle savedInstanceState) {
 | 
				
			||||||
 | 
					        super.onCreate(savedInstanceState);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (Build.VERSION.SDK_INT < 19) {
 | 
				
			||||||
 | 
					            finish();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Intent intent = getIntent();
 | 
				
			||||||
 | 
					        if (intent == null || TextUtils.isEmpty(intent.getAction())
 | 
				
			||||||
 | 
					                || !UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(intent.getAction())) {
 | 
				
			||||||
 | 
					            Log.i(TAG, "ignoring irrelevant intent: " + intent);
 | 
				
			||||||
 | 
					            finish();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Log.i(TAG, "handling intent: " + intent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        final ContentResolver contentResolver = getContentResolver();
 | 
				
			||||||
 | 
					        BroadcastReceiver receiver = new BroadcastReceiver() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onReceive(Context context, Intent intent) {
 | 
				
			||||||
 | 
					                if (!UsbManager.ACTION_USB_DEVICE_DETACHED.equals(intent.getAction())) {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                NearbyViewBinder.updateUsbOtg(UsbDeviceAttachedActivity.this);
 | 
				
			||||||
 | 
					                unregisterReceiver(this);
 | 
				
			||||||
 | 
					                for (ContentObserver contentObserver : contentObservers.values()) {
 | 
				
			||||||
 | 
					                    contentResolver.unregisterContentObserver(contentObserver);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        registerReceiver(receiver, new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (final UriPermission uriPermission : contentResolver.getPersistedUriPermissions()) {
 | 
				
			||||||
 | 
					            Uri uri = uriPermission.getUri();
 | 
				
			||||||
 | 
					            final ContentObserver contentObserver = new ContentObserver(new Handler()) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                @Override
 | 
				
			||||||
 | 
					                public void onChange(boolean selfChange, Uri uri) {
 | 
				
			||||||
 | 
					                    NearbyViewBinder.updateUsbOtg(UsbDeviceAttachedActivity.this);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            contentResolver.registerContentObserver(uri, true, contentObserver);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        intent.setComponent(new ComponentName(this, MainActivity.class));
 | 
				
			||||||
 | 
					        intent.putExtra(MainActivity.EXTRA_VIEW_NEARBY, true);
 | 
				
			||||||
 | 
					        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
 | 
				
			||||||
 | 
					        startActivity(intent);
 | 
				
			||||||
 | 
					        finish();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void finish() {
 | 
				
			||||||
 | 
					        setResult(RESULT_OK);
 | 
				
			||||||
 | 
					        super.finish();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -2,13 +2,21 @@ package org.fdroid.fdroid.views.main;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import android.Manifest;
 | 
					import android.Manifest;
 | 
				
			||||||
import android.app.Activity;
 | 
					import android.app.Activity;
 | 
				
			||||||
 | 
					import android.content.Context;
 | 
				
			||||||
import android.content.Intent;
 | 
					import android.content.Intent;
 | 
				
			||||||
 | 
					import android.content.UriPermission;
 | 
				
			||||||
import android.content.pm.PackageManager;
 | 
					import android.content.pm.PackageManager;
 | 
				
			||||||
 | 
					import android.hardware.usb.UsbDevice;
 | 
				
			||||||
 | 
					import android.hardware.usb.UsbManager;
 | 
				
			||||||
 | 
					import android.net.Uri;
 | 
				
			||||||
import android.os.Build;
 | 
					import android.os.Build;
 | 
				
			||||||
import android.os.Environment;
 | 
					import android.os.Environment;
 | 
				
			||||||
 | 
					import android.os.storage.StorageManager;
 | 
				
			||||||
 | 
					import android.os.storage.StorageVolume;
 | 
				
			||||||
import android.support.annotation.RequiresApi;
 | 
					import android.support.annotation.RequiresApi;
 | 
				
			||||||
import android.support.v4.app.ActivityCompat;
 | 
					import android.support.v4.app.ActivityCompat;
 | 
				
			||||||
import android.support.v4.content.ContextCompat;
 | 
					import android.support.v4.content.ContextCompat;
 | 
				
			||||||
 | 
					import android.text.TextUtils;
 | 
				
			||||||
import android.util.Log;
 | 
					import android.util.Log;
 | 
				
			||||||
import android.view.View;
 | 
					import android.view.View;
 | 
				
			||||||
import android.widget.Button;
 | 
					import android.widget.Button;
 | 
				
			||||||
@ -17,11 +25,13 @@ import android.widget.ImageView;
 | 
				
			|||||||
import android.widget.TextView;
 | 
					import android.widget.TextView;
 | 
				
			||||||
import android.widget.Toast;
 | 
					import android.widget.Toast;
 | 
				
			||||||
import org.fdroid.fdroid.R;
 | 
					import org.fdroid.fdroid.R;
 | 
				
			||||||
 | 
					import org.fdroid.fdroid.Utils;
 | 
				
			||||||
import org.fdroid.fdroid.nearby.SDCardScannerService;
 | 
					import org.fdroid.fdroid.nearby.SDCardScannerService;
 | 
				
			||||||
import org.fdroid.fdroid.nearby.SwapService;
 | 
					import org.fdroid.fdroid.nearby.SwapService;
 | 
				
			||||||
import org.fdroid.fdroid.nearby.TreeUriScannerIntentService;
 | 
					import org.fdroid.fdroid.nearby.TreeUriScannerIntentService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.File;
 | 
					import java.io.File;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * A splash screen encouraging people to start the swap process. The swap
 | 
					 * A splash screen encouraging people to start the swap process. The swap
 | 
				
			||||||
@ -50,13 +60,14 @@ import java.io.File;
 | 
				
			|||||||
 * @see TreeUriScannerIntentService
 | 
					 * @see TreeUriScannerIntentService
 | 
				
			||||||
 * @see org.fdroid.fdroid.nearby.SDCardScannerService
 | 
					 * @see org.fdroid.fdroid.nearby.SDCardScannerService
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class NearbyViewBinder {
 | 
					public class NearbyViewBinder {
 | 
				
			||||||
    public static final String TAG = "NearbyViewBinder";
 | 
					    public static final String TAG = "NearbyViewBinder";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    static File externalStorage = null;
 | 
					    private static File externalStorage = null;
 | 
				
			||||||
 | 
					    private static View swapView;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    NearbyViewBinder(final Activity activity, FrameLayout parent) {
 | 
					    NearbyViewBinder(final Activity activity, FrameLayout parent) {
 | 
				
			||||||
        View swapView = activity.getLayoutInflater().inflate(R.layout.main_tab_swap, parent, true);
 | 
					        swapView = activity.getLayoutInflater().inflate(R.layout.main_tab_swap, parent, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        TextView subtext = swapView.findViewById(R.id.both_parties_need_fdroid_text);
 | 
					        TextView subtext = swapView.findViewById(R.id.both_parties_need_fdroid_text);
 | 
				
			||||||
        subtext.setText(activity.getString(R.string.nearby_splash__both_parties_need_fdroid,
 | 
					        subtext.setText(activity.getString(R.string.nearby_splash__both_parties_need_fdroid,
 | 
				
			||||||
@ -131,5 +142,62 @@ class NearbyViewBinder {
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        updateUsbOtg(activity);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static void updateUsbOtg(final Activity activity) {
 | 
				
			||||||
 | 
					        if (Build.VERSION.SDK_INT < 24) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (swapView == null) {
 | 
				
			||||||
 | 
					            Utils.debugLog(TAG, "swapView == null");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        TextView storageVolumeText = swapView.findViewById(R.id.storage_volume_text);
 | 
				
			||||||
 | 
					        Button requestStorageVolume = swapView.findViewById(R.id.request_storage_volume_button);
 | 
				
			||||||
 | 
					        storageVolumeText.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					        requestStorageVolume.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        final StorageManager storageManager = (StorageManager) activity.getSystemService(Context.STORAGE_SERVICE);
 | 
				
			||||||
 | 
					        for (final 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;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                storageVolumeText.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                String text = storageVolume.getDescription(activity);
 | 
				
			||||||
 | 
					                if (!TextUtils.isEmpty(text)) {
 | 
				
			||||||
 | 
					                    requestStorageVolume.setText(text);
 | 
				
			||||||
 | 
					                    UsbDevice usb = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
 | 
				
			||||||
 | 
					                    if (usb != null) {
 | 
				
			||||||
 | 
					                        text = String.format("%s (%s %s)", text, usb.getManufacturerName(), usb.getProductName());
 | 
				
			||||||
 | 
					                        Toast.makeText(activity, text, Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                requestStorageVolume.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					                requestStorageVolume.setOnClickListener(new View.OnClickListener() {
 | 
				
			||||||
 | 
					                    @Override
 | 
				
			||||||
 | 
					                    @RequiresApi(api = 24)
 | 
				
			||||||
 | 
					                    public void onClick(View v) {
 | 
				
			||||||
 | 
					                        List<UriPermission> list = activity.getContentResolver().getPersistedUriPermissions();
 | 
				
			||||||
 | 
					                        if (list != null) for (UriPermission uriPermission : list) {
 | 
				
			||||||
 | 
					                            Uri uri = uriPermission.getUri();
 | 
				
			||||||
 | 
					                            if (uri.getPath().equals(String.format("/tree/%s:", storageVolume.getUuid()))) {
 | 
				
			||||||
 | 
					                                intent.setData(uri);
 | 
				
			||||||
 | 
					                                TreeUriScannerIntentService.onActivityResult(activity, intent);
 | 
				
			||||||
 | 
					                                return;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        activity.startActivityForResult(intent, MainActivity.REQUEST_STORAGE_ACCESS);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -72,11 +72,12 @@
 | 
				
			|||||||
            android:layout_height="wrap_content"
 | 
					            android:layout_height="wrap_content"
 | 
				
			||||||
            android:layout_weight="1"
 | 
					            android:layout_weight="1"
 | 
				
			||||||
            android:text="@string/nearby_splash__read_external_storage"
 | 
					            android:text="@string/nearby_splash__read_external_storage"
 | 
				
			||||||
            android:textSize="20sp"
 | 
					            android:textSize="17sp"
 | 
				
			||||||
            android:textColor="?attr/lightGrayTextColor"
 | 
					            android:textColor="?attr/lightGrayTextColor"
 | 
				
			||||||
            android:textAlignment="center"
 | 
					            android:textAlignment="center"
 | 
				
			||||||
            android:gravity="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_marginStart="24dp" app:layout_constraintStart_toStartOf="parent"
 | 
				
			||||||
            android:layout_marginLeft="24dp" android:layout_marginTop="48dp"
 | 
					            android:layout_marginLeft="24dp" android:layout_marginTop="48dp"
 | 
				
			||||||
            app:layout_constraintTop_toBottomOf="@+id/both_parties_need_fdroid_text"
 | 
					            app:layout_constraintTop_toBottomOf="@+id/both_parties_need_fdroid_text"
 | 
				
			||||||
@ -96,4 +97,35 @@
 | 
				
			|||||||
            app:layout_constraintTop_toBottomOf="@+id/read_external_storage_text"
 | 
					            app:layout_constraintTop_toBottomOf="@+id/read_external_storage_text"
 | 
				
			||||||
            android:visibility="gone"
 | 
					            android:visibility="gone"
 | 
				
			||||||
            tools:visibility="visible"/>
 | 
					            tools:visibility="visible"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <TextView
 | 
				
			||||||
 | 
					            android:id="@+id/storage_volume_text"
 | 
				
			||||||
 | 
					            android:layout_width="0dp"
 | 
				
			||||||
 | 
					            android:layout_height="wrap_content"
 | 
				
			||||||
 | 
					            android:layout_weight="1"
 | 
				
			||||||
 | 
					            android:text="@string/nearby_splash__document_tree"
 | 
				
			||||||
 | 
					            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_marginStart="24dp" app:layout_constraintStart_toStartOf="parent"
 | 
				
			||||||
 | 
					            android:layout_marginLeft="24dp" android:layout_marginTop="48dp"
 | 
				
			||||||
 | 
					            app:layout_constraintTop_toBottomOf="@+id/request_read_external_storage_button"
 | 
				
			||||||
 | 
					            android:visibility="gone"/>
 | 
				
			||||||
 | 
					    <Button
 | 
				
			||||||
 | 
					            android:id="@+id/request_storage_volume_button"
 | 
				
			||||||
 | 
					            android:layout_width="wrap_content"
 | 
				
			||||||
 | 
					            android:layout_height="wrap_content"
 | 
				
			||||||
 | 
					            android:maxEms="16"
 | 
				
			||||||
 | 
					            android:text="@string/nearby_splash__request_permission"
 | 
				
			||||||
 | 
					            style="@style/DetailsSecondaryButtonStyle"
 | 
				
			||||||
 | 
					            app:layout_constraintEnd_toEndOf="parent"
 | 
				
			||||||
 | 
					            android:layout_marginEnd="8dp" android:layout_marginRight="8dp"
 | 
				
			||||||
 | 
					            app:layout_constraintStart_toStartOf="parent" android:layout_marginLeft="8dp"
 | 
				
			||||||
 | 
					            android:layout_marginStart="8dp" android:layout_marginTop="24dp"
 | 
				
			||||||
 | 
					            app:layout_constraintTop_toBottomOf="@+id/storage_volume_text"
 | 
				
			||||||
 | 
					            android:visibility="gone"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</android.support.constraint.ConstraintLayout>
 | 
					</android.support.constraint.ConstraintLayout>
 | 
				
			||||||
							
								
								
									
										26
									
								
								app/src/full/res/xml/device_filter.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								app/src/full/res/xml/device_filter.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<!--
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * (C) Copyright 2014 mjahnen <jahnen@in.tum.de>
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					 * you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					 * You may obtain a copy of the License at
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					 * See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					 * limitations under the License.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					-->
 | 
				
			||||||
 | 
					<resources>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <usb-device
 | 
				
			||||||
 | 
					        class="8"
 | 
				
			||||||
 | 
					        protocol="80"
 | 
				
			||||||
 | 
					        subclass="6" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
@ -91,7 +91,7 @@ public class TreeUriDownloader extends Downloader {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    protected long totalDownloadSize() {
 | 
					    protected long totalDownloadSize() {
 | 
				
			||||||
        return documentFile.length();
 | 
					        return documentFile.length(); // TODO how should this actually be implemented?
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
 | 
				
			|||||||
@ -58,11 +58,12 @@ import org.fdroid.fdroid.Utils;
 | 
				
			|||||||
import org.fdroid.fdroid.data.NewRepoConfig;
 | 
					import org.fdroid.fdroid.data.NewRepoConfig;
 | 
				
			||||||
import org.fdroid.fdroid.nearby.SDCardScannerService;
 | 
					import org.fdroid.fdroid.nearby.SDCardScannerService;
 | 
				
			||||||
import org.fdroid.fdroid.nearby.SwapService;
 | 
					import org.fdroid.fdroid.nearby.SwapService;
 | 
				
			||||||
 | 
					import org.fdroid.fdroid.nearby.SwapWorkflowActivity;
 | 
				
			||||||
 | 
					import org.fdroid.fdroid.nearby.TreeUriScannerIntentService;
 | 
				
			||||||
import org.fdroid.fdroid.nearby.WifiStateChangeService;
 | 
					import org.fdroid.fdroid.nearby.WifiStateChangeService;
 | 
				
			||||||
import org.fdroid.fdroid.views.AppDetailsActivity;
 | 
					import org.fdroid.fdroid.views.AppDetailsActivity;
 | 
				
			||||||
import org.fdroid.fdroid.views.ManageReposActivity;
 | 
					import org.fdroid.fdroid.views.ManageReposActivity;
 | 
				
			||||||
import org.fdroid.fdroid.views.apps.AppListActivity;
 | 
					import org.fdroid.fdroid.views.apps.AppListActivity;
 | 
				
			||||||
import org.fdroid.fdroid.nearby.SwapWorkflowActivity;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.lang.reflect.Field;
 | 
					import java.lang.reflect.Field;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -85,10 +86,12 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
 | 
				
			|||||||
    private static final String TAG = "MainActivity";
 | 
					    private static final String TAG = "MainActivity";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static final String EXTRA_VIEW_UPDATES = "org.fdroid.fdroid.views.main.MainActivity.VIEW_UPDATES";
 | 
					    public static final String EXTRA_VIEW_UPDATES = "org.fdroid.fdroid.views.main.MainActivity.VIEW_UPDATES";
 | 
				
			||||||
 | 
					    public static final String EXTRA_VIEW_NEARBY = "org.fdroid.fdroid.views.main.MainActivity.VIEW_NEARBY";
 | 
				
			||||||
    public static final String EXTRA_VIEW_SETTINGS = "org.fdroid.fdroid.views.main.MainActivity.VIEW_SETTINGS";
 | 
					    public static final String EXTRA_VIEW_SETTINGS = "org.fdroid.fdroid.views.main.MainActivity.VIEW_SETTINGS";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    static final int REQUEST_LOCATION_PERMISSIONS = 0xEF0F;
 | 
					    static final int REQUEST_LOCATION_PERMISSIONS = 0xEF0F;
 | 
				
			||||||
    static final int REQUEST_STORAGE_PERMISSIONS = 0xB004;
 | 
					    static final int REQUEST_STORAGE_PERMISSIONS = 0xB004;
 | 
				
			||||||
 | 
					    public static final int REQUEST_STORAGE_ACCESS = 0x40E5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static final String ADD_REPO_INTENT_HANDLED = "addRepoIntentHandled";
 | 
					    private static final String ADD_REPO_INTENT_HANDLED = "addRepoIntentHandled";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -133,6 +136,15 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
 | 
				
			|||||||
            bottomNavigation
 | 
					            bottomNavigation
 | 
				
			||||||
                    .addItem(new BottomNavigationItem(R.drawable.ic_categories, R.string.main_menu__categories))
 | 
					                    .addItem(new BottomNavigationItem(R.drawable.ic_categories, R.string.main_menu__categories))
 | 
				
			||||||
                    .addItem(new BottomNavigationItem(R.drawable.ic_nearby, R.string.main_menu__swap_nearby));
 | 
					                    .addItem(new BottomNavigationItem(R.drawable.ic_nearby, R.string.main_menu__swap_nearby));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            bottomNavigation.setOnClickListener(new View.OnClickListener() {
 | 
				
			||||||
 | 
					                @Override
 | 
				
			||||||
 | 
					                public void onClick(View v) {
 | 
				
			||||||
 | 
					                    if (bottomNavigation.getCurrentSelectedPosition() == 2) {
 | 
				
			||||||
 | 
					                        NearbyViewBinder.updateUsbOtg(MainActivity.this);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        bottomNavigation.setTabSelectedListener(this)
 | 
					        bottomNavigation.setTabSelectedListener(this)
 | 
				
			||||||
                .setBarBackgroundColor(getBottomNavigationBackgroundColorResId())
 | 
					                .setBarBackgroundColor(getBottomNavigationBackgroundColorResId())
 | 
				
			||||||
@ -217,6 +229,11 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
 | 
				
			|||||||
            pager.scrollToPosition(adapter.adapterPositionFromItemId(R.id.updates));
 | 
					            pager.scrollToPosition(adapter.adapterPositionFromItemId(R.id.updates));
 | 
				
			||||||
            selectedMenuId = R.id.updates;
 | 
					            selectedMenuId = R.id.updates;
 | 
				
			||||||
            setSelectedMenuInNav();
 | 
					            setSelectedMenuInNav();
 | 
				
			||||||
 | 
					        } else if (getIntent().hasExtra(EXTRA_VIEW_NEARBY)) {
 | 
				
			||||||
 | 
					            getIntent().removeExtra(EXTRA_VIEW_NEARBY);
 | 
				
			||||||
 | 
					            pager.scrollToPosition(adapter.adapterPositionFromItemId(R.id.nearby));
 | 
				
			||||||
 | 
					            selectedMenuId = R.id.nearby;
 | 
				
			||||||
 | 
					            setSelectedMenuInNav();
 | 
				
			||||||
        } else if (getIntent().hasExtra(EXTRA_VIEW_SETTINGS)) {
 | 
					        } else if (getIntent().hasExtra(EXTRA_VIEW_SETTINGS)) {
 | 
				
			||||||
            getIntent().removeExtra(EXTRA_VIEW_SETTINGS);
 | 
					            getIntent().removeExtra(EXTRA_VIEW_SETTINGS);
 | 
				
			||||||
            pager.scrollToPosition(adapter.adapterPositionFromItemId(R.id.settings));
 | 
					            pager.scrollToPosition(adapter.adapterPositionFromItemId(R.id.settings));
 | 
				
			||||||
@ -246,6 +263,14 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
 | 
				
			|||||||
        checkForAddRepoIntent(intent);
 | 
					        checkForAddRepoIntent(intent);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
 | 
				
			||||||
 | 
					        super.onActivityResult(requestCode, resultCode, data);
 | 
				
			||||||
 | 
					        if (requestCode == REQUEST_STORAGE_ACCESS) {
 | 
				
			||||||
 | 
					            TreeUriScannerIntentService.onActivityResult(this, data);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { // NOCHECKSTYLE LineLength
 | 
					    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { // NOCHECKSTYLE LineLength
 | 
				
			||||||
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
 | 
					        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
 | 
				
			||||||
 | 
				
			|||||||
@ -82,7 +82,7 @@ class MainViewAdapter extends RecyclerView.Adapter<MainViewController> {
 | 
				
			|||||||
    @NonNull
 | 
					    @NonNull
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public MainViewController onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
 | 
					    public MainViewController onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
 | 
				
			||||||
        MainViewController holder = createEmptyView();
 | 
					        MainViewController holder = createEmptyView(activity);
 | 
				
			||||||
        switch (viewType) {
 | 
					        switch (viewType) {
 | 
				
			||||||
            case R.id.whats_new:
 | 
					            case R.id.whats_new:
 | 
				
			||||||
                holder.bindWhatsNewView();
 | 
					                holder.bindWhatsNewView();
 | 
				
			||||||
@ -106,7 +106,7 @@ class MainViewAdapter extends RecyclerView.Adapter<MainViewController> {
 | 
				
			|||||||
        return holder;
 | 
					        return holder;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private MainViewController createEmptyView() {
 | 
					    static MainViewController createEmptyView(AppCompatActivity activity) {
 | 
				
			||||||
        FrameLayout frame = new FrameLayout(activity);
 | 
					        FrameLayout frame = new FrameLayout(activity);
 | 
				
			||||||
        frame.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
 | 
					        frame.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
 | 
				
			||||||
                ViewGroup.LayoutParams.MATCH_PARENT));
 | 
					                ViewGroup.LayoutParams.MATCH_PARENT));
 | 
				
			||||||
 | 
				
			|||||||
@ -458,7 +458,8 @@ This often occurs with apps installed via Google Play or other sources, if they
 | 
				
			|||||||
    <!-- The word "nearby" should be the same as used for Google/Android Nearby. This is a button label, it should ideally be 10-20 characters. -->
 | 
					    <!-- The word "nearby" should be the same as used for Google/Android Nearby. This is a button label, it should ideally be 10-20 characters. -->
 | 
				
			||||||
    <string name="nearby_splash__find_people_button">Find people nearby</string>
 | 
					    <string name="nearby_splash__find_people_button">Find people nearby</string>
 | 
				
			||||||
    <string name="nearby_splash__both_parties_need_fdroid">Both parties need %1$s to use nearby.</string>
 | 
					    <string name="nearby_splash__both_parties_need_fdroid">Both parties need %1$s to use nearby.</string>
 | 
				
			||||||
    <string name="nearby_splash__read_external_storage">SD Cards can be used to swap!</string>
 | 
					    <string name="nearby_splash__read_external_storage">Search SD Card for repos and mirrors.</string>
 | 
				
			||||||
 | 
					    <string name="nearby_splash__document_tree">Search USB OTG for repos and mirrors.</string>
 | 
				
			||||||
    <!-- This is a button label for a small button, the text needs to be under 10 characters, the shorter the better. Also, a literal translation probably will miss the point. "Try it" is more a saying than two words. Google has been pushing these short buttons, like "Got it" and "Try now" so this button is trying to match those buttons in Android. -->
 | 
					    <!-- This is a button label for a small button, the text needs to be under 10 characters, the shorter the better. Also, a literal translation probably will miss the point. "Try it" is more a saying than two words. Google has been pushing these short buttons, like "Got it" and "Try now" so this button is trying to match those buttons in Android. -->
 | 
				
			||||||
    <string name="nearby_splash__request_permission">Try it</string>
 | 
					    <string name="nearby_splash__request_permission">Try it</string>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user