diff --git a/F-Droid/res/layout/login.xml b/F-Droid/res/layout/login.xml
new file mode 100644
index 000000000..332d9e02f
--- /dev/null
+++ b/F-Droid/res/layout/login.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/F-Droid/res/layout/repodetails.xml b/F-Droid/res/layout/repodetails.xml
index ce8bf3eab..1c6460782 100644
--- a/F-Droid/res/layout/repodetails.xml
+++ b/F-Droid/res/layout/repodetails.xml
@@ -75,6 +75,25 @@
android:layout_height="wrap_content"
style="@style/RepoDetailsBody"/>
+
+
+
+
+
Use Private Connection
Use encrypted HTTPS:// connection for local repo
+ Authentication required
+ Username
+ Password
+ Change Password
+ Empty username, credentials not changed
+
Search Results
App Details
No such app found.
diff --git a/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java b/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java
index 2805baba7..45d90a9d0 100644
--- a/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java
+++ b/F-Droid/src/org/fdroid/fdroid/RepoUpdater.java
@@ -95,7 +95,10 @@ public class RepoUpdater {
Downloader downloader = null;
try {
downloader = DownloaderFactory.create(context,
- getIndexAddress(), File.createTempFile("index-", "-downloaded", context.getCacheDir()));
+ getIndexAddress(), File.createTempFile("index-", "-downloaded", context.getCacheDir()),
+ repo.username,
+ repo.password
+ );
downloader.setCacheTag(repo.lastetag);
downloader.downloadUninterrupted();
diff --git a/F-Droid/src/org/fdroid/fdroid/data/DBHelper.java b/F-Droid/src/org/fdroid/fdroid/data/DBHelper.java
index 64ab6aec0..3d301f62e 100644
--- a/F-Droid/src/org/fdroid/fdroid/data/DBHelper.java
+++ b/F-Droid/src/org/fdroid/fdroid/data/DBHelper.java
@@ -34,7 +34,9 @@ public class DBHelper extends SQLiteOpenHelper {
+ "maxage integer not null default 0, "
+ "version integer not null default 0, "
+ "lastetag text, lastUpdated string,"
- + "isSwap integer boolean default 0);";
+ + "isSwap integer boolean default 0,"
+ + "username string, password string"
+ + ");";
private static final String CREATE_TABLE_APK =
"CREATE TABLE " + TABLE_APK + " ( "
@@ -102,7 +104,7 @@ public class DBHelper extends SQLiteOpenHelper {
+ " );";
private static final String DROP_TABLE_INSTALLED_APP = "DROP TABLE " + TABLE_INSTALLED_APP + ";";
- private static final int DB_VERSION = 51;
+ private static final int DB_VERSION = 52;
private final Context context;
@@ -167,12 +169,14 @@ public class DBHelper extends SQLiteOpenHelper {
+ "maxage integer not null default 0, "
+ "version integer not null default 0, "
+ "lastetag text, "
- + "lastUpdated string);";
+ + "lastUpdated string,"
+ + "username string, password string"
+ + ");";
db.execSQL(createTableDdl);
String nonIdFields = "address, name, description, inuse, priority, " +
- "pubkey, fingerprint, maxage, version, lastetag, lastUpdated";
+ "pubkey, fingerprint, maxage, version, lastetag, lastUpdated, username, password";
String insertSql = "INSERT INTO " + TABLE_REPO +
"(_id, " + nonIdFields + " ) " +
@@ -281,6 +285,7 @@ public class DBHelper extends SQLiteOpenHelper {
populateRepoNames(db, oldVersion);
if (oldVersion < 43) createInstalledApp(db);
addIsSwapToRepo(db, oldVersion);
+ addCredentialsToRepo(db, oldVersion);
addChangelogToApp(db, oldVersion);
addIconUrlLargeToApp(db, oldVersion);
updateIconUrlLarge(db, oldVersion);
@@ -419,6 +424,15 @@ public class DBHelper extends SQLiteOpenHelper {
}
}
+ private void addCredentialsToRepo(SQLiteDatabase db, int oldVersion) {
+ if (oldVersion < 52 && !columnExists(db, TABLE_REPO, "username") && !columnExists(db, TABLE_REPO, "password")) {
+ Utils.debugLog(TAG, "Adding username field to " + TABLE_REPO + " table in db.");
+ db.execSQL("alter table " + TABLE_REPO + " add column username string;");
+ Utils.debugLog(TAG, "Adding password field to " + TABLE_REPO + " table in db.");
+ db.execSQL("alter table " + TABLE_REPO + " add column password string;");
+ }
+ }
+
private void addChangelogToApp(SQLiteDatabase db, int oldVersion) {
if (oldVersion < 48 && !columnExists(db, TABLE_APP, "changelogURL")) {
Utils.debugLog(TAG, "Adding changelogURL column to " + TABLE_APP);
diff --git a/F-Droid/src/org/fdroid/fdroid/data/Repo.java b/F-Droid/src/org/fdroid/fdroid/data/Repo.java
index be53e9907..34680c772 100644
--- a/F-Droid/src/org/fdroid/fdroid/data/Repo.java
+++ b/F-Droid/src/org/fdroid/fdroid/data/Repo.java
@@ -37,6 +37,9 @@ public class Repo extends ValueObject {
public Date lastUpdated;
public boolean isSwap;
+ public String username;
+ public String password;
+
public Repo() {
}
@@ -85,6 +88,12 @@ public class Repo extends ValueObject {
case RepoProvider.DataColumns.IS_SWAP:
isSwap = cursor.getInt(i) == 1;
break;
+ case RepoProvider.DataColumns.USERNAME:
+ username = cursor.getString(i);
+ break;
+ case RepoProvider.DataColumns.PASSWORD:
+ password = cursor.getString(i);
+ break;
}
}
}
@@ -192,5 +201,13 @@ public class Repo extends ValueObject {
if (values.containsKey(RepoProvider.DataColumns.IS_SWAP)) {
isSwap = toInt(values.getAsInteger(RepoProvider.DataColumns.IS_SWAP)) == 1;
}
+
+ if (values.containsKey(RepoProvider.DataColumns.USERNAME)) {
+ username = values.getAsString(RepoProvider.DataColumns.USERNAME);
+ }
+
+ if (values.containsKey(RepoProvider.DataColumns.PASSWORD)) {
+ password = values.getAsString(RepoProvider.DataColumns.PASSWORD);
+ }
}
}
diff --git a/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java b/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java
index 52180d3df..5692091e6 100644
--- a/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java
+++ b/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java
@@ -223,10 +223,13 @@ public class RepoProvider extends FDroidProvider {
String LAST_UPDATED = "lastUpdated";
String VERSION = "version";
String IS_SWAP = "isSwap";
+ String USERNAME = "username";
+ String PASSWORD = "password";
String[] ALL = {
_ID, ADDRESS, NAME, DESCRIPTION, IN_USE, PRIORITY, PUBLIC_KEY,
FINGERPRINT, MAX_AGE, LAST_UPDATED, LAST_ETAG, VERSION, IS_SWAP,
+ USERNAME, PASSWORD,
};
}
@@ -319,7 +322,7 @@ public class RepoProvider extends FDroidProvider {
// to be present.
if (!values.containsKey(DataColumns.IN_USE)) {
- values.put(DataColumns.IN_USE, 1);
+ values.put(DataColumns.IN_USE, true);
}
if (!values.containsKey(DataColumns.PRIORITY)) {
@@ -376,5 +379,4 @@ public class RepoProvider extends FDroidProvider {
getContext().getContentResolver().notifyChange(uri, null);
return numRows;
}
-
}
diff --git a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java
index 1dd7512a4..4edc5e6b6 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java
@@ -45,6 +45,11 @@ public class DownloaderFactory {
public static Downloader create(Context context, URL url, File destFile)
throws IOException {
+ return create(context, url, destFile, null, null);
+ }
+
+ public static Downloader create(Context context, URL url, File destFile, final String username, final String password)
+ throws IOException {
if (isBluetoothAddress(url)) {
String macAddress = url.getHost().replace("-", ":");
return new BluetoothDownloader(context, macAddress, url, destFile);
@@ -53,7 +58,7 @@ public class DownloaderFactory {
} else if (isLocalFile(url)) {
return new LocalFileDownloader(context, url, destFile);
}
- return new HttpDownloader(context, url, destFile);
+ return new HttpDownloader(context, url, destFile, username, password);
}
private static boolean isBluetoothAddress(URL url) {
diff --git a/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java b/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java
index 8b930988b..0bc01979e 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/HttpDownloader.java
@@ -29,11 +29,21 @@ public class HttpDownloader extends Downloader {
protected static final String HEADER_FIELD_ETAG = "ETag";
protected HttpURLConnection connection;
- private int statusCode = -1;
+ private final String username;
+ private final String password;
+ private int statusCode = -1;
HttpDownloader(Context context, URL url, File destFile)
throws FileNotFoundException, MalformedURLException {
+ this(context, url, destFile, null, null);
+ }
+
+ HttpDownloader(Context context, URL url, File destFile, final String username, final String password)
+ throws FileNotFoundException, MalformedURLException {
super(context, url, destFile);
+
+ this.username = username;
+ this.password = password;
}
/**
@@ -44,7 +54,7 @@ public class HttpDownloader extends Downloader {
* same one twice, bail with an exception).
* @throws IOException
*/
-
+ @Override
protected InputStream getDownloadersInputStream() throws IOException {
setupConnection();
return new BufferedInputStream(connection.getInputStream());
@@ -87,10 +97,33 @@ public class HttpDownloader extends Downloader {
Proxy proxy = new Proxy(Proxy.Type.HTTP, sa);
connection = (HttpURLConnection) sourceUrl.openConnection(proxy);
} else {
+
+ // send HEAD request first, then GET afterwards
connection = (HttpURLConnection) sourceUrl.openConnection();
- final String userInfo = sourceUrl.getUserInfo();
- if (userInfo != null) {
- connection.setRequestProperty("Authorization", "Basic " + Base64.encodeBase64String(userInfo.getBytes()));
+ connection.setRequestMethod("HEAD");
+
+ // fetch HTTP status code and check for authentication
+ statusCode = connection.getResponseCode();
+ connection.disconnect();
+
+ // reset connection
+ connection = (HttpURLConnection) sourceUrl.openConnection();
+
+ // handle status codes
+ switch (statusCode) {
+ case 401:
+
+ final String userInfo = sourceUrl.getUserInfo();
+ if (userInfo != null) {
+ // add authorization header from user info in URL if present
+ connection.setRequestProperty("Authorization", "Basic " + Base64.encodeBase64String(userInfo.getBytes()));
+ } else {
+ // add authorization header from username / password
+ connection.setRequestProperty("Authorization", "Basic " + Base64.encodeBase64String((username + ":" + password).getBytes()));
+ }
+ break;
+ default:
+ break;
}
}
}
diff --git a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java
index 4f149725a..96d664c85 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java
@@ -54,9 +54,6 @@ import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.methods.HttpHead;
-import org.apache.http.impl.client.DefaultHttpClient;
import org.fdroid.fdroid.FDroid;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
@@ -68,11 +65,13 @@ import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
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.Locale;
+import org.apache.commons.net.util.Base64;
public class ManageReposActivity extends ActionBarActivity {
private static final String TAG = "ManageReposActivity";
@@ -451,6 +450,8 @@ public class ManageReposActivity extends ActionBarActivity {
final AsyncTask checker = new AsyncTask() {
+ private int statusCode = -1;
+
@Override
protected String doInBackground(String... params) {
@@ -485,9 +486,27 @@ public class ManageReposActivity extends ActionBarActivity {
}
private boolean checkForRepository(Uri indexUri) throws IOException {
- HttpClient client = new DefaultHttpClient();
- HttpHead head = new HttpHead(indexUri.toString());
- return client.execute(head).getStatusLine().getStatusCode() == 200;
+
+ final URL url = new URL(indexUri.toString());
+ final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("HEAD");
+
+ // support discovery of Basic Auth repository URLs without login prompt
+ final String userInfo = url.getUserInfo();
+ if (userInfo != null) {
+ // authorize request
+ connection.setRequestProperty("Authorization", "Basic " + Base64.encodeBase64String(userInfo.getBytes()));
+ }
+
+ statusCode = connection.getResponseCode();
+ switch (statusCode) {
+
+ case 401:
+ case 200:
+ return true;
+ }
+
+ return false;
}
@Override
@@ -497,9 +516,45 @@ public class ManageReposActivity extends ActionBarActivity {
}
@Override
- protected void onPostExecute(String newAddress) {
+ protected void onPostExecute(final String newAddress) {
+
if (addRepoDialog.isShowing()) {
- createNewRepo(newAddress, fingerprint);
+
+ if (statusCode == 401) {
+
+ final View view = getLayoutInflater().inflate(R.layout.login, null);
+ final AlertDialog credentialsDialog = new AlertDialog.Builder(context).setView(view).create();
+ final EditText nameInput = (EditText) view.findViewById(R.id.edit_name);
+ final EditText passwordInput = (EditText) view.findViewById(R.id.edit_password);
+
+ credentialsDialog.setTitle(R.string.login_title);
+ credentialsDialog.setButton(DialogInterface.BUTTON_NEGATIVE,
+ getString(R.string.cancel),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ // cancel parent dialog, don't add repo
+ addRepoDialog.cancel();
+ }
+ });
+
+ credentialsDialog.setButton(DialogInterface.BUTTON_POSITIVE,
+ getString(R.string.ok),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ createNewRepo(newAddress, fingerprint, nameInput.getText().toString(), passwordInput.getText().toString());
+ }
+ });
+
+ credentialsDialog.show();
+
+ } else {
+
+ // create repo without username/password
+ createNewRepo(newAddress, fingerprint, null, null);
+ }
}
}
};
@@ -515,7 +570,7 @@ public class ManageReposActivity extends ActionBarActivity {
// or their internet is playing up, then you'd have to wait for several
// connection timeouts before being able to proceed.
- createNewRepo(originalAddress, fingerprint);
+ createNewRepo(originalAddress, fingerprint, null, null);
checker.cancel(false);
}
});
@@ -551,17 +606,23 @@ public class ManageReposActivity extends ActionBarActivity {
path, uri.getQuery(), uri.getFragment()).toString();
}
- private void createNewRepo(String address, String fingerprint) {
+ private void createNewRepo(String address, String fingerprint, final String username, final String password) {
try {
address = normalizeUrl(address);
} catch (URISyntaxException e) {
// Leave address as it was.
}
- ContentValues values = new ContentValues(2);
+ ContentValues values = new ContentValues(4);
values.put(RepoProvider.DataColumns.ADDRESS, address);
if (!TextUtils.isEmpty(fingerprint)) {
values.put(RepoProvider.DataColumns.FINGERPRINT, fingerprint.toUpperCase(Locale.ENGLISH));
}
+
+ if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) {
+ values.put(RepoProvider.DataColumns.USERNAME, username);
+ values.put(RepoProvider.DataColumns.PASSWORD, password);
+ }
+
RepoProvider.Helper.insert(context, values);
finishedAddingRepo();
Toast.makeText(ManageReposActivity.this, getString(R.string.repo_added, address), Toast.LENGTH_SHORT).show();
diff --git a/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java b/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java
index 8583f8925..e7c3d8223 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java
@@ -2,6 +2,7 @@ package org.fdroid.fdroid.views;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
+import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -21,6 +22,8 @@ import android.text.format.DateUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
@@ -54,6 +57,9 @@ public class RepoDetailsActivity extends ActionBarActivity {
R.id.text_num_apps,
R.id.label_last_update,
R.id.text_last_update,
+ R.id.label_username,
+ R.id.text_username,
+ R.id.button_edit_credentials,
R.id.label_repo_fingerprint,
R.id.text_repo_fingerprint,
R.id.text_repo_fingerprint_description,
@@ -271,6 +277,25 @@ public class RepoDetailsActivity extends ActionBarActivity {
repoFingerprintView.setText(repoFingerprint);
}
+ private void setupCredentials(View parent, Repo repo) {
+
+ TextView usernameLabel = (TextView) parent.findViewById(R.id.label_username);
+ TextView username = (TextView) parent.findViewById(R.id.text_username);
+ Button changePassword = (Button) parent.findViewById(R.id.button_edit_credentials);
+
+ if (TextUtils.isEmpty(repo.username)) {
+ usernameLabel.setVisibility(View.GONE);
+ username.setVisibility(View.GONE);
+ username.setText("");
+ changePassword.setVisibility(View.GONE);
+ } else {
+ usernameLabel.setVisibility(View.VISIBLE);
+ username.setVisibility(View.VISIBLE);
+ username.setText(repo.username);
+ changePassword.setVisibility(View.VISIBLE);
+ }
+ }
+
private void updateRepoView() {
if (repo.hasBeenUpdated()) {
@@ -301,6 +326,7 @@ public class RepoDetailsActivity extends ActionBarActivity {
setupDescription(repoView, repo);
setupRepoFingerprint(repoView, repo);
+ setupCredentials(repoView, repo);
// Repos that existed before this feature was supported will have an
// "Unknown" last update until next time they update...
@@ -335,4 +361,54 @@ public class RepoDetailsActivity extends ActionBarActivity {
).show();
}
+ public void showChangePasswordDialog(final View parentView) {
+
+ final View view = getLayoutInflater().inflate(R.layout.login, null);
+ final AlertDialog credentialsDialog = new AlertDialog.Builder(this).setView(view).create();
+ final EditText nameInput = (EditText) view.findViewById(R.id.edit_name);
+ final EditText passwordInput = (EditText) view.findViewById(R.id.edit_password);
+
+ nameInput.setText(repo.username);
+ passwordInput.requestFocus();
+
+ credentialsDialog.setTitle(R.string.repo_edit_credentials);
+ credentialsDialog.setButton(DialogInterface.BUTTON_NEGATIVE,
+ getString(R.string.cancel),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+
+ credentialsDialog.setButton(DialogInterface.BUTTON_POSITIVE,
+ getString(R.string.ok),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+
+ final String name = nameInput.getText().toString();
+ final String password = passwordInput.getText().toString();
+
+ if (name != null && !name.isEmpty()) {
+
+ final ContentValues values = new ContentValues(2);
+ values.put(RepoProvider.DataColumns.USERNAME, name);
+ values.put(RepoProvider.DataColumns.PASSWORD, password);
+
+ RepoProvider.Helper.update(RepoDetailsActivity.this, repo, values);
+
+ updateRepoView();
+
+ dialog.dismiss();
+
+ } else {
+
+ Toast.makeText(RepoDetailsActivity.this, R.string.repo_error_empty_username, Toast.LENGTH_LONG).show();
+ }
+ }
+ });
+
+ credentialsDialog.show();
+ }
}