Merge branch 'use-repos-from-usb-and-sdcard' into 'master'
use repos from USB-OTG Drives and SDCards Closes #1377 and #656 See merge request fdroid/fdroidclient!769
This commit is contained in:
commit
8c5263c5c5
@ -1,5 +1,6 @@
|
|||||||
package org.fdroid.fdroid;
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
import android.app.Instrumentation;
|
import android.app.Instrumentation;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.support.test.InstrumentationRegistry;
|
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.espresso.ViewInteraction;
|
||||||
import android.support.test.filters.LargeTest;
|
import android.support.test.filters.LargeTest;
|
||||||
import android.support.test.rule.ActivityTestRule;
|
import android.support.test.rule.ActivityTestRule;
|
||||||
|
import android.support.test.rule.GrantPermissionRule;
|
||||||
import android.support.test.runner.AndroidJUnit4;
|
import android.support.test.runner.AndroidJUnit4;
|
||||||
import android.support.test.uiautomator.UiDevice;
|
import android.support.test.uiautomator.UiDevice;
|
||||||
import android.support.test.uiautomator.UiObject;
|
import android.support.test.uiautomator.UiObject;
|
||||||
@ -120,6 +122,14 @@ public class MainActivityEspressoTest {
|
|||||||
public ActivityTestRule<MainActivity> activityTestRule =
|
public ActivityTestRule<MainActivity> activityTestRule =
|
||||||
new ActivityTestRule<>(MainActivity.class);
|
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
|
@Test
|
||||||
public void bottomNavFlavorCheck() {
|
public void bottomNavFlavorCheck() {
|
||||||
onView(withText(R.string.updates)).check(matches(isDisplayed()));
|
onView(withText(R.string.updates)).check(matches(isDisplayed()));
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.localrepo;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dummy version for basic app flavor.
|
||||||
|
*/
|
||||||
|
public class SDCardScannerService {
|
||||||
|
public static void scan(Context context) {
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
@ -38,11 +38,14 @@
|
|||||||
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<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.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
|
|
||||||
|
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
@ -77,6 +80,12 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".localrepo.CacheSwapAppsService"
|
android:name=".localrepo.CacheSwapAppsService"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
<service
|
||||||
|
android:name=".localrepo.TreeUriScannerIntentService"
|
||||||
|
android:exported="false"/>
|
||||||
|
<service
|
||||||
|
android:name=".localrepo.SDCardScannerService"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".views.panic.PanicPreferencesActivity"
|
android:name=".views.panic.PanicPreferencesActivity"
|
||||||
|
@ -0,0 +1,176 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.localrepo;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.app.IntentService;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.os.Process;
|
||||||
|
import android.support.v4.content.ContextCompat;
|
||||||
|
import android.util.Log;
|
||||||
|
import org.fdroid.fdroid.IndexUpdater;
|
||||||
|
import org.fdroid.fdroid.IndexV1Updater;
|
||||||
|
import org.fdroid.fdroid.Preferences;
|
||||||
|
import org.fdroid.fdroid.Utils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link IntentService} subclass for scanning removable "external storage"
|
||||||
|
* for F-Droid package repos, e.g. SD Cards. This is intented to support
|
||||||
|
* sharable package repos, so it ignores non-removable storage, like the fake
|
||||||
|
* emulated sdcard from devices with only built-in storage. This method will
|
||||||
|
* only ever allow for reading repos, never writing. It also will not work
|
||||||
|
* for removeable storage devices plugged in via USB, since do not show up as
|
||||||
|
* "External Storage"
|
||||||
|
* <p>
|
||||||
|
* Scanning the removable storage requires that the user allowed it. This
|
||||||
|
* requires both the {@link Preferences#isScanRemovableStorageEnabled()}
|
||||||
|
* and the {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}
|
||||||
|
* permission to be enabled.
|
||||||
|
*
|
||||||
|
* @see TreeUriScannerIntentService TreeUri method for writing repos to be shared
|
||||||
|
* @see <a href="https://stackoverflow.com/a/40201333">Universal way to write to external SD card on Android</a>
|
||||||
|
* @see <a href="https://commonsware.com/blog/2017/11/14/storage-situation-external-storage.html"> The Storage Situation: External Storage </a>
|
||||||
|
*/
|
||||||
|
public class SDCardScannerService extends IntentService {
|
||||||
|
public static final String TAG = "SDCardScannerService";
|
||||||
|
|
||||||
|
private static final String ACTION_SCAN = "org.fdroid.fdroid.localrepo.SCAN";
|
||||||
|
|
||||||
|
private static final List<String> SKIP_DIRS = Arrays.asList(".android_secure", "LOST.DIR");
|
||||||
|
|
||||||
|
public SDCardScannerService() {
|
||||||
|
super("SDCardScannerService");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void scan(Context context) {
|
||||||
|
if (Preferences.get().isScanRemovableStorageEnabled()) {
|
||||||
|
Intent intent = new Intent(context, SDCardScannerService.class);
|
||||||
|
intent.setAction(ACTION_SCAN);
|
||||||
|
context.startService(intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHandleIntent(Intent intent) {
|
||||||
|
if (intent == null || !ACTION_SCAN.equals(intent.getAction())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
|
||||||
|
|
||||||
|
HashSet<File> files = new HashSet<>();
|
||||||
|
if (Build.VERSION.SDK_INT < 21) {
|
||||||
|
if (Environment.isExternalStorageRemovable()) {
|
||||||
|
File sdcard = Environment.getExternalStorageDirectory();
|
||||||
|
String state = Environment.getExternalStorageState();
|
||||||
|
Collections.addAll(files, checkExternalStorage(sdcard, state));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (File f : getExternalFilesDirs(null)) {
|
||||||
|
Log.i(TAG, "getExternalFilesDirs " + f);
|
||||||
|
if (f == null || !f.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Log.i(TAG, "getExternalFilesDirs " + f);
|
||||||
|
if (Environment.isExternalStorageRemovable(f)) {
|
||||||
|
String state = Environment.getExternalStorageState(f);
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
== PackageManager.PERMISSION_GRANTED) {
|
||||||
|
// remove Android/data/org.fdroid.fdroid/files to get root
|
||||||
|
File sdcard = f.getParentFile().getParentFile().getParentFile().getParentFile();
|
||||||
|
Collections.addAll(files, checkExternalStorage(sdcard, state));
|
||||||
|
} else {
|
||||||
|
Collections.addAll(files, checkExternalStorage(f, state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "sdcard files " + files.toString());
|
||||||
|
ArrayList<String> filesList = new ArrayList<>();
|
||||||
|
for (File dir : files) {
|
||||||
|
if (!dir.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
searchDirectory(dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File[] checkExternalStorage(File sdcard, String state) {
|
||||||
|
File[] files = null;
|
||||||
|
if (sdcard != null &&
|
||||||
|
(Environment.MEDIA_MOUNTED_READ_ONLY.equals(state) || Environment.MEDIA_MOUNTED.equals(state))) {
|
||||||
|
files = sdcard.listFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files == null) {
|
||||||
|
Utils.debugLog(TAG, "checkExternalStorage returned blank, F-Droid probaby doesn't have Storage perm!");
|
||||||
|
return new File[0];
|
||||||
|
} else {
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void searchDirectory(File dir) {
|
||||||
|
if (SKIP_DIRS.contains(dir.getName())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
File[] files = dir.listFiles();
|
||||||
|
if (files == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (File file : files) {
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
searchDirectory(file);
|
||||||
|
} else {
|
||||||
|
if (IndexV1Updater.SIGNED_FILE_NAME.equals(file.getName())) {
|
||||||
|
registerRepo(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerRepo(File file) {
|
||||||
|
InputStream inputStream = null;
|
||||||
|
try {
|
||||||
|
inputStream = new FileInputStream(file);
|
||||||
|
TreeUriScannerIntentService.registerRepo(this, inputStream, Uri.fromFile(file.getParentFile()));
|
||||||
|
} catch (IOException | IndexUpdater.SigningException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
Utils.closeQuietly(inputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,165 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.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.AddRepoIntentService;
|
||||||
|
import org.fdroid.fdroid.IndexUpdater;
|
||||||
|
import org.fdroid.fdroid.IndexV1Updater;
|
||||||
|
import org.fdroid.fdroid.Preferences;
|
||||||
|
import org.fdroid.fdroid.Utils;
|
||||||
|
import org.fdroid.fdroid.data.Repo;
|
||||||
|
import org.fdroid.fdroid.data.RepoProvider;
|
||||||
|
|
||||||
|
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}.
|
||||||
|
* <p>
|
||||||
|
* 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 <a href="https://commonsware.com/blog/2017/11/15/storage-situation-removable-storage.html"> The Storage Situation: Removable Storage </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>
|
||||||
|
*/
|
||||||
|
@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) {
|
||||||
|
if (Preferences.get().isScanRemovableStorageEnabled()) {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddRepoIntentService.addRepo(context, repoUri, fingerprint);
|
||||||
|
// TODO rework IndexUpdater.getSigningCertFromJar to work for here
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,12 @@
|
|||||||
package org.fdroid.fdroid.views.main;
|
package org.fdroid.fdroid.views.main;
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.TextView;
|
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
import org.fdroid.fdroid.views.PreferencesFragment;
|
import org.fdroid.fdroid.views.PreferencesFragment;
|
||||||
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
|
|
||||||
import org.fdroid.fdroid.views.updates.UpdatesViewBinder;
|
import org.fdroid.fdroid.views.updates.UpdatesViewBinder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,29 +60,8 @@ class MainViewController extends RecyclerView.ViewHolder {
|
|||||||
new CategoriesViewBinder(activity, frame);
|
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() {
|
public void bindSwapView() {
|
||||||
View swapView = activity.getLayoutInflater().inflate(R.layout.main_tab_swap, frame, true);
|
new NearbyViewBinder(activity, frame);
|
||||||
|
|
||||||
// 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));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,134 @@
|
|||||||
|
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.SDCardScannerService;
|
||||||
|
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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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}.
|
||||||
|
* <p>
|
||||||
|
* 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
|
||||||
|
* @see org.fdroid.fdroid.localrepo.SDCardScannerService
|
||||||
|
*/
|
||||||
|
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) {
|
||||||
|
final String coarseLocation = Manifest.permission.ACCESS_COARSE_LOCATION;
|
||||||
|
if (Build.VERSION.SDK_INT >= 23
|
||||||
|
&& PackageManager.PERMISSION_GRANTED
|
||||||
|
!= ContextCompat.checkSelfPermission(activity, coarseLocation)) {
|
||||||
|
ActivityCompat.requestPermissions(activity, new String[]{coarseLocation},
|
||||||
|
MainActivity.REQUEST_LOCATION_PERMISSIONS);
|
||||||
|
} else {
|
||||||
|
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 {
|
||||||
|
SDCardScannerService.scan(activity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -54,6 +54,37 @@
|
|||||||
android:layout_marginStart="48dp"
|
android:layout_marginStart="48dp"
|
||||||
android:layout_marginLeft="48dp" />
|
android:layout_marginLeft="48dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/readExternalStorage"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/text2"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
android:layout_marginTop="48dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:layout_marginRight="24dp"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginLeft="24dp">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:text="@string/nearby_splash__read_external_storage"
|
||||||
|
android:gravity="fill"
|
||||||
|
android:paddingRight="5dp"
|
||||||
|
android:paddingEnd="5dp"
|
||||||
|
android:textSize="17sp"
|
||||||
|
android:textColor="?attr/lightGrayTextColor"/>
|
||||||
|
<Button
|
||||||
|
android:id="@+id/requestReadExternalStorage"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:text="@string/nearby_splash__request_permission"
|
||||||
|
style="@style/SwapTheme.Wizard.OptionButton"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image"
|
android:id="@+id/image"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -271,6 +271,9 @@
|
|||||||
android:name=".data.InstalledAppProviderService"
|
android:name=".data.InstalledAppProviderService"
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
<service
|
||||||
|
android:name=".AddRepoIntentService"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
|
|
||||||
<!-- Warning: Please add all new services to HidingManager -->
|
<!-- Warning: Please add all new services to HidingManager -->
|
||||||
|
147
app/src/main/java/org/fdroid/fdroid/AddRepoIntentService.java
Normal file
147
app/src/main/java/org/fdroid/fdroid/AddRepoIntentService.java
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
|
import android.app.IntentService;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
import org.fdroid.fdroid.data.Repo;
|
||||||
|
import org.fdroid.fdroid.data.RepoProvider;
|
||||||
|
import org.fdroid.fdroid.views.ManageReposActivity;
|
||||||
|
import org.fdroid.fdroid.views.main.MainActivity;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles requests to add new repos via URLs. This is an {@code IntentService}
|
||||||
|
* so that requests are queued, which is necessary when either
|
||||||
|
* {@link org.fdroid.fdroid.localrepo.TreeUriScannerIntentService} or
|
||||||
|
* {@link org.fdroid.fdroid.localrepo.SDCardScannerService} finds multiple
|
||||||
|
* repos on a disk. This should hopefully also serve as the beginnings of
|
||||||
|
* a new architecture for handling these requests. This does all the
|
||||||
|
* processing first, up front, then only launches UI as needed.
|
||||||
|
* {@link org.fdroid.fdroid.views.ManageReposActivity} currently does the
|
||||||
|
* opposite.
|
||||||
|
* <p>
|
||||||
|
* This only really properly queues {@link Intent}s that get filtered out. The
|
||||||
|
* {@code Intent}s that go on to {@code ManageReposActivity} will not wait
|
||||||
|
* until for that {@code Activity} to be ready to handle the next. So when
|
||||||
|
* multiple mirrors are discovered at once, only one in that session will
|
||||||
|
* likely be added.
|
||||||
|
*/
|
||||||
|
public class AddRepoIntentService extends IntentService {
|
||||||
|
public static final String TAG = "AddRepoIntentService";
|
||||||
|
|
||||||
|
private static final String ACTION_ADD_REPO = "org.fdroid.fdroid.action.ADD_REPO";
|
||||||
|
|
||||||
|
public AddRepoIntentService() {
|
||||||
|
super("AddRepoIntentService");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addRepo(Context context, @NonNull Uri repoUri, @Nullable String fingerprint) {
|
||||||
|
Intent intent = new Intent(context, AddRepoIntentService.class);
|
||||||
|
intent.setAction(ACTION_ADD_REPO);
|
||||||
|
if (TextUtils.isEmpty(fingerprint)) {
|
||||||
|
intent.setData(repoUri);
|
||||||
|
} else {
|
||||||
|
intent.setData(repoUri.buildUpon()
|
||||||
|
.appendQueryParameter("fingerprint", fingerprint)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
context.startService(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHandleIntent(Intent intent) {
|
||||||
|
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
|
||||||
|
if (intent == null || intent.getData() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri uri = intent.getData();
|
||||||
|
String urlString;
|
||||||
|
try {
|
||||||
|
urlString = normalizeUrl(uri);
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
Log.i(TAG, e.getLocalizedMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String fingerprint = uri.getQueryParameter("fingerprint");
|
||||||
|
for (Repo repo : RepoProvider.Helper.all(this)) {
|
||||||
|
if (repo.inuse && TextUtils.equals(fingerprint, repo.fingerprint)) {
|
||||||
|
if (TextUtils.equals(urlString, repo.address)) {
|
||||||
|
Utils.debugLog(TAG, urlString + " already added as a repo");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
for (String mirrorUrl : repo.getMirrorList()) {
|
||||||
|
if (urlString.startsWith(mirrorUrl)) {
|
||||||
|
Utils.debugLog(TAG, urlString + " already added as a mirror");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
intent.putExtra(ManageReposActivity.EXTRA_FINISH_AFTER_ADDING_REPO, false);
|
||||||
|
intent.setComponent(new ComponentName(this, MainActivity.class));
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some basic sanitization of URLs, so that two URLs which have the same semantic meaning
|
||||||
|
* are represented by the exact same string by F-Droid. This will help to make sure that,
|
||||||
|
* e.g. "http://10.0.1.50" and "http://10.0.1.50/" are not two different repositories.
|
||||||
|
* <p>
|
||||||
|
* Currently it normalizes the path so that "/./" are removed and "test/../" is collapsed.
|
||||||
|
* This is done using {@link URI#normalize()}. It also removes multiple consecutive forward
|
||||||
|
* slashes in the path and replaces them with one. Finally, it removes trailing slashes.
|
||||||
|
* <p>
|
||||||
|
* {@code content://} URLs used for repos stored on removable storage get messed up by
|
||||||
|
* {@link URI}.
|
||||||
|
*/
|
||||||
|
public static String normalizeUrl(String urlString) throws URISyntaxException {
|
||||||
|
if (TextUtils.isEmpty(urlString)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalizeUrl(Uri.parse(urlString));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String normalizeUrl(Uri uri) throws URISyntaxException {
|
||||||
|
if (!uri.isAbsolute()) {
|
||||||
|
throw new URISyntaxException(uri.toString(), "Must provide an absolute URI for repositories");
|
||||||
|
}
|
||||||
|
if (!uri.isHierarchical()) {
|
||||||
|
throw new URISyntaxException(uri.toString(), "Must provide an hierarchical URI for repositories");
|
||||||
|
}
|
||||||
|
if ("content".equals(uri.getScheme())) {
|
||||||
|
return uri.toString();
|
||||||
|
}
|
||||||
|
String path = uri.getPath();
|
||||||
|
if (path != null) {
|
||||||
|
path = path.replaceAll("//*/", "/"); // Collapse multiple forward slashes into 1.
|
||||||
|
if (path.length() > 0 && path.charAt(path.length() - 1) == '/') {
|
||||||
|
path = path.substring(0, path.length() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String scheme = uri.getScheme();
|
||||||
|
String host = uri.getHost();
|
||||||
|
if (TextUtils.isEmpty(scheme) || TextUtils.isEmpty(host)) {
|
||||||
|
return uri.toString();
|
||||||
|
}
|
||||||
|
return new URI(scheme.toLowerCase(Locale.ENGLISH),
|
||||||
|
uri.getUserInfo(),
|
||||||
|
host.toLowerCase(Locale.ENGLISH),
|
||||||
|
uri.getPort(),
|
||||||
|
path,
|
||||||
|
uri.getQuery(),
|
||||||
|
uri.getFragment()).normalize().toString();
|
||||||
|
}
|
||||||
|
}
|
@ -69,6 +69,7 @@ import org.fdroid.fdroid.data.Repo;
|
|||||||
import org.fdroid.fdroid.data.RepoProvider;
|
import org.fdroid.fdroid.data.RepoProvider;
|
||||||
import org.fdroid.fdroid.installer.ApkFileProvider;
|
import org.fdroid.fdroid.installer.ApkFileProvider;
|
||||||
import org.fdroid.fdroid.installer.InstallHistoryService;
|
import org.fdroid.fdroid.installer.InstallHistoryService;
|
||||||
|
import org.fdroid.fdroid.localrepo.SDCardScannerService;
|
||||||
import org.fdroid.fdroid.net.ConnectivityMonitorService;
|
import org.fdroid.fdroid.net.ConnectivityMonitorService;
|
||||||
import org.fdroid.fdroid.net.HttpDownloader;
|
import org.fdroid.fdroid.net.HttpDownloader;
|
||||||
import org.fdroid.fdroid.net.ImageLoaderForUIL;
|
import org.fdroid.fdroid.net.ImageLoaderForUIL;
|
||||||
@ -502,6 +503,8 @@ public class FDroidApp extends Application {
|
|||||||
} else {
|
} else {
|
||||||
atStartTime.edit().remove(queryStringKey).apply();
|
atStartTime.edit().remove(queryStringKey).apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDCardScannerService.scan(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,6 +43,7 @@ import org.fdroid.fdroid.installer.InstallManagerService;
|
|||||||
import org.fdroid.fdroid.installer.InstallerService;
|
import org.fdroid.fdroid.installer.InstallerService;
|
||||||
import org.fdroid.fdroid.net.Downloader;
|
import org.fdroid.fdroid.net.Downloader;
|
||||||
import org.fdroid.fdroid.net.DownloaderFactory;
|
import org.fdroid.fdroid.net.DownloaderFactory;
|
||||||
|
import org.fdroid.fdroid.net.TreeUriDownloader;
|
||||||
import org.xml.sax.InputSource;
|
import org.xml.sax.InputSource;
|
||||||
import org.xml.sax.SAXException;
|
import org.xml.sax.SAXException;
|
||||||
import org.xml.sax.XMLReader;
|
import org.xml.sax.XMLReader;
|
||||||
@ -114,7 +115,11 @@ public class IndexUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected String getIndexUrl(@NonNull Repo repo) {
|
protected String getIndexUrl(@NonNull Repo repo) {
|
||||||
return repo.address + "/index.jar";
|
if (repo.address.startsWith("content://")) {
|
||||||
|
return repo.address + TreeUriDownloader.ESCAPED_SLASH + SIGNED_FILE_NAME;
|
||||||
|
} else {
|
||||||
|
return repo.address + "/" + SIGNED_FILE_NAME;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasChanged() {
|
public boolean hasChanged() {
|
||||||
|
@ -62,8 +62,10 @@ import java.net.SocketTimeoutException;
|
|||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.jar.JarEntry;
|
import java.util.jar.JarEntry;
|
||||||
@ -87,7 +89,7 @@ import java.util.jar.JarFile;
|
|||||||
public class IndexV1Updater extends IndexUpdater {
|
public class IndexV1Updater extends IndexUpdater {
|
||||||
public static final String TAG = "IndexV1Updater";
|
public static final String TAG = "IndexV1Updater";
|
||||||
|
|
||||||
private static final String SIGNED_FILE_NAME = "index-v1.jar";
|
public static final String SIGNED_FILE_NAME = "index-v1.jar";
|
||||||
public static final String DATA_FILE_NAME = "index-v1.json";
|
public static final String DATA_FILE_NAME = "index-v1.json";
|
||||||
|
|
||||||
public IndexV1Updater(@NonNull Context context, @NonNull Repo repo) {
|
public IndexV1Updater(@NonNull Context context, @NonNull Repo repo) {
|
||||||
@ -95,9 +97,16 @@ public class IndexV1Updater extends IndexUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
/**
|
||||||
|
* Storage Access Framework URLs have a crazy encoded path within the URL path.
|
||||||
|
*/
|
||||||
protected String getIndexUrl(@NonNull Repo repo) {
|
protected String getIndexUrl(@NonNull Repo repo) {
|
||||||
|
if (repo.address.startsWith("content://")) {
|
||||||
|
return repo.address + "%2F" + SIGNED_FILE_NAME;
|
||||||
|
} else {
|
||||||
return Uri.parse(repo.address).buildUpon().appendPath(SIGNED_FILE_NAME).build().toString();
|
return Uri.parse(repo.address).buildUpon().appendPath(SIGNED_FILE_NAME).build().toString();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return whether this successfully found an index of this version
|
* @return whether this successfully found an index of this version
|
||||||
@ -284,7 +293,14 @@ public class IndexV1Updater extends IndexUpdater {
|
|||||||
repo.name = getStringRepoValue(repoMap, "name");
|
repo.name = getStringRepoValue(repoMap, "name");
|
||||||
repo.icon = getStringRepoValue(repoMap, "icon");
|
repo.icon = getStringRepoValue(repoMap, "icon");
|
||||||
repo.description = getStringRepoValue(repoMap, "description");
|
repo.description = getStringRepoValue(repoMap, "description");
|
||||||
repo.mirrors = getStringArrayRepoValue(repoMap, "mirrors");
|
|
||||||
|
// ensure the canonical URL is included in the "mirrors" list
|
||||||
|
List<String> mirrorsList = getStringListRepoValue(repoMap, "mirrors");
|
||||||
|
HashSet<String> 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
|
// below are optional, can be default value
|
||||||
repo.maxage = getIntRepoValue(repoMap, "maxage");
|
repo.maxage = getIntRepoValue(repoMap, "maxage");
|
||||||
repo.version = getIntRepoValue(repoMap, "version");
|
repo.version = getIntRepoValue(repoMap, "version");
|
||||||
@ -372,13 +388,12 @@ public class IndexV1Updater extends IndexUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private String[] getStringArrayRepoValue(Map<String, Object> repoMap, String key) {
|
private List<String> getStringListRepoValue(Map<String, Object> repoMap, String key) {
|
||||||
Object value = repoMap.get(key);
|
Object value = repoMap.get(key);
|
||||||
if (value != null && value instanceof ArrayList) {
|
if (value != null && value instanceof ArrayList) {
|
||||||
ArrayList<String> list = (ArrayList<String>) value;
|
return (List<String>) value;
|
||||||
return list.toArray(new String[list.size()]);
|
|
||||||
}
|
}
|
||||||
return null;
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private HashMap<String, Object> parseRepo(ObjectMapper mapper, JsonParser parser) throws IOException {
|
private HashMap<String, Object> parseRepo(ObjectMapper mapper, JsonParser parser) throws IOException {
|
||||||
|
@ -92,6 +92,7 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
|
|||||||
public static final String PREF_PRIVILEGED_INSTALLER = "privilegedInstaller";
|
public static final String PREF_PRIVILEGED_INSTALLER = "privilegedInstaller";
|
||||||
public static final String PREF_LOCAL_REPO_NAME = "localRepoName";
|
public static final String PREF_LOCAL_REPO_NAME = "localRepoName";
|
||||||
public static final String PREF_LOCAL_REPO_HTTPS = "localRepoHttps";
|
public static final String PREF_LOCAL_REPO_HTTPS = "localRepoHttps";
|
||||||
|
public static final String PREF_SCAN_REMOVABLE_STORAGE = "scanRemovableStorage";
|
||||||
public static final String PREF_LANGUAGE = "language";
|
public static final String PREF_LANGUAGE = "language";
|
||||||
public static final String PREF_USE_TOR = "useTor";
|
public static final String PREF_USE_TOR = "useTor";
|
||||||
public static final String PREF_ENABLE_PROXY = "enableProxy";
|
public static final String PREF_ENABLE_PROXY = "enableProxy";
|
||||||
@ -400,6 +401,10 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
|
|||||||
return preferences.getString(PREF_LOCAL_REPO_NAME, getDefaultLocalRepoName());
|
return preferences.getString(PREF_LOCAL_REPO_NAME, getDefaultLocalRepoName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isScanRemovableStorageEnabled() {
|
||||||
|
return preferences.getBoolean(PREF_SCAN_REMOVABLE_STORAGE, true);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isUpdateNotificationEnabled() {
|
public boolean isUpdateNotificationEnabled() {
|
||||||
return preferences.getBoolean(PREF_UPDATE_NOTIFICATION_ENABLED, true);
|
return preferences.getBoolean(PREF_UPDATE_NOTIFICATION_ENABLED, true);
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import android.app.job.JobInfo;
|
|||||||
import android.app.job.JobScheduler;
|
import android.app.job.JobScheduler;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
@ -406,6 +407,13 @@ public class UpdateService extends JobIntentService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isLocalRepoAddress(String address) {
|
||||||
|
return address != null &&
|
||||||
|
(address.startsWith(BluetoothDownloader.SCHEME)
|
||||||
|
|| address.startsWith(ContentResolver.SCHEME_CONTENT)
|
||||||
|
|| address.startsWith(ContentResolver.SCHEME_FILE));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onHandleWork(@NonNull Intent intent) {
|
protected void onHandleWork(@NonNull Intent intent) {
|
||||||
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
|
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
|
||||||
@ -417,16 +425,38 @@ public class UpdateService extends JobIntentService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final Preferences fdroidPrefs = Preferences.get();
|
final Preferences fdroidPrefs = Preferences.get();
|
||||||
|
|
||||||
|
// Grab some preliminary information, then we can release the
|
||||||
|
// database while we do all the downloading, etc...
|
||||||
|
List<Repo> repos = RepoProvider.Helper.all(this);
|
||||||
|
|
||||||
// See if it's time to actually do anything yet...
|
// See if it's time to actually do anything yet...
|
||||||
int netState = ConnectivityMonitorService.getNetworkState(this);
|
int netState = ConnectivityMonitorService.getNetworkState(this);
|
||||||
if (address != null && address.startsWith(BluetoothDownloader.SCHEME)) {
|
if (isLocalRepoAddress(address)) {
|
||||||
Utils.debugLog(TAG, "skipping internet check, this is bluetooth");
|
Utils.debugLog(TAG, "skipping internet check, this is local: " + address);
|
||||||
} else if (netState == ConnectivityMonitorService.FLAG_NET_UNAVAILABLE) {
|
} else if (netState == ConnectivityMonitorService.FLAG_NET_UNAVAILABLE) {
|
||||||
|
boolean foundLocalRepo = false;
|
||||||
|
for (Repo repo : repos) {
|
||||||
|
if (isLocalRepoAddress(repo.address)) {
|
||||||
|
foundLocalRepo = true;
|
||||||
|
} else {
|
||||||
|
for (String mirrorAddress : repo.getMirrorList()) {
|
||||||
|
if (isLocalRepoAddress(mirrorAddress)) {
|
||||||
|
foundLocalRepo = true;
|
||||||
|
//localRepos.add(repo);
|
||||||
|
//FDroidApp.setLastWorkingMirror(repo.getId(), mirrorAddress);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundLocalRepo) {
|
||||||
Utils.debugLog(TAG, "No internet, cannot update");
|
Utils.debugLog(TAG, "No internet, cannot update");
|
||||||
if (manualUpdate) {
|
if (manualUpdate) {
|
||||||
sendNoInternetToast();
|
sendNoInternetToast();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
} else if ((manualUpdate || forcedUpdate) && fdroidPrefs.isOnDemandDownloadAllowed()) {
|
} else if ((manualUpdate || forcedUpdate) && fdroidPrefs.isOnDemandDownloadAllowed()) {
|
||||||
Utils.debugLog(TAG, "manually requested or forced update");
|
Utils.debugLog(TAG, "manually requested or forced update");
|
||||||
if (forcedUpdate) {
|
if (forcedUpdate) {
|
||||||
@ -442,10 +472,6 @@ public class UpdateService extends JobIntentService {
|
|||||||
LocalBroadcastManager.getInstance(this).registerReceiver(updateStatusReceiver,
|
LocalBroadcastManager.getInstance(this).registerReceiver(updateStatusReceiver,
|
||||||
new IntentFilter(LOCAL_ACTION_STATUS));
|
new IntentFilter(LOCAL_ACTION_STATUS));
|
||||||
|
|
||||||
// Grab some preliminary information, then we can release the
|
|
||||||
// database while we do all the downloading, etc...
|
|
||||||
List<Repo> repos = RepoProvider.Helper.all(this);
|
|
||||||
|
|
||||||
int unchangedRepos = 0;
|
int unchangedRepos = 0;
|
||||||
int updatedRepos = 0;
|
int updatedRepos = 0;
|
||||||
int errorRepos = 0;
|
int errorRepos = 0;
|
||||||
@ -482,7 +508,8 @@ public class UpdateService extends JobIntentService {
|
|||||||
} catch (IndexUpdater.UpdateException e) {
|
} catch (IndexUpdater.UpdateException e) {
|
||||||
errorRepos++;
|
errorRepos++;
|
||||||
repoErrors.add(e.getMessage());
|
repoErrors.add(e.getMessage());
|
||||||
Log.e(TAG, "Error updating repository " + repo.address, e);
|
Log.e(TAG, "Error updating repository " + repo.address);
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
// now that downloading the index is done, start downloading updates
|
// now that downloading the index is done, start downloading updates
|
||||||
|
@ -1394,6 +1394,12 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
return exists;
|
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,
|
private void insertRepo(SQLiteDatabase db, String name, String address,
|
||||||
String description, String version, String enabled,
|
String description, String version, String enabled,
|
||||||
String priority, String pushRequests, String pubKey) {
|
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.LAST_ETAG, (String) null);
|
||||||
values.put(RepoTable.Cols.TIMESTAMP, 0);
|
values.put(RepoTable.Cols.TIMESTAMP, 0);
|
||||||
|
|
||||||
|
String[] initializeMirrors = {address};
|
||||||
|
values.put(Schema.RepoTable.Cols.MIRRORS, Utils.serializeCommaSeparatedString(initializeMirrors));
|
||||||
|
|
||||||
switch (pushRequests) {
|
switch (pushRequests) {
|
||||||
case "ignore":
|
case "ignore":
|
||||||
values.put(RepoTable.Cols.PUSH_REQUESTS, Repo.PUSH_REQUEST_IGNORE);
|
values.put(RepoTable.Cols.PUSH_REQUESTS, Repo.PUSH_REQUEST_IGNORE);
|
||||||
|
@ -4,6 +4,7 @@ import android.content.Context;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
import org.fdroid.fdroid.localrepo.peers.WifiPeer;
|
import org.fdroid.fdroid.localrepo.peers.WifiPeer;
|
||||||
@ -53,8 +54,9 @@ public class NewRepoConfig {
|
|||||||
String scheme = uri.getScheme();
|
String scheme = uri.getScheme();
|
||||||
host = uri.getHost();
|
host = uri.getHost();
|
||||||
port = uri.getPort();
|
port = uri.getPort();
|
||||||
if (TextUtils.isEmpty(scheme) || TextUtils.isEmpty(host)) {
|
if (TextUtils.isEmpty(scheme) || (TextUtils.isEmpty(host) && !"file".equals(scheme))) {
|
||||||
errorMessage = String.format(context.getString(R.string.malformed_repo_uri), uri);
|
errorMessage = String.format(context.getString(R.string.malformed_repo_uri), uri);
|
||||||
|
Log.i(TAG, errorMessage);
|
||||||
isValidRepo = false;
|
isValidRepo = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -82,7 +84,7 @@ public class NewRepoConfig {
|
|||||||
host = host.toLowerCase(Locale.ENGLISH);
|
host = host.toLowerCase(Locale.ENGLISH);
|
||||||
|
|
||||||
if (uri.getPath() == null
|
if (uri.getPath() == null
|
||||||
|| !Arrays.asList("https", "http", "fdroidrepos", "fdroidrepo").contains(scheme)) {
|
|| !Arrays.asList("https", "http", "fdroidrepos", "fdroidrepo", "content", "file").contains(scheme)) {
|
||||||
isValidRepo = false;
|
isValidRepo = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
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() {
|
public boolean hasMirrors() {
|
||||||
return (mirrors != null && mirrors.length > 1)
|
return (mirrors != null && mirrors.length > 1)
|
||||||
|| (userMirrors != null && userMirrors.length > 0);
|
|| (userMirrors != null && userMirrors.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {@link List} of valid URLs to reach this repo, including the canonical URL
|
||||||
|
*/
|
||||||
public List<String> getMirrorList() {
|
public List<String> getMirrorList() {
|
||||||
final ArrayList<String> allMirrors = new ArrayList<String>();
|
final HashSet<String> allMirrors = new HashSet<>();
|
||||||
if (userMirrors != null) {
|
if (userMirrors != null) {
|
||||||
allMirrors.addAll(Arrays.asList(userMirrors));
|
allMirrors.addAll(Arrays.asList(userMirrors));
|
||||||
}
|
}
|
||||||
if (mirrors != null) {
|
if (mirrors != null) {
|
||||||
allMirrors.addAll(Arrays.asList(mirrors));
|
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() {
|
public int getMirrorCount() {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (String m : getMirrorList()) {
|
for (String m : getMirrorList()) {
|
||||||
if (!m.equals(address)) {
|
|
||||||
if (FDroidApp.isUsingTor()) {
|
if (FDroidApp.isUsingTor()) {
|
||||||
count++;
|
count++;
|
||||||
} else {
|
} else if (!m.contains(".onion")) {
|
||||||
if (!m.contains(".onion")) {
|
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
return 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.
|
||||||
|
* <p>
|
||||||
|
* 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) {
|
public String getMirror(String lastWorkingMirror) {
|
||||||
if (TextUtils.isEmpty(lastWorkingMirror)) {
|
if (TextUtils.isEmpty(lastWorkingMirror)) {
|
||||||
lastWorkingMirror = address;
|
lastWorkingMirror = address;
|
||||||
@ -382,7 +399,7 @@ public class Repo extends ValueObject {
|
|||||||
if (shuffledMirrors.size() > 1) {
|
if (shuffledMirrors.size() > 1) {
|
||||||
for (String m : shuffledMirrors) {
|
for (String m : shuffledMirrors) {
|
||||||
// Return a non default, and not last used mirror
|
// Return a non default, and not last used mirror
|
||||||
if (!m.equals(address) && !m.equals(lastWorkingMirror)) {
|
if (!m.equals(lastWorkingMirror)) {
|
||||||
if (FDroidApp.isUsingTor()) {
|
if (FDroidApp.isUsingTor()) {
|
||||||
return m;
|
return m;
|
||||||
} else {
|
} else {
|
||||||
|
@ -31,6 +31,10 @@ public class DownloaderFactory {
|
|||||||
String scheme = uri.getScheme();
|
String scheme = uri.getScheme();
|
||||||
if ("bluetooth".equals(scheme)) {
|
if ("bluetooth".equals(scheme)) {
|
||||||
downloader = new BluetoothDownloader(uri, destFile);
|
downloader = new BluetoothDownloader(uri, destFile);
|
||||||
|
} else if ("content".equals(scheme)) {
|
||||||
|
downloader = new TreeUriDownloader(uri, destFile);
|
||||||
|
} else if ("file".equals(scheme)) {
|
||||||
|
downloader = new LocalFileDownloader(uri, destFile);
|
||||||
} else {
|
} else {
|
||||||
final String[] projection = {Schema.RepoTable.Cols.USERNAME, Schema.RepoTable.Cols.PASSWORD};
|
final String[] projection = {Schema.RepoTable.Cols.USERNAME, Schema.RepoTable.Cols.PASSWORD};
|
||||||
Repo repo = RepoProvider.Helper.findByUrl(context, uri, projection);
|
Repo repo = RepoProvider.Helper.findByUrl(context, uri, projection);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.fdroid.fdroid.net;
|
package org.fdroid.fdroid.net;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
|
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -28,6 +29,10 @@ public class ImageLoaderForUIL implements com.nostra13.universalimageloader.core
|
|||||||
case HTTP:
|
case HTTP:
|
||||||
case HTTPS:
|
case HTTPS:
|
||||||
return DownloaderFactory.create(context, imageUri).getInputStream();
|
return DownloaderFactory.create(context, imageUri).getInputStream();
|
||||||
|
case CONTENT:
|
||||||
|
if (Build.VERSION.SDK_INT >= 19) {
|
||||||
|
return DownloaderFactory.create(context, imageUri).getInputStream();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return new BaseImageDownloader(context).getStream(imageUri, extra);
|
return new BaseImageDownloader(context).getStream(imageUri, extra);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,86 @@
|
|||||||
|
package org.fdroid.fdroid.net;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.ConnectException;
|
||||||
|
import java.net.ProtocolException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Downloads" files from {@code file:///} {@link Uri}s. Even though it is
|
||||||
|
* obviously unnecessary to download a file that is locally available, this
|
||||||
|
* class is here so that the whole security-sensitive installation process is
|
||||||
|
* the same, no matter where the files are downloaded from. Also, for things
|
||||||
|
* like icons and graphics, it makes sense to have them copied to the cache so
|
||||||
|
* that they are available even after removable storage is no longer present.
|
||||||
|
*/
|
||||||
|
public class LocalFileDownloader extends Downloader {
|
||||||
|
|
||||||
|
private InputStream inputStream;
|
||||||
|
private final File sourceFile;
|
||||||
|
|
||||||
|
LocalFileDownloader(Uri uri, File destFile) {
|
||||||
|
super(uri, destFile);
|
||||||
|
sourceFile = new File(uri.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This needs to convert {@link FileNotFoundException}
|
||||||
|
* and {@link SecurityException} to {@link ProtocolException} since the
|
||||||
|
* mirror failover logic expects network errors, not filesystem or other
|
||||||
|
* errors. In the downloading logic, filesystem errors are related to the
|
||||||
|
* file as it is being downloaded and written to disk. Things can fail
|
||||||
|
* here if the SDCard is not longer mounted, the files were deleted by
|
||||||
|
* some other process, etc.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected InputStream getDownloadersInputStream() throws IOException {
|
||||||
|
try {
|
||||||
|
inputStream = new FileInputStream(sourceFile);
|
||||||
|
return inputStream;
|
||||||
|
} catch (FileNotFoundException | SecurityException e) {
|
||||||
|
throw new ProtocolException(e.getLocalizedMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void close() {
|
||||||
|
IOUtils.closeQuietly(inputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasChanged() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected long totalDownloadSize() {
|
||||||
|
return sourceFile.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download() throws ConnectException, IOException, InterruptedException {
|
||||||
|
if (!sourceFile.exists()) {
|
||||||
|
notFound = true;
|
||||||
|
throw new ConnectException(sourceFile + " does not exist, try a mirror");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean resumable = false;
|
||||||
|
long contentLength = sourceFile.length();
|
||||||
|
long fileLength = outputFile.length();
|
||||||
|
if (fileLength > contentLength) {
|
||||||
|
FileUtils.deleteQuietly(outputFile);
|
||||||
|
} else if (fileLength == contentLength && outputFile.isFile()) {
|
||||||
|
return; // already have it!
|
||||||
|
} else if (fileLength > 0) {
|
||||||
|
resumable = true;
|
||||||
|
}
|
||||||
|
downloadFromStream(8192, resumable);
|
||||||
|
}
|
||||||
|
}
|
105
app/src/main/java/org/fdroid/fdroid/net/TreeUriDownloader.java
Normal file
105
app/src/main/java/org/fdroid/fdroid/net/TreeUriDownloader.java
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package org.fdroid.fdroid.net;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.support.v4.provider.DocumentFile;
|
||||||
|
import org.fdroid.fdroid.FDroidApp;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.ProtocolException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link Downloader} subclass for downloading files from a repo on 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 via a
|
||||||
|
* {@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 android.net.Uri}.
|
||||||
|
* <p>
|
||||||
|
* The base repo URL of such a repo looks like:
|
||||||
|
* {@code content://com.android.externalstorage.documents/tree/1AFB-2402%3A/document/1AFB-2402%3Atesty.at.or.at%2Ffdroid%2Frepo}
|
||||||
|
*
|
||||||
|
* @see android.support.v4.provider.DocumentFile#fromTreeUri(Context, Uri)
|
||||||
|
* @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/training/articles/scoped-directory-access.html">Using Scoped Directory Access</a>
|
||||||
|
*/
|
||||||
|
@TargetApi(21)
|
||||||
|
public class TreeUriDownloader extends Downloader {
|
||||||
|
public static final String TAG = "TreeUriDownloader";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whoever designed this {@link android.provider.DocumentsContract#isTreeUri(Uri) URI system}
|
||||||
|
* was smoking crack, it escapes <b>part</b> of the URI path, but not all.
|
||||||
|
* So crazy tricks are required.
|
||||||
|
*/
|
||||||
|
public static final String ESCAPED_SLASH = "%2F";
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final Uri treeUri;
|
||||||
|
private final DocumentFile documentFile;
|
||||||
|
|
||||||
|
TreeUriDownloader(Uri uri, File destFile)
|
||||||
|
throws FileNotFoundException, MalformedURLException {
|
||||||
|
super(uri, destFile);
|
||||||
|
context = FDroidApp.getInstance();
|
||||||
|
String path = uri.getEncodedPath();
|
||||||
|
int lastEscapedSlash = path.lastIndexOf(ESCAPED_SLASH);
|
||||||
|
String pathChunkToEscape = path.substring(lastEscapedSlash + ESCAPED_SLASH.length());
|
||||||
|
String escapedPathChunk = Uri.encode(pathChunkToEscape);
|
||||||
|
treeUri = uri.buildUpon().encodedPath(path.replace(pathChunkToEscape, escapedPathChunk)).build();
|
||||||
|
documentFile = DocumentFile.fromTreeUri(context, treeUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This needs to convert {@link FileNotFoundException} and
|
||||||
|
* {@link IllegalArgumentException} to {@link ProtocolException} since the mirror
|
||||||
|
* failover logic expects network errors, not filesystem or other errors.
|
||||||
|
* In the downloading logic, filesystem errors are related to the file as it is
|
||||||
|
* being downloaded and written to disk. Things can fail here if the USB stick is
|
||||||
|
* not longer plugged in, the files were deleted by some other process, etc.
|
||||||
|
* <p>
|
||||||
|
* Example: {@code IllegalArgumentException: Failed to determine if
|
||||||
|
* 6EED-6A10:guardianproject.info/wind-demo/fdroid/repo/index-v1.jar is child of
|
||||||
|
* 6EED-6A10:: java.io.File NotFoundException: No root for 6EED-6A10}
|
||||||
|
* <p>
|
||||||
|
* Example:
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected InputStream getDownloadersInputStream() throws IOException {
|
||||||
|
try {
|
||||||
|
InputStream inputStream = context.getContentResolver().openInputStream(treeUri);
|
||||||
|
if (inputStream == null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return new BufferedInputStream(inputStream);
|
||||||
|
}
|
||||||
|
} catch (FileNotFoundException | IllegalArgumentException e) {
|
||||||
|
throw new ProtocolException(e.getLocalizedMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasChanged() {
|
||||||
|
return true; // TODO how should this actually be implemented?
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected long totalDownloadSize() {
|
||||||
|
return documentFile.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void download() throws IOException, InterruptedException {
|
||||||
|
downloadFromStream(8192, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void close() {
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@ package org.fdroid.fdroid.views;
|
|||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.content.ClipboardManager;
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
@ -32,9 +33,12 @@ import android.net.Uri;
|
|||||||
import android.net.wifi.WifiInfo;
|
import android.net.wifi.WifiInfo;
|
||||||
import android.net.wifi.WifiManager;
|
import android.net.wifi.WifiManager;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.app.LoaderManager;
|
import android.support.v4.app.LoaderManager;
|
||||||
|
import android.support.v4.app.NavUtils;
|
||||||
|
import android.support.v4.app.TaskStackBuilder;
|
||||||
import android.support.v4.content.CursorLoader;
|
import android.support.v4.content.CursorLoader;
|
||||||
import android.support.v4.content.Loader;
|
import android.support.v4.content.Loader;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
@ -53,6 +57,7 @@ import android.widget.EditText;
|
|||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
import org.fdroid.fdroid.AddRepoIntentService;
|
||||||
import org.fdroid.fdroid.FDroidApp;
|
import org.fdroid.fdroid.FDroidApp;
|
||||||
import org.fdroid.fdroid.IndexUpdater;
|
import org.fdroid.fdroid.IndexUpdater;
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
@ -68,7 +73,6 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -79,10 +83,12 @@ public class ManageReposActivity extends AppCompatActivity
|
|||||||
implements LoaderManager.LoaderCallbacks<Cursor>, RepoAdapter.EnabledListener {
|
implements LoaderManager.LoaderCallbacks<Cursor>, RepoAdapter.EnabledListener {
|
||||||
private static final String TAG = "ManageReposActivity";
|
private static final String TAG = "ManageReposActivity";
|
||||||
|
|
||||||
|
public static final String EXTRA_FINISH_AFTER_ADDING_REPO = "finishAfterAddingRepo";
|
||||||
|
|
||||||
private static final String DEFAULT_NEW_REPO_TEXT = "https://";
|
private static final String DEFAULT_NEW_REPO_TEXT = "https://";
|
||||||
|
|
||||||
private enum AddRepoState {
|
private enum AddRepoState {
|
||||||
DOESNT_EXIST, EXISTS_FINGERPRINT_MISMATCH, EXISTS_ADD_MIRROR,
|
DOESNT_EXIST, EXISTS_FINGERPRINT_MISMATCH, EXISTS_ADD_MIRROR, EXISTS_ALREADY_MIRROR,
|
||||||
EXISTS_DISABLED, EXISTS_ENABLED, EXISTS_UPGRADABLE_TO_SIGNED, INVALID_URL,
|
EXISTS_DISABLED, EXISTS_ENABLED, EXISTS_UPGRADABLE_TO_SIGNED, INVALID_URL,
|
||||||
IS_SWAP
|
IS_SWAP
|
||||||
}
|
}
|
||||||
@ -93,7 +99,7 @@ public class ManageReposActivity extends AppCompatActivity
|
|||||||
* True if activity started with an intent such as from QR code. False if
|
* True if activity started with an intent such as from QR code. False if
|
||||||
* opened from, e.g. the main menu.
|
* opened from, e.g. the main menu.
|
||||||
*/
|
*/
|
||||||
private boolean isImportingRepo;
|
private boolean finishAfterAddingRepo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@ -156,6 +162,16 @@ public class ManageReposActivity extends AppCompatActivity
|
|||||||
case R.id.action_add_repo:
|
case R.id.action_add_repo:
|
||||||
showAddRepo();
|
showAddRepo();
|
||||||
return true;
|
return true;
|
||||||
|
case android.R.id.home:
|
||||||
|
Intent upIntent = NavUtils.getParentActivityIntent(this);
|
||||||
|
if (NavUtils.shouldUpRecreateTask(this, upIntent) || isTaskRoot()) {
|
||||||
|
TaskStackBuilder.create(this)
|
||||||
|
.addNextIntentWithParentStack(upIntent)
|
||||||
|
.startActivities();
|
||||||
|
} else {
|
||||||
|
NavUtils.navigateUpTo(this, upIntent);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
@ -272,7 +288,7 @@ public class ManageReposActivity extends AppCompatActivity
|
|||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
if (isImportingRepo) {
|
if (finishAfterAddingRepo) {
|
||||||
ManageReposActivity.this.finish();
|
ManageReposActivity.this.finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -311,7 +327,7 @@ public class ManageReposActivity extends AppCompatActivity
|
|||||||
String url = uriEditText.getText().toString();
|
String url = uriEditText.getText().toString();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
url = normalizeUrl(url);
|
url = AddRepoIntentService.normalizeUrl(url);
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
invalidUrl();
|
invalidUrl();
|
||||||
return;
|
return;
|
||||||
@ -415,7 +431,7 @@ public class ManageReposActivity extends AppCompatActivity
|
|||||||
private void validateRepoDetails(@NonNull String uri, @NonNull String fingerprint) {
|
private void validateRepoDetails(@NonNull String uri, @NonNull String fingerprint) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
uri = normalizeUrl(uri);
|
uri = AddRepoIntentService.normalizeUrl(uri);
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
// Don't bother dealing with this exception yet, as this is called every time
|
// Don't bother dealing with this exception yet, as this is called every time
|
||||||
// a letter is added to the repo URL text input. We don't want to display a message
|
// a letter is added to the repo URL text input. We don't want to display a message
|
||||||
@ -437,8 +453,9 @@ public class ManageReposActivity extends AppCompatActivity
|
|||||||
} else if (repo.fingerprint != null && !repo.fingerprint.equalsIgnoreCase(fingerprint)) {
|
} else if (repo.fingerprint != null && !repo.fingerprint.equalsIgnoreCase(fingerprint)) {
|
||||||
repoFingerprintDoesntMatch(repo);
|
repoFingerprintDoesntMatch(repo);
|
||||||
} else {
|
} else {
|
||||||
if (!TextUtils.equals(repo.address, uri)
|
if (repo.getMirrorList().contains(uri) && !TextUtils.equals(repo.address, uri) && repo.inuse) {
|
||||||
&& !repo.getMirrorList().contains(uri)) {
|
repoExistsAlreadyMirror(repo);
|
||||||
|
} else if (!TextUtils.equals(repo.address, uri) && repo.inuse) {
|
||||||
repoExistsAddMirror(repo);
|
repoExistsAddMirror(repo);
|
||||||
} else if (repo.inuse) {
|
} else if (repo.inuse) {
|
||||||
repoExistsAndEnabled(repo);
|
repoExistsAndEnabled(repo);
|
||||||
@ -487,6 +504,10 @@ public class ManageReposActivity extends AppCompatActivity
|
|||||||
R.string.repo_add_mirror, true);
|
R.string.repo_add_mirror, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void repoExistsAlreadyMirror(Repo repo) {
|
||||||
|
updateUi(repo, AddRepoState.EXISTS_ALREADY_MIRROR, 0, false, R.string.ok, true);
|
||||||
|
}
|
||||||
|
|
||||||
private void upgradingToSigned(Repo repo) {
|
private void upgradingToSigned(Repo repo) {
|
||||||
updateUi(repo, AddRepoState.EXISTS_UPGRADABLE_TO_SIGNED, R.string.repo_exists_add_fingerprint,
|
updateUi(repo, AddRepoState.EXISTS_UPGRADABLE_TO_SIGNED, R.string.repo_exists_add_fingerprint,
|
||||||
false, R.string.add_key, true);
|
false, R.string.add_key, true);
|
||||||
@ -518,6 +539,13 @@ public class ManageReposActivity extends AppCompatActivity
|
|||||||
|
|
||||||
addButton.setText(addTextRes);
|
addButton.setText(addTextRes);
|
||||||
addButton.setEnabled(addEnabled);
|
addButton.setEnabled(addEnabled);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 15 && addRepoState == AddRepoState.EXISTS_ALREADY_MIRROR) {
|
||||||
|
addButton.callOnClick();
|
||||||
|
editRepo(repo);
|
||||||
|
String msg = getString(R.string.repo_exists_and_enabled, repo.address);
|
||||||
|
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -555,6 +583,12 @@ public class ManageReposActivity extends AppCompatActivity
|
|||||||
return originalAddress;
|
return originalAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (originalAddress.startsWith(ContentResolver.SCHEME_CONTENT)
|
||||||
|
|| originalAddress.startsWith(ContentResolver.SCHEME_FILE)) {
|
||||||
|
// TODO check whether there is read access
|
||||||
|
return originalAddress;
|
||||||
|
}
|
||||||
|
|
||||||
final String[] pathsToCheck = {"", "fdroid/repo", "repo"};
|
final String[] pathsToCheck = {"", "fdroid/repo", "repo"};
|
||||||
for (final String path : pathsToCheck) {
|
for (final String path : pathsToCheck) {
|
||||||
|
|
||||||
@ -683,53 +717,6 @@ public class ManageReposActivity extends AppCompatActivity
|
|||||||
checker.execute(originalAddress);
|
checker.execute(originalAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Some basic sanitization of URLs, so that two URLs which have the same semantic meaning
|
|
||||||
* are represented by the exact same string by F-Droid. This will help to make sure that,
|
|
||||||
* e.g. "http://10.0.1.50" and "http://10.0.1.50/" are not two different repositories.
|
|
||||||
* <p>
|
|
||||||
* Currently it normalizes the path so that "/./" are removed and "test/../" is collapsed.
|
|
||||||
* This is done using {@link URI#normalize()}. It also removes multiple consecutive forward
|
|
||||||
* slashes in the path and replaces them with one. Finally, it removes trailing slashes.
|
|
||||||
* <p>
|
|
||||||
* {@code content://} URLs used for repos stored on removable storage get messed up by
|
|
||||||
* {@link URI}.
|
|
||||||
*/
|
|
||||||
private String normalizeUrl(String urlString) throws URISyntaxException {
|
|
||||||
if (urlString == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Uri uri = Uri.parse(urlString);
|
|
||||||
if (!uri.isAbsolute()) {
|
|
||||||
throw new URISyntaxException(urlString, "Must provide an absolute URI for repositories");
|
|
||||||
}
|
|
||||||
if (!uri.isHierarchical()) {
|
|
||||||
throw new URISyntaxException(urlString, "Must provide an hierarchical URI for repositories");
|
|
||||||
}
|
|
||||||
if ("content".equals(uri.getScheme())) {
|
|
||||||
return uri.toString();
|
|
||||||
}
|
|
||||||
String path = uri.getPath();
|
|
||||||
if (path != null) {
|
|
||||||
path = path.replaceAll("//*/", "/"); // Collapse multiple forward slashes into 1.
|
|
||||||
if (path.length() > 0 && path.charAt(path.length() - 1) == '/') {
|
|
||||||
path = path.substring(0, path.length() - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String scheme = uri.getScheme();
|
|
||||||
String host = uri.getHost();
|
|
||||||
if (TextUtils.isEmpty(scheme) || TextUtils.isEmpty(host)) {
|
|
||||||
return urlString;
|
|
||||||
}
|
|
||||||
return new URI(scheme.toLowerCase(Locale.ENGLISH),
|
|
||||||
uri.getUserInfo(),
|
|
||||||
host.toLowerCase(Locale.ENGLISH),
|
|
||||||
uri.getPort(),
|
|
||||||
path,
|
|
||||||
uri.getQuery(),
|
|
||||||
uri.getFragment()).normalize().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a repository without a username or password.
|
* Create a repository without a username or password.
|
||||||
*/
|
*/
|
||||||
@ -740,7 +727,7 @@ public class ManageReposActivity extends AppCompatActivity
|
|||||||
private void createNewRepo(String address, String fingerprint,
|
private void createNewRepo(String address, String fingerprint,
|
||||||
final String username, final String password) {
|
final String username, final String password) {
|
||||||
try {
|
try {
|
||||||
address = normalizeUrl(address);
|
address = AddRepoIntentService.normalizeUrl(address);
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
// Leave address as it was.
|
// Leave address as it was.
|
||||||
}
|
}
|
||||||
@ -816,7 +803,7 @@ public class ManageReposActivity extends AppCompatActivity
|
|||||||
if (addRepoDialog.isShowing()) {
|
if (addRepoDialog.isShowing()) {
|
||||||
addRepoDialog.dismiss();
|
addRepoDialog.dismiss();
|
||||||
}
|
}
|
||||||
if (isImportingRepo) {
|
if (finishAfterAddingRepo) {
|
||||||
setResult(RESULT_OK);
|
setResult(RESULT_OK);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
@ -827,7 +814,7 @@ public class ManageReposActivity extends AppCompatActivity
|
|||||||
/* an URL from a click, NFC, QRCode scan, etc */
|
/* an URL from a click, NFC, QRCode scan, etc */
|
||||||
NewRepoConfig newRepoConfig = new NewRepoConfig(this, intent);
|
NewRepoConfig newRepoConfig = new NewRepoConfig(this, intent);
|
||||||
if (newRepoConfig.isValidRepo()) {
|
if (newRepoConfig.isValidRepo()) {
|
||||||
isImportingRepo = true;
|
finishAfterAddingRepo = intent.getBooleanExtra(EXTRA_FINISH_AFTER_ADDING_REPO, true);
|
||||||
showAddRepo(newRepoConfig.getRepoUriString(), newRepoConfig.getFingerprint(),
|
showAddRepo(newRepoConfig.getRepoUriString(), newRepoConfig.getFingerprint(),
|
||||||
newRepoConfig.getUsername(), newRepoConfig.getPassword());
|
newRepoConfig.getUsername(), newRepoConfig.getPassword());
|
||||||
checkIfNewRepoOnSameWifi(newRepoConfig);
|
checkIfNewRepoOnSameWifi(newRepoConfig);
|
||||||
|
@ -30,6 +30,7 @@ import android.content.IntentFilter;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.content.LocalBroadcastManager;
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
@ -41,7 +42,6 @@ import android.widget.Toast;
|
|||||||
import com.ashokvarma.bottomnavigation.BottomNavigationBar;
|
import com.ashokvarma.bottomnavigation.BottomNavigationBar;
|
||||||
import com.ashokvarma.bottomnavigation.BottomNavigationItem;
|
import com.ashokvarma.bottomnavigation.BottomNavigationItem;
|
||||||
import com.ashokvarma.bottomnavigation.TextBadgeItem;
|
import com.ashokvarma.bottomnavigation.TextBadgeItem;
|
||||||
import org.fdroid.fdroid.views.AppDetailsActivity;
|
|
||||||
import org.fdroid.fdroid.AppUpdateStatusManager;
|
import org.fdroid.fdroid.AppUpdateStatusManager;
|
||||||
import org.fdroid.fdroid.AppUpdateStatusManager.AppUpdateStatus;
|
import org.fdroid.fdroid.AppUpdateStatusManager.AppUpdateStatus;
|
||||||
import org.fdroid.fdroid.BuildConfig;
|
import org.fdroid.fdroid.BuildConfig;
|
||||||
@ -52,6 +52,7 @@ import org.fdroid.fdroid.R;
|
|||||||
import org.fdroid.fdroid.UpdateService;
|
import org.fdroid.fdroid.UpdateService;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
import org.fdroid.fdroid.data.NewRepoConfig;
|
import org.fdroid.fdroid.data.NewRepoConfig;
|
||||||
|
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.views.swap.SwapWorkflowActivity;
|
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
|
||||||
@ -77,6 +78,9 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
|
|||||||
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_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_STORAGE_PERMISSIONS = 0xB004;
|
||||||
|
|
||||||
private static final String ADD_REPO_INTENT_HANDLED = "addRepoIntentHandled";
|
private static final String ADD_REPO_INTENT_HANDLED = "addRepoIntentHandled";
|
||||||
|
|
||||||
private static final String ACTION_ADD_REPO = "org.fdroid.fdroid.MainActivity.ACTION_ADD_REPO";
|
private static final String ACTION_ADD_REPO = "org.fdroid.fdroid.MainActivity.ACTION_ADD_REPO";
|
||||||
@ -206,6 +210,14 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
|
|||||||
checkForAddRepoIntent(intent);
|
checkForAddRepoIntent(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { // NOCHECKSTYLE LineLength
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
if (requestCode == REQUEST_LOCATION_PERMISSIONS) {
|
||||||
|
startActivity(new Intent(this, SwapWorkflowActivity.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTabSelected(int position) {
|
public void onTabSelected(int position) {
|
||||||
pager.scrollToPosition(position);
|
pager.scrollToPosition(position);
|
||||||
@ -341,7 +353,12 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
|
|||||||
confirmIntent.setData(intent.getData());
|
confirmIntent.setData(intent.getData());
|
||||||
startActivityForResult(confirmIntent, REQUEST_SWAP);
|
startActivityForResult(confirmIntent, REQUEST_SWAP);
|
||||||
} else {
|
} else {
|
||||||
startActivity(new Intent(ACTION_ADD_REPO, intent.getData(), this, ManageReposActivity.class));
|
Intent clean = new Intent(ACTION_ADD_REPO, intent.getData(), this, ManageReposActivity.class);
|
||||||
|
if (intent.hasExtra(ManageReposActivity.EXTRA_FINISH_AFTER_ADDING_REPO)) {
|
||||||
|
clean.putExtra(ManageReposActivity.EXTRA_FINISH_AFTER_ADDING_REPO,
|
||||||
|
intent.getBooleanExtra(ManageReposActivity.EXTRA_FINISH_AFTER_ADDING_REPO, true));
|
||||||
|
}
|
||||||
|
startActivity(clean);
|
||||||
}
|
}
|
||||||
finish();
|
finish();
|
||||||
} else if (parser.getErrorMessage() != null) {
|
} else if (parser.getErrorMessage() != null) {
|
||||||
|
@ -59,6 +59,10 @@
|
|||||||
<string name="local_repo_name">Name of your Local Repo</string>
|
<string name="local_repo_name">Name of your Local Repo</string>
|
||||||
<string name="local_repo_name_summary">The advertised title of your local repo: %s</string>
|
<string name="local_repo_name_summary">The advertised title of your local repo: %s</string>
|
||||||
<string name="local_repo_https_on">Use encrypted HTTPS:// connection for local repo</string>
|
<string name="local_repo_https_on">Use encrypted HTTPS:// connection for local repo</string>
|
||||||
|
<string name="scan_removable_storage_title">Scan removable storage</string>
|
||||||
|
<string name="scan_removable_storage_summary">Look for package repos on removable storage like SD Cards
|
||||||
|
and USB thumb drives
|
||||||
|
</string>
|
||||||
|
|
||||||
<string name="login_title">Authentication required</string>
|
<string name="login_title">Authentication required</string>
|
||||||
<string name="login_name">Username</string>
|
<string name="login_name">Username</string>
|
||||||
@ -406,6 +410,8 @@ This often occurs with apps installed via Google Play or other sources, if they
|
|||||||
<string name="nearby_splash__download_apps_from_people_nearby">No Internet? Get apps from people near you!</string>
|
<string name="nearby_splash__download_apps_from_people_nearby">No Internet? Get apps from people near you!</string>
|
||||||
<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__request_permission">Try it</string>
|
||||||
|
|
||||||
<string name="swap_nfc_title">Touch to swap</string>
|
<string name="swap_nfc_title">Touch to swap</string>
|
||||||
<string name="swap_nfc_description">If your friend has F-Droid and NFC turned on touch your devices together.
|
<string name="swap_nfc_description">If your friend has F-Droid and NFC turned on touch your devices together.
|
||||||
@ -452,6 +458,10 @@ This often occurs with apps installed via Google Play or other sources, if they
|
|||||||
<string name="swap_connection_misc_error">Error occurred while connecting to device, can\'t swap with it!</string>
|
<string name="swap_connection_misc_error">Error occurred while connecting to device, can\'t swap with it!</string>
|
||||||
<string name="swap_not_enabled">Swapping not enabled</string>
|
<string name="swap_not_enabled">Swapping not enabled</string>
|
||||||
<string name="swap_not_enabled_description">Before swapping, your device must be made visible.</string>
|
<string name="swap_not_enabled_description">Before swapping, your device must be made visible.</string>
|
||||||
|
<string name="swap_toast_using_path">Using %1$s</string>
|
||||||
|
<string name="swap_toast_not_removable_storage">That choice did not match any removeable storage devices, try
|
||||||
|
again!</string>
|
||||||
|
<string name="swap_toast_find_removeable_storage">Choose your removeable SD Card or USB</string>
|
||||||
<string name="swap_toast_invalid_url">Invalid URL for swapping: %1$s</string>
|
<string name="swap_toast_invalid_url">Invalid URL for swapping: %1$s</string>
|
||||||
<string name="swap_toast_hotspot_enabled">Wi-Fi Hotspot enabled</string>
|
<string name="swap_toast_hotspot_enabled">Wi-Fi Hotspot enabled</string>
|
||||||
<string name="swap_toast_could_not_enable_hotspot">Could not enable Wi-Fi Hotspot!</string>
|
<string name="swap_toast_could_not_enable_hotspot">Could not enable Wi-Fi Hotspot!</string>
|
||||||
|
@ -96,6 +96,11 @@
|
|||||||
<EditTextPreference
|
<EditTextPreference
|
||||||
android:key="localRepoName"
|
android:key="localRepoName"
|
||||||
android:title="@string/local_repo_name"/>
|
android:title="@string/local_repo_name"/>
|
||||||
|
<SwitchPreference
|
||||||
|
android:key="scanRemovableStorage"
|
||||||
|
android:defaultValue="true"
|
||||||
|
android:title="@string/scan_removable_storage_title"
|
||||||
|
android:summary="@string/scan_removable_storage_summary"/>
|
||||||
</android.support.v7.preference.PreferenceCategory>
|
</android.support.v7.preference.PreferenceCategory>
|
||||||
|
|
||||||
<android.support.v7.preference.PreferenceCategory android:title="@string/proxy">
|
<android.support.v7.preference.PreferenceCategory android:title="@string/proxy">
|
||||||
|
@ -34,13 +34,13 @@ public class FDroidRepoUpdateTest extends MultiIndexUpdaterTest {
|
|||||||
|
|
||||||
protected void updateEarlier() throws IndexUpdater.UpdateException {
|
protected void updateEarlier() throws IndexUpdater.UpdateException {
|
||||||
Utils.debugLog(TAG, "Updating earlier version of F-Droid repo");
|
Utils.debugLog(TAG, "Updating earlier version of F-Droid repo");
|
||||||
updateRepo(createRepoUpdater(REPO_FDROID, REPO_FDROID_URI, context, REPO_FDROID_PUB_KEY),
|
updateRepo(createIndexUpdater(REPO_FDROID, REPO_FDROID_URI, context, REPO_FDROID_PUB_KEY),
|
||||||
"index.fdroid.2016-10-30.jar");
|
"index.fdroid.2016-10-30.jar");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void updateLater() throws IndexUpdater.UpdateException {
|
protected void updateLater() throws IndexUpdater.UpdateException {
|
||||||
Utils.debugLog(TAG, "Updating later version of F-Droid repo");
|
Utils.debugLog(TAG, "Updating later version of F-Droid repo");
|
||||||
updateRepo(createRepoUpdater(REPO_FDROID, REPO_FDROID_URI, context, REPO_FDROID_PUB_KEY),
|
updateRepo(createIndexUpdater(REPO_FDROID, REPO_FDROID_URI, context, REPO_FDROID_PUB_KEY),
|
||||||
"index.fdroid.2016-11-10.jar");
|
"index.fdroid.2016-11-10.jar");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,11 +171,11 @@ public abstract class MultiIndexUpdaterTest extends FDroidProviderTest {
|
|||||||
return RepoProvider.Helper.findByAddress(context, uri);
|
return RepoProvider.Helper.findByAddress(context, uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected IndexUpdater createRepoUpdater(String name, String uri, Context context) {
|
protected IndexUpdater createIndexUpdater(String name, String uri, Context context) {
|
||||||
return new IndexUpdater(context, createRepo(name, uri, context));
|
return new IndexUpdater(context, createRepo(name, uri, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected IndexUpdater createRepoUpdater(String name, String uri, Context context, String signingCert) {
|
protected IndexUpdater createIndexUpdater(String name, String uri, Context context, String signingCert) {
|
||||||
return new IndexUpdater(context, createRepo(name, uri, context, signingCert));
|
return new IndexUpdater(context, createRepo(name, uri, context, signingCert));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,15 +184,15 @@ public abstract class MultiIndexUpdaterTest extends FDroidProviderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void updateConflicting() throws UpdateException {
|
protected void updateConflicting() throws UpdateException {
|
||||||
updateRepo(createRepoUpdater(REPO_CONFLICTING, REPO_CONFLICTING_URI, context), "multiRepo.conflicting.jar");
|
updateRepo(createIndexUpdater(REPO_CONFLICTING, REPO_CONFLICTING_URI, context), "multiRepo.conflicting.jar");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void updateMain() throws UpdateException {
|
protected void updateMain() throws UpdateException {
|
||||||
updateRepo(createRepoUpdater(REPO_MAIN, REPO_MAIN_URI, context), "multiRepo.normal.jar");
|
updateRepo(createIndexUpdater(REPO_MAIN, REPO_MAIN_URI, context), "multiRepo.normal.jar");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void updateArchive() throws UpdateException {
|
protected void updateArchive() throws UpdateException {
|
||||||
updateRepo(createRepoUpdater(REPO_ARCHIVE, REPO_ARCHIVE_URI, context), "multiRepo.archive.jar");
|
updateRepo(createIndexUpdater(REPO_ARCHIVE, REPO_ARCHIVE_URI, context), "multiRepo.archive.jar");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void updateRepo(IndexUpdater updater, String indexJarPath) throws UpdateException {
|
protected void updateRepo(IndexUpdater updater, String indexJarPath) throws UpdateException {
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
<module name="FileContentsHolder" />
|
<module name="FileContentsHolder" />
|
||||||
<module name="LineLength">
|
<module name="LineLength">
|
||||||
<property name="max" value="118"/>
|
<property name="max" value="118"/>
|
||||||
<property name="ignorePattern" value="https?://"/>
|
<property name="ignorePattern" value="[a-z]+://"/>
|
||||||
</module>
|
</module>
|
||||||
|
|
||||||
<module name="ConstantName">
|
<module name="ConstantName">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user