/*
* 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.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
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.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.support.v7.app.AlertDialog;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
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.fdroid.fdroid.FDroid;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.ClipboardCompat;
import org.fdroid.fdroid.compat.CursorAdapterCompat;
import org.fdroid.fdroid.data.NewRepoConfig;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.Schema.RepoTable;
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;
public class ManageReposActivity extends ActionBarActivity {
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 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;
@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();
FDroidApp.checkStartTor(this);
/* let's see if someone is trying to send us a new repo */
addRepoFromIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
setIntent(intent);
}
@Override
public void finish() {
Intent ret = new Intent();
setResult(RESULT_OK, ret);
super.finish();
}
@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);
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:
UpdateService.updateNow(this);
return true;
}
return super.onOptionsItemSelected(item);
}
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;
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.setTitle(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 url = uriEditText.getText().toString();
try {
url = normalizeUrl(url);
} catch (URISyntaxException e) {
invalidUrl();
return;
}
String fp = fingerprintEditText.getText().toString();
// remove any whitespace from fingerprint
fp = fp.replaceAll("\\s", "");
switch (addRepoState) {
case DOESNT_EXIST:
prepareToCreateNewRepo(url, fp);
break;
case IS_SWAP:
Utils.debugLog(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 = !TextUtils.isEmpty(uri) ? 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>() {
private int statusCode = -1;
@Override
protected String doInBackground(String... params) {
final String originalAddress = params[0];
final String[] pathsToCheck = {"", "fdroid/repo", "repo"};
for (final String path : pathsToCheck) {
Utils.debugLog(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)) {
Utils.debugLog(TAG, "Found F-Droid repo at " + addressWithoutIndex);
return addressWithoutIndex;
}
} catch (IOException e) {
Log.e(TAG, "Error while searching for repo at " + addressWithoutIndex, e);
return originalAddress;
}
if (isCancelled()) {
Utils.debugLog(TAG, "Not checking any more repo addresses, because process was skipped.");
break;
}
}
return originalAddress;
}
private boolean checkForRepository(Uri indexUri) throws IOException {
final URL url = new URL(indexUri.toString());
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("HEAD");
statusCode = connection.getResponseCode();
return statusCode == 401 || statusCode == 200;
}
@Override
protected void onProgressUpdate(String... values) {
String address = values[0];
textSearching.setText(getString(R.string.repo_searching_address, address));
}
@Override
protected void onPostExecute(final String newAddress) {
if (addRepoDialog.isShowing()) {
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);
}
}
}
};
Button skip = addRepoDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
skip.setText(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 {
if (urlString == null) {
return null;
}
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();
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);
}
}
return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),
path, uri.getQuery(), uri.getFragment()).toString();
}
/**
* Create a repository without a username or password.
*/
private void createNewRepo(String address, String fingerprint) {
createNewRepo(address, fingerprint, null, null);
}
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(4);
values.put(RepoTable.Cols.ADDRESS, address);
if (!TextUtils.isEmpty(fingerprint)) {
values.put(RepoTable.Cols.FINGERPRINT, fingerprint.toUpperCase(Locale.ENGLISH));
}
if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) {
values.put(RepoTable.Cols.USERNAME, username);
values.put(RepoTable.Cols.PASSWORD, password);
}
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 (TextUtils.isEmpty(fingerprint)) {
fingerprint = null;
} else {
fingerprint = fingerprint.toUpperCase(Locale.ENGLISH);
}
}
Utils.debugLog(TAG, "Enabling existing repo: " + url);
Repo repo = RepoProvider.Helper.findByAddress(context, url);
ContentValues values = new ContentValues(2);
values.put(RepoTable.Cols.IN_USE, 1);
values.put(RepoTable.Cols.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 updateViews the list of repos
* to reflect the newly created repo.
*/
private void finishedAddingRepo() {
UpdateService.updateNow(ManageReposActivity.this);
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 {
private RepoAdapter repoAdapter;
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri = RepoProvider.allExceptSwapUri();
Utils.debugLog(TAG, "Creating repo loader '" + uri + "'.");
final String[] projection = {
RepoTable.Cols._ID,
RepoTable.Cols.NAME,
RepoTable.Cols.SIGNING_CERT,
RepoTable.Cols.FINGERPRINT,
RepoTable.Cols.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 updateViews. 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(RepoTable.Cols.IN_USE, isEnabled ? 1 : 0);
RepoProvider.Helper.update(getActivity(), repo, values);
if (isEnabled) {
UpdateService.updateNow(getActivity());
} else {
RepoProvider.Helper.purgeApps(getActivity(), repo);
String notification = getString(R.string.repo_disabled_notification, repo.name);
Toast.makeText(getActivity(), notification, Toast.LENGTH_LONG).show();
}
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
setHasOptionsMenu(true);
repoAdapter = RepoAdapter.create(getActivity(), null, CursorAdapterCompat.FLAG_AUTO_REQUERY);
repoAdapter.setEnabledListener(this);
setListAdapter(repoAdapter);
}
@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(RepoDetailsActivity.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);
}
}
}