handle repo Intents for mirrors that are already enabled

This adds a new IntentService to pre-process Intents that request a
new repo is added.  Right now, this only handles Intents that come
from the new storage scanners.

This also adds a new case to the AddRepo UI logic to cover when an
incoming Intent is for a mirror that is already included in an enabled
repo.  In that case, the user is show the Repo Details screen for the
repo that includes that mirror.  This is done is a hacky way right now
since the only path through is to click the button.  So this clicks
the button in code.
This commit is contained in:
Hans-Christoph Steiner 2018-04-19 10:31:48 +02:00
parent c4b0955c96
commit 1d1f489d85
4 changed files with 170 additions and 65 deletions

View File

@ -29,13 +29,13 @@ 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 org.fdroid.fdroid.views.main.MainActivity;
import java.io.File;
import java.io.IOException;
@ -159,16 +159,7 @@ public class TreeUriScannerIntentService extends IntentService {
}
}
Intent intent = new Intent(context, MainActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(repoUri.buildUpon()
.appendQueryParameter("fingerprint", fingerprint)
.build());
context.startActivity(intent);
// TODO parse repo URL/mirrors/fingerprint using Jackson
// https://stackoverflow.com/questions/24835431/use-jackson-to-stream-parse-an-array-of-json-objects#
AddRepoIntentService.addRepo(context, repoUri, fingerprint);
// TODO rework IndexUpdater.getSigningCertFromJar to work for here
// TODO check whether fingerprint is already in the database
}
}

View File

@ -271,6 +271,9 @@
android:name=".data.InstalledAppProviderService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<service
android:name=".AddRepoIntentService"
android:exported="false"/>
<!-- Warning: Please add all new services to HidingManager -->

View File

@ -0,0 +1,145 @@
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.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.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();
}
}

View File

@ -33,6 +33,7 @@ import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.LoaderManager;
@ -54,6 +55,7 @@ import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import org.fdroid.fdroid.AddRepoIntentService;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.IndexUpdater;
import org.fdroid.fdroid.R;
@ -69,7 +71,6 @@ import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
@ -83,7 +84,7 @@ public class ManageReposActivity extends AppCompatActivity
private static final String DEFAULT_NEW_REPO_TEXT = "https://";
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,
IS_SWAP
}
@ -312,7 +313,7 @@ public class ManageReposActivity extends AppCompatActivity
String url = uriEditText.getText().toString();
try {
url = normalizeUrl(url);
url = AddRepoIntentService.normalizeUrl(url);
} catch (URISyntaxException e) {
invalidUrl();
return;
@ -416,7 +417,7 @@ public class ManageReposActivity extends AppCompatActivity
private void validateRepoDetails(@NonNull String uri, @NonNull String fingerprint) {
try {
uri = normalizeUrl(uri);
uri = AddRepoIntentService.normalizeUrl(uri);
} catch (URISyntaxException e) {
// 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
@ -438,8 +439,9 @@ public class ManageReposActivity extends AppCompatActivity
} else if (repo.fingerprint != null && !repo.fingerprint.equalsIgnoreCase(fingerprint)) {
repoFingerprintDoesntMatch(repo);
} else {
if (!TextUtils.equals(repo.address, uri)
&& !repo.getMirrorList().contains(uri)) {
if (repo.getMirrorList().contains(uri) && !TextUtils.equals(repo.address, uri) && repo.inuse) {
repoExistsAlreadyMirror(repo);
} else if (!TextUtils.equals(repo.address, uri) && repo.inuse) {
repoExistsAddMirror(repo);
} else if (repo.inuse) {
repoExistsAndEnabled(repo);
@ -488,6 +490,10 @@ public class ManageReposActivity extends AppCompatActivity
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) {
updateUi(repo, AddRepoState.EXISTS_UPGRADABLE_TO_SIGNED, R.string.repo_exists_add_fingerprint,
false, R.string.add_key, true);
@ -519,6 +525,13 @@ public class ManageReposActivity extends AppCompatActivity
addButton.setText(addTextRes);
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();
}
}
}
@ -690,53 +703,6 @@ public class ManageReposActivity extends AppCompatActivity
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.
*/
@ -747,7 +713,7 @@ public class ManageReposActivity extends AppCompatActivity
private void createNewRepo(String address, String fingerprint,
final String username, final String password) {
try {
address = normalizeUrl(address);
address = AddRepoIntentService.normalizeUrl(address);
} catch (URISyntaxException e) {
// Leave address as it was.
}