BobStore/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java
Peter Serwylo 5065c37e13 Fix #263 "cannot manually add repo that was swapped before"
Pretends that the swap repo never existed, by deleting it before adding
the new repo, and showing the same message that is shown when a new
repo is added. This does not change behaviour for existing non-swap
repos. They are not deleted before being added again, or else we would
lose the ability to verify the fingerprint of an existing repo is the
same as a newly added one with the same URL.

Note that this has the effect that the fingerprint/pubkey of the swap
repo is nuked when adding that repo manually.

Internationalised the string "BAD FINGERPRINT" while I was at it.
2015-05-20 08:26:57 +10:00

908 lines
37 KiB
Java

/*
* Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com
* Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt
*
* 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.views;
import android.app.AlertDialog;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.ColorStateList;
import android.database.Cursor;
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.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.app.NavUtils;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.ActionBarActivity;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
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.Preferences;
import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.compat.ClipboardCompat;
import org.fdroid.fdroid.data.NewRepoConfig;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.net.MDnsHelper;
import org.fdroid.fdroid.net.MDnsHelper.DiscoveredRepo;
import org.fdroid.fdroid.net.MDnsHelper.RepoScanListAdapter;
import org.fdroid.fdroid.views.fragments.RepoDetailsFragment;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Date;
import java.util.Locale;
import javax.jmdns.ServiceInfo;
public class ManageReposActivity extends ActionBarActivity {
/**
* If we have a new repo added, or the address of a repo has changed, then
* we when we're finished, we'll set this boolean to true in the intent that
* we finish with, to signify that we want the main list of apps updated.
*/
public static final String REQUEST_UPDATE = "update";
private static final String TAG = "ManageReposActivity";
private static final String DEFAULT_NEW_REPO_TEXT = "https://";
private enum AddRepoState {
DOESNT_EXIST, EXISTS_FINGERPRINT_MISMATCH, EXISTS_FINGERPRINT_MATCH,
EXISTS_DISABLED, EXISTS_ENABLED, EXISTS_UPGRADABLE_TO_SIGNED, INVALID_URL,
IS_SWAP
}
private UpdateService.UpdateReceiver updateHandler = null;
private static boolean changed = false;
private RepoListFragment listFragment;
/**
* True if activity started with an intent such as from QR code. False if
* opened from, e.g. the main menu.
*/
private boolean isImportingRepo = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
((FDroidApp) getApplication()).applyTheme(this);
super.onCreate(savedInstanceState);
FragmentManager fm = getSupportFragmentManager();
if (fm.findFragmentById(android.R.id.content) == null) {
/*
* Need to set a dummy view (which will get overridden by the
* fragment manager below) so that we can call setContentView().
* This is a work around for a (bug?) thing in 3.0, 3.1 which
* requires setContentView to be invoked before the actionbar is
* played with:
* http://blog.perpetumdesign.com/2011/08/strange-case-of
* -dr-action-and-mr-bar.html
*/
if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT <= 13) {
setContentView(new LinearLayout(this));
}
listFragment = new RepoListFragment();
fm.beginTransaction()
.add(android.R.id.content, listFragment)
.commit();
}
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// title is "Repositories" here, but "F-Droid" in VIEW Intent chooser
getSupportActionBar().setTitle(R.string.menu_manage);
}
@Override
protected void onResume() {
super.onResume();
if (updateHandler != null) {
updateHandler.showDialog(this);
}
/* let's see if someone is trying to send us a new repo */
addRepoFromIntent(getIntent());
}
@Override
protected void onPause() {
super.onPause();
if (updateHandler != null) {
updateHandler.hideDialog();
}
}
@Override
protected void onNewIntent(Intent intent) {
setIntent(intent);
}
@Override
public void finish() {
Intent ret = new Intent();
markChangedIfRequired(ret);
setResult(RESULT_OK, ret);
super.finish();
}
private boolean hasChanged() {
return changed;
}
private void markChangedIfRequired(Intent intent) {
if (hasChanged()) {
Log.i(TAG, "Repo details have changed, prompting for update.");
intent.putExtra(REQUEST_UPDATE, true);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.manage_repos, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
Intent destIntent = new Intent(this, FDroid.class);
markChangedIfRequired(destIntent);
setResult(RESULT_OK, destIntent);
NavUtils.navigateUpTo(this, destIntent);
return true;
case R.id.action_add_repo:
showAddRepo();
return true;
case R.id.action_update_repo:
updateRepos();
return true;
case R.id.action_find_local_repos:
scanForRepos();
return true;
}
return super.onOptionsItemSelected(item);
}
private void updateRepos() {
updateHandler = UpdateService.updateNow(this).setListener(
new ProgressListener() {
@Override
public void onProgress(Event event) {
switch (event.type) {
case UpdateService.EVENT_COMPLETE_AND_SAME:
case UpdateService.EVENT_COMPLETE_WITH_CHANGES:
// No need to prompt to update any more, we just
// did it!
changed = false;
break;
case UpdateService.EVENT_FINISHED:
updateHandler = null;
break;
}
}
});
}
private void scanForRepos() {
final RepoScanListAdapter adapter = new RepoScanListAdapter(this);
final MDnsHelper mDnsHelper = new MDnsHelper(this, adapter);
final View view = getLayoutInflater().inflate(R.layout.repodiscoverylist, null);
final ListView repoScanList = (ListView) view.findViewById(R.id.reposcanlist);
final AlertDialog alrt = new AlertDialog.Builder(this).setView(view)
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mDnsHelper.stopDiscovery();
dialog.dismiss();
}
}).create();
alrt.setTitle(R.string.local_repos_title);
alrt.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
mDnsHelper.stopDiscovery();
}
});
repoScanList.setAdapter(adapter);
repoScanList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, final View view,
int position, long id) {
final DiscoveredRepo discoveredService =
(DiscoveredRepo) parent.getItemAtPosition(position);
final ServiceInfo serviceInfo = discoveredService.getServiceInfo();
String type = serviceInfo.getPropertyString("type");
String protocol = type.contains("fdroidrepos") ? "https:/" : "http:/";
String path = serviceInfo.getPropertyString("path");
if (TextUtils.isEmpty(path))
path = "/fdroid/repo";
String serviceUrl = protocol + serviceInfo.getInetAddresses()[0]
+ ":" + serviceInfo.getPort() + path;
showAddRepo(serviceUrl, serviceInfo.getPropertyString("fingerprint"));
}
});
alrt.show();
mDnsHelper.discoverServices();
}
private void showAddRepo() {
/*
* If there is text in the clipboard, and it looks like a URL, use that.
* Otherwise use "https://" as default repo string.
*/
ClipboardCompat clipboard = ClipboardCompat.create(this);
String text = clipboard.getText();
String fingerprint = null;
if (!TextUtils.isEmpty(text)) {
try {
new URL(text);
Uri uri = Uri.parse(text);
fingerprint = uri.getQueryParameter("fingerprint");
// uri might contain a QR-style, all uppercase URL:
if (TextUtils.isEmpty(fingerprint))
fingerprint = uri.getQueryParameter("FINGERPRINT");
text = NewRepoConfig.sanitizeRepoUri(uri);
} catch (MalformedURLException e) {
text = null;
}
}
if (TextUtils.isEmpty(text)) {
text = DEFAULT_NEW_REPO_TEXT;
}
showAddRepo(text, fingerprint);
}
private void showAddRepo(String newAddress, String newFingerprint) {
new AddRepo(newAddress, newFingerprint);
}
/**
* Utility class to encapsulate the process of adding a new repo (or an existing one,
* depending on if the incoming address is the same as a previous repo). It is responsible
* for managing the lifecycle of adding a repo:
* * Showing the add dialog
* * Deciding whether to add a new repo or update an existing one
* * Search for repos at common suffixes (/, /fdroid/repo, /repo)
*/
private class AddRepo {
private final Context context;
private final AlertDialog addRepoDialog;
private final TextView overwriteMessage;
private final ColorStateList defaultTextColour;
private final Button addButton;
private AddRepoState addRepoState;
public AddRepo(String newAddress, String newFingerprint) {
context = ManageReposActivity.this;
final View view = getLayoutInflater().inflate(R.layout.addrepo, null);
addRepoDialog = new AlertDialog.Builder(context).setView(view).create();
final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri);
final EditText fingerprintEditText = (EditText) view.findViewById(R.id.edit_fingerprint);
addRepoDialog.setIcon(android.R.drawable.ic_menu_add);
addRepoDialog.setTitle(getString(R.string.repo_add_title));
addRepoDialog.setButton(DialogInterface.BUTTON_NEGATIVE,
getString(R.string.cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
// HACK:
// After adding a new repo, need to show feedback to the user.
// This could use either a fresh dialog with some status messages,
// or modify the existing one. Either way is hard with the default API.
// A fresh dialog is impossible until after the dialog is dismissed,
// which happens after calling our OnClickListener. Thus we'd have to
// remember which button was pressed, wait for the dialog to be dismissed,
// then create a new one.
// Editing the existing dialog is preferable, but the dialog is dismissed
// after our onclick listener. We don't want this, we want the dialog to
// hang around so we can show further info on it.
//
// Thus, the hack described at http://stackoverflow.com/a/15619098 is implemented.
addRepoDialog.setButton(DialogInterface.BUTTON_POSITIVE,
getString(R.string.repo_add_add),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
addRepoDialog.show();
// This must be *after* addRepoDialog.show() otherwise getButtion() returns null:
// https://code.google.com/p/android/issues/detail?id=6360
addRepoDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
String fp = fingerprintEditText.getText().toString();
String url = uriEditText.getText().toString();
try {
url = normalizeUrl(url);
} catch (URISyntaxException e) {
invalidUrl();
return;
}
switch(addRepoState) {
case DOESNT_EXIST:
prepareToCreateNewRepo(url, fp);
break;
case IS_SWAP:
Log.i(TAG, "Removing existing swap repo " + url + " before adding new repo.");
Repo repo = RepoProvider.Helper.findByAddress(context, url);
RepoProvider.Helper.remove(context, repo.getId());
prepareToCreateNewRepo(url, fp);
break;
case EXISTS_DISABLED:
case EXISTS_UPGRADABLE_TO_SIGNED:
case EXISTS_FINGERPRINT_MATCH:
updateAndEnableExistingRepo(url, fp);
finishedAddingRepo();
break;
default:
finishedAddingRepo();
break;
}
}
}
);
addButton = addRepoDialog.getButton(DialogInterface.BUTTON_POSITIVE);
overwriteMessage = (TextView) view.findViewById(R.id.overwrite_message);
overwriteMessage.setVisibility(View.GONE);
defaultTextColour = overwriteMessage.getTextColors();
if (newFingerprint != null) {
fingerprintEditText.setText(newFingerprint);
}
if (newAddress != null) {
// This trick of emptying text then appending, rather than just setting in
// the first place, is necessary to move the cursor to the end of the input.
uriEditText.setText("");
uriEditText.append(newAddress);
}
final TextWatcher textChangedListener = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
validateRepoDetails(uriEditText.getText().toString(), fingerprintEditText.getText().toString());
}
};
uriEditText.addTextChangedListener(textChangedListener);
fingerprintEditText.addTextChangedListener(textChangedListener);
validateRepoDetails(newAddress == null ? "" : newAddress, newFingerprint == null ? "" : newFingerprint);
}
/**
* Compare the repo and the fingerprint against existing repositories, to see if this
* repo matches and display a relevant message to the user if that is the case.
*/
private void validateRepoDetails(@NonNull String uri, @NonNull String fingerprint) {
try {
uri = 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
// to the user until they try to save the repo.
}
final Repo repo = uri.length() > 0 ? RepoProvider.Helper.findByAddress(context, uri) : null;
if (repo == null) {
repoDoesntExist();
} else {
if (repo.isSwap) {
repoIsSwap();
} else if (repo.fingerprint == null && fingerprint.length() > 0) {
upgradingToSigned();
} else if (repo.fingerprint != null && !repo.fingerprint.equalsIgnoreCase(fingerprint)) {
repoFingerprintDoesntMatch();
} else {
// Could be either an unsigned repo, and no fingerprint was supplied,
// or it could be a signed repo with a matching fingerprint.
if (repo.inuse) {
repoExistsAndEnabled();
} else {
repoExistsAndDisabled();
}
}
}
}
private void repoDoesntExist() {
updateUi(AddRepoState.DOESNT_EXIST, 0, false, R.string.repo_add_add, true);
}
private void repoIsSwap() {
updateUi(AddRepoState.IS_SWAP, 0, false, R.string.repo_add_add, true);
}
/**
* Same address with different fingerprint, this could be malicious, so display a message
* force the user to manually delete the repo before adding this one.
*/
private void repoFingerprintDoesntMatch() {
updateUi(AddRepoState.EXISTS_FINGERPRINT_MISMATCH, R.string.repo_delete_to_overwrite,
true, R.string.overwrite, false);
}
private void invalidUrl() {
updateUi(AddRepoState.INVALID_URL, R.string.invalid_url, true,
R.string.repo_add_add, false);
}
private void repoExistsAndDisabled() {
updateUi(AddRepoState.EXISTS_DISABLED,
R.string.repo_exists_enable, false, R.string.enable, true);
}
private void repoExistsAndEnabled() {
updateUi(AddRepoState.EXISTS_ENABLED, R.string.repo_exists_and_enabled, false,
R.string.ok, true);
}
private void upgradingToSigned() {
updateUi(AddRepoState.EXISTS_UPGRADABLE_TO_SIGNED, R.string.repo_exists_add_fingerprint,
false, R.string.add_key, true);
}
private void updateUi(AddRepoState state, int messageRes, boolean redMessage, int addTextRes, boolean addEnabled) {
if (addRepoState != state) {
addRepoState = state;
if (messageRes > 0) {
overwriteMessage.setText(messageRes);
overwriteMessage.setVisibility(View.VISIBLE);
if (redMessage) {
overwriteMessage.setTextColor(getResources().getColor(R.color.red));
} else {
overwriteMessage.setTextColor(defaultTextColour);
}
} else {
overwriteMessage.setVisibility(View.GONE);
}
addButton.setText(addTextRes);
addButton.setEnabled(addEnabled);
}
}
/**
* Adds a new repo to the database.
*/
private void prepareToCreateNewRepo(final String originalAddress, final String fingerprint) {
addRepoDialog.findViewById(R.id.add_repo_form).setVisibility(View.GONE);
addRepoDialog.getButton(AlertDialog.BUTTON_POSITIVE).setVisibility(View.GONE);
final TextView textSearching = (TextView) addRepoDialog.findViewById(R.id.text_searching_for_repo);
textSearching.setText(getString(R.string.repo_searching_address, originalAddress));
final AsyncTask<String, String, String> checker = new AsyncTask<String, String, String>() {
@Override
protected String doInBackground(String... params) {
final String originalAddress = params[0];
final String[] pathsToCheck = {"", "fdroid/repo", "repo"};
for (final String path : pathsToCheck) {
Log.d(TAG, "Checking for repo at " + originalAddress + " with suffix \"" + path + "\".");
Uri.Builder builder = Uri.parse(originalAddress).buildUpon().appendEncodedPath(path);
final String addressWithoutIndex = builder.build().toString();
publishProgress(addressWithoutIndex);
final Uri uri = builder.appendPath("index.jar").build();
try {
if (checkForRepository(uri)) {
Log.i(TAG, "Found F-Droid repo at " + addressWithoutIndex);
return addressWithoutIndex;
}
} catch (IOException e) {
Log.e(TAG, "Error while searching for repo at " + addressWithoutIndex + ": " + e.getMessage());
return originalAddress;
}
if (isCancelled()) {
Log.d(TAG, "Not checking any more repo addresses, because process was skipped.");
break;
}
}
return originalAddress;
}
private boolean checkForRepository(Uri indexUri) throws IOException {
HttpClient client = new DefaultHttpClient();
HttpHead head = new HttpHead(indexUri.toString());
return client.execute(head).getStatusLine().getStatusCode() == 200;
}
@Override
protected void onProgressUpdate(String... values) {
String address = values[0];
textSearching.setText(getString(R.string.repo_searching_address, address));
}
@Override
protected void onPostExecute(String newAddress) {
if (addRepoDialog.isShowing()) {
createNewRepo(newAddress, fingerprint);
}
}
};
Button skip = addRepoDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
skip.setText(getString(R.string.skip));
skip.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Still proceed with adding the repo, just don't bother searching for
// a better alternative than the one provided.
// The reason for this is that if they are not connected to the internet,
// or their internet is playing up, then you'd have to wait for several
// connection timeouts before being able to proceed.
createNewRepo(originalAddress, fingerprint);
checker.cancel(false);
}
});
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.
*
* 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.
*/
private String normalizeUrl(String urlString) throws URISyntaxException {
URI uri = new URI(urlString);
if (!uri.isAbsolute()) {
throw new URISyntaxException(urlString, "Must provide an absolute URI for repositories");
}
uri = uri.normalize();
String path = uri.getPath().replaceAll("//*/", "/"); // Collapse multiple forward slashes into 1.
if (path.length() > 0 && path.charAt(path.length() - 1) == '/') {
path = path.substring(0, path.length() - 1);
}
return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),
path, uri.getQuery(), uri.getFragment()).toString();
}
private void createNewRepo(String address, String fingerprint) {
try {
address = normalizeUrl(address);
} catch (URISyntaxException e) {
// Leave address as it was.
}
ContentValues values = new ContentValues(2);
values.put(RepoProvider.DataColumns.ADDRESS, address);
if (fingerprint != null && fingerprint.length() > 0) {
values.put(RepoProvider.DataColumns.FINGERPRINT, fingerprint.toUpperCase(Locale.ENGLISH));
}
RepoProvider.Helper.insert(context, values);
finishedAddingRepo();
Toast.makeText(ManageReposActivity.this, getString(R.string.repo_added, address), Toast.LENGTH_SHORT).show();
}
/**
* Seeing as this repo already exists, we will force it to be enabled again.
*/
private void updateAndEnableExistingRepo(String url, String fingerprint) {
if (fingerprint != null) {
fingerprint = fingerprint.trim();
if (fingerprint.length() == 0) {
fingerprint = null;
} else {
fingerprint = fingerprint.toUpperCase(Locale.ENGLISH);
}
}
Log.d(TAG, "Enabling existing repo: " + url);
Repo repo = RepoProvider.Helper.findByAddress(context, url);
ContentValues values = new ContentValues(2);
values.put(RepoProvider.DataColumns.IN_USE, 1);
values.put(RepoProvider.DataColumns.FINGERPRINT, fingerprint);
RepoProvider.Helper.update(context, repo, values);
listFragment.notifyDataSetChanged();
finishedAddingRepo();
}
/**
* If started by an intent that expects a result (e.g. QR codes) then we
* will set a result and finish. Otherwise, we'll refresh the list of repos
* to reflect the newly created repo.
*/
private void finishedAddingRepo() {
changed = true;
if (addRepoDialog.isShowing()) {
addRepoDialog.dismiss();
}
if (isImportingRepo) {
setResult(RESULT_OK);
finish();
}
}
}
private void addRepoFromIntent(Intent intent) {
/* an URL from a click, NFC, QRCode scan, etc */
NewRepoConfig newRepoConfig = new NewRepoConfig(this, intent);
if (newRepoConfig.isValidRepo()) {
isImportingRepo = true;
showAddRepo(newRepoConfig.getRepoUriString(), newRepoConfig.getFingerprint());
checkIfNewRepoOnSameWifi(newRepoConfig);
} else if (newRepoConfig.getErrorMessage() != null) {
Toast.makeText(this, newRepoConfig.getErrorMessage(), Toast.LENGTH_LONG).show();
}
}
private void checkIfNewRepoOnSameWifi(NewRepoConfig newRepo) {
// if this is a local repo, check we're on the same wifi
if (!TextUtils.isEmpty(newRepo.getBssid())) {
WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
String bssid = wifiInfo.getBSSID();
if (TextUtils.isEmpty(bssid)) /* not all devices have wifi */
return;
bssid = bssid.toLowerCase(Locale.ENGLISH);
String newRepoBssid = Uri.decode(newRepo.getBssid()).toLowerCase(Locale.ENGLISH);
if (!bssid.equals(newRepoBssid)) {
String msg = String.format(getString(R.string.not_on_same_wifi), newRepo.getSsid());
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
}
// TODO we should help the user to the right thing here,
// instead of just showing a message!
}
}
public static class RepoListFragment extends ListFragment
implements LoaderManager.LoaderCallbacks<Cursor>, RepoAdapter.EnabledListener {
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri = RepoProvider.allExceptSwapUri();
Log.i(TAG, "Creating repo loader '" + uri + "'.");
final String[] projection = {
RepoProvider.DataColumns._ID,
RepoProvider.DataColumns.NAME,
RepoProvider.DataColumns.PUBLIC_KEY,
RepoProvider.DataColumns.FINGERPRINT,
RepoProvider.DataColumns.IN_USE
};
return new CursorLoader(getActivity(), uri, projection, null, null, null);
}
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
repoAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
repoAdapter.swapCursor(null);
}
/**
* NOTE: If somebody toggles a repo off then on again, it will have
* removed all apps from the index when it was toggled off, so when it
* is toggled on again, then it will require a refresh. Previously, I
* toyed with the idea of remembering whether they had toggled on or
* off, and then only actually performing the function when the activity
* stopped, but I think that will be problematic. What about when they
* press the home button, or edit a repos details? It will start to
* become somewhat-random as to when the actual enabling, disabling is
* performed. So now, it just does the disable as soon as the user
* clicks "Off" and then removes the apps. To compensate for the removal
* of apps from index, it notifies the user via a toast that the apps
* have been removed. Also, as before, it will still prompt the user to
* update the repos if you toggled on on.
*/
@Override
public void onSetEnabled(Repo repo, boolean isEnabled) {
if (repo.inuse != isEnabled) {
ContentValues values = new ContentValues(1);
values.put(RepoProvider.DataColumns.IN_USE, isEnabled ? 1 : 0);
RepoProvider.Helper.update(getActivity(), repo, values);
if (isEnabled) {
changed = true;
} else {
FDroidApp app = (FDroidApp) getActivity().getApplication();
RepoProvider.Helper.purgeApps(getActivity(), repo, app);
String notification = getString(R.string.repo_disabled_notification, repo.name);
Toast.makeText(getActivity(), notification, Toast.LENGTH_LONG).show();
}
}
}
private RepoAdapter repoAdapter;
private View createHeaderView() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
TextView textLastUpdate = new TextView(getActivity());
long lastUpdate = prefs.getLong(Preferences.PREF_UPD_LAST, 0);
String lastUpdateCheck;
if (lastUpdate == 0) {
lastUpdateCheck = getString(R.string.never);
} else {
Date d = new Date(lastUpdate);
lastUpdateCheck = DateFormat.getDateFormat(getActivity()).format(d) +
" " + DateFormat.getTimeFormat(getActivity()).format(d);
}
textLastUpdate.setText(getString(R.string.last_update_check, lastUpdateCheck));
int sidePadding = (int) getResources().getDimension(R.dimen.padding_side);
int topPadding = (int) getResources().getDimension(R.dimen.padding_top);
textLastUpdate.setPadding(sidePadding, topPadding, sidePadding, topPadding);
return textLastUpdate;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getListAdapter() == null) {
/*
* Can't do this in the onCreate view, because "onCreateView"
* which returns the list view is "called between onCreate and
* onActivityCreated" according to the docs.
*/
getListView().addHeaderView(createHeaderView(), null, false);
/*
* This could go in onCreate (and used to) but it needs to be
* called after addHeaderView, which can only be called after
* onCreate...
*/
repoAdapter = new RepoAdapter(getActivity(), null);
repoAdapter.setEnabledListener(this);
setListAdapter(repoAdapter);
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
setHasOptionsMenu(true);
}
@Override
public void onResume() {
super.onResume();
// Starts a new or restarts an existing Loader in this manager
getLoaderManager().restartLoader(0, null, this);
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
Repo repo = new Repo((Cursor) getListView().getItemAtPosition(position));
editRepo(repo);
}
public static final int SHOW_REPO_DETAILS = 1;
public void editRepo(Repo repo) {
Intent intent = new Intent(getActivity(), RepoDetailsActivity.class);
intent.putExtra(RepoDetailsFragment.ARG_REPO_ID, repo.getId());
startActivityForResult(intent, SHOW_REPO_DETAILS);
}
/**
* This is necessary because even though the list will listen to content changes
* in the RepoProvider, it doesn't update the list items if they are changed (but not
* added or removed. The example which made this necessary was enabling an existing
* repo, and wanting the switch to be changed to on).
*/
private void notifyDataSetChanged() {
getLoaderManager().restartLoader(0, null, this);
}
}
}