Java tutorial
/* * Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com * * 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 id.ridon.keude; import android.app.AlarmManager; import android.app.IntentService; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.ProgressDialog; import android.content.ContentProviderOperation; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.OperationApplicationException; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.database.Cursor; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; import id.ridon.keude.data.Apk; import id.ridon.keude.data.ApkProvider; import id.ridon.keude.data.App; import id.ridon.keude.data.AppProvider; import id.ridon.keude.data.Repo; import id.ridon.keude.data.RepoProvider; import id.ridon.keude.net.Downloader; import id.ridon.keude.updater.RepoUpdater; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; public class UpdateService extends IntentService implements ProgressListener { public static final String RESULT_MESSAGE = "msg"; public static final String RESULT_EVENT = "event"; public static final String RESULT_REPO_ERRORS = "repoErrors"; public static final int STATUS_COMPLETE_WITH_CHANGES = 0; public static final int STATUS_COMPLETE_AND_SAME = 1; public static final int STATUS_ERROR_GLOBAL = 2; public static final int STATUS_ERROR_LOCAL = 3; public static final int STATUS_ERROR_LOCAL_SMALL = 4; public static final int STATUS_INFO = 5; // I don't like that I've had to dupliacte the statuses above with strings here, however // one method of communication/notification is using ResultReceiver (int status codes) // while the other uses progress events (string event types). public static final String EVENT_COMPLETE_WITH_CHANGES = "repoUpdateComplete (changed)"; public static final String EVENT_COMPLETE_AND_SAME = "repoUpdateComplete (not changed)"; public static final String EVENT_FINISHED = "repoUpdateFinished"; public static final String EVENT_ERROR = "repoUpdateError"; public static final String EVENT_INFO = "repoUpdateInfo"; public static final String EXTRA_RECEIVER = "receiver"; public static final String EXTRA_ADDRESS = "address"; private ResultReceiver receiver = null; public UpdateService() { super("UpdateService"); } /** * When an app already exists in the db, and we are updating it on the off chance that some * values changed in the index, some fields should not be updated. Rather, they should be * ignored, because they were explicitly set by the user, and hence can't be automatically * overridden by the index. * * NOTE: In the future, these attributes will be moved to a join table, so that the app table * is essentially completely transient, and can be nuked at any time. */ private static final String[] APP_FIELDS_TO_IGNORE = { AppProvider.DataColumns.IGNORE_ALLUPDATES, AppProvider.DataColumns.IGNORE_THISUPDATE }; // For receiving results from the UpdateService when we've told it to // update in response to a user request. public static class UpdateReceiver extends ResultReceiver { private Context context; private ProgressDialog dialog; private ProgressListener listener; private String lastShownMessage = null; public UpdateReceiver(Handler handler) { super(handler); } public UpdateReceiver setDialog(ProgressDialog dialog) { this.dialog = dialog; return this; } public UpdateReceiver setListener(ProgressListener listener) { this.listener = listener; return this; } private void forwardEvent(String type) { if (listener != null) { listener.onProgress(new Event(type)); } } private void ensureDialog() { if (dialog == null) { String title = context.getString(R.string.process_wait_title); String message = lastShownMessage == null ? context.getString(R.string.process_update_msg) : lastShownMessage; dialog = ProgressDialog.show(context, title, message, true, true); dialog.setIcon(android.R.drawable.ic_dialog_info); dialog.setCanceledOnTouchOutside(false); } } public UpdateReceiver showDialog(Context context) { this.context = context; ensureDialog(); dialog.show(); return this; } public void hideDialog() { dialog.hide(); dialog.dismiss(); dialog = null; } @Override protected void onReceiveResult(int resultCode, Bundle resultData) { final String message = resultData.getString(UpdateService.RESULT_MESSAGE); boolean finished = false; if (resultCode == UpdateService.STATUS_ERROR_GLOBAL) { forwardEvent(EVENT_ERROR); Toast.makeText(context, context.getString(R.string.global_error_updating_repos) + " " + message, Toast.LENGTH_LONG).show(); finished = true; } else if (resultCode == UpdateService.STATUS_ERROR_LOCAL || resultCode == UpdateService.STATUS_ERROR_LOCAL_SMALL) { StringBuilder msgB = new StringBuilder(); ArrayList<CharSequence> repoErrors = resultData .getCharSequenceArrayList(UpdateService.RESULT_REPO_ERRORS); for (CharSequence error : repoErrors) { if (msgB.length() > 0) msgB.append('\n'); msgB.append(error); } if (resultCode == UpdateService.STATUS_ERROR_LOCAL_SMALL) { msgB.append("\n").append(context.getString(R.string.all_other_repos_fine)); } Toast.makeText(context, msgB.toString(), Toast.LENGTH_LONG).show(); finished = true; } else if (resultCode == UpdateService.STATUS_COMPLETE_WITH_CHANGES) { forwardEvent(EVENT_COMPLETE_WITH_CHANGES); finished = true; } else if (resultCode == UpdateService.STATUS_COMPLETE_AND_SAME) { forwardEvent(EVENT_COMPLETE_AND_SAME); Toast.makeText(context, context.getString(R.string.repos_unchanged), Toast.LENGTH_LONG).show(); finished = true; } else if (resultCode == UpdateService.STATUS_INFO) { forwardEvent(EVENT_INFO); if (dialog != null) { lastShownMessage = message; dialog.setMessage(message); } } if (finished) { forwardEvent(EVENT_FINISHED); if (dialog != null && dialog.isShowing()) { try { dialog.dismiss(); } catch (IllegalArgumentException e) { // sometimes dialog.isShowing() doesn't work :( // https://stackoverflow.com/questions/19538282/view-not-attached-to-window-manager-dialog-dismiss e.printStackTrace(); } } } } } public static UpdateReceiver updateNow(Context context) { return updateRepoNow(null, context); } public static UpdateReceiver updateRepoNow(String address, Context context) { Intent intent = new Intent(context, UpdateService.class); UpdateReceiver receiver = new UpdateReceiver(new Handler()); receiver.showDialog(context); intent.putExtra(EXTRA_RECEIVER, receiver); if (!TextUtils.isEmpty(address)) { intent.putExtra(EXTRA_ADDRESS, address); } context.startService(intent); return receiver; } // Schedule (or cancel schedule for) this service, according to the // current preferences. Should be called a) at boot, b) if the preference // is changed, or c) on startup, in case we get upgraded. public static void schedule(Context ctx) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); String sint = prefs.getString(Preferences.PREF_UPD_INTERVAL, "0"); int interval = Integer.parseInt(sint); Intent intent = new Intent(ctx, UpdateService.class); PendingIntent pending = PendingIntent.getService(ctx, 0, intent, 0); AlarmManager alarm = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE); alarm.cancel(pending); if (interval > 0) { alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 5000, AlarmManager.INTERVAL_HOUR, pending); Log.d("Keude", "Update scheduler alarm set"); } else { Log.d("Keude", "Update scheduler alarm not set"); } } protected void sendStatus(int statusCode) { sendStatus(statusCode, null); } protected void sendStatus(int statusCode, String message) { if (receiver != null) { Bundle resultData = new Bundle(); if (message != null && message.length() > 0) resultData.putString(RESULT_MESSAGE, message); receiver.send(statusCode, resultData); } } protected void sendRepoErrorStatus(int statusCode, ArrayList<CharSequence> repoErrors) { if (receiver != null) { Bundle resultData = new Bundle(); resultData.putCharSequenceArrayList(RESULT_REPO_ERRORS, repoErrors); receiver.send(statusCode, resultData); } } /** * We might be doing a scheduled run, or we might have been launched by the * app in response to a user's request. If we have a receiver, it's the * latter... */ private boolean isScheduledRun() { return receiver == null; } /** * Check whether it is time to run the scheduled update. * We don't want to run if: * - The time between scheduled runs is set to zero (though don't know * when that would occur) * - Last update was too recent * - Not on wifi, but the property for "Only auto update on wifi" is set. * @return True if we are due for a scheduled update. */ private boolean verifyIsTimeForScheduledRun() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); long lastUpdate = prefs.getLong(Preferences.PREF_UPD_LAST, 0); String sint = prefs.getString(Preferences.PREF_UPD_INTERVAL, "0"); int interval = Integer.parseInt(sint); if (interval == 0) { Log.d("Keude", "Skipping update - disabled"); return false; } long elapsed = System.currentTimeMillis() - lastUpdate; if (elapsed < interval * 60 * 60 * 1000) { Log.d("Keude", "Skipping update - done " + elapsed + "ms ago, interval is " + interval + " hours"); return false; } // If we are to update the repos only on wifi, make sure that // connection is active if (prefs.getBoolean(Preferences.PREF_UPD_WIFI_ONLY, false)) { ConnectivityManager conMan = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo.State wifi = conMan.getNetworkInfo(1).getState(); if (wifi != NetworkInfo.State.CONNECTED && wifi != NetworkInfo.State.CONNECTING) { Log.d("Keude", "Skipping update - wifi not available"); return false; } } return true; } @Override protected void onHandleIntent(Intent intent) { receiver = intent.getParcelableExtra(EXTRA_RECEIVER); String address = intent.getStringExtra(EXTRA_ADDRESS); long startTime = System.currentTimeMillis(); try { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); // See if it's time to actually do anything yet... if (!isScheduledRun()) { Log.d("Keude", "Unscheduled (manually requested) update"); } else if (!verifyIsTimeForScheduledRun()) { return; } // Grab some preliminary information, then we can release the // database while we do all the downloading, etc... List<Repo> repos = RepoProvider.Helper.all(this); // Process each repo... Map<String, App> appsToUpdate = new HashMap<String, App>(); List<Apk> apksToUpdate = new ArrayList<Apk>(); List<Repo> unchangedRepos = new ArrayList<Repo>(); List<Repo> updatedRepos = new ArrayList<Repo>(); List<Repo> disabledRepos = new ArrayList<Repo>(); ArrayList<CharSequence> errorRepos = new ArrayList<CharSequence>(); ArrayList<CharSequence> repoErrors = new ArrayList<CharSequence>(); List<RepoUpdater.RepoUpdateRememberer> repoUpdateRememberers = new ArrayList<RepoUpdater.RepoUpdateRememberer>(); boolean changes = false; for (final Repo repo : repos) { if (!repo.inuse) { disabledRepos.add(repo); continue; } else if (!TextUtils.isEmpty(address) && !repo.address.equals(address)) { unchangedRepos.add(repo); continue; } sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address)); RepoUpdater updater = RepoUpdater.createUpdaterFor(getBaseContext(), repo); updater.setProgressListener(this); try { updater.update(); if (updater.hasChanged()) { for (final App app : updater.getApps()) { appsToUpdate.put(app.id, app); } apksToUpdate.addAll(updater.getApks()); updatedRepos.add(repo); changes = true; repoUpdateRememberers.add(updater.getRememberer()); } else { unchangedRepos.add(repo); } } catch (RepoUpdater.UpdateException e) { errorRepos.add(repo.address); repoErrors.add(e.getMessage()); Log.e("Keude", "Error updating repository " + repo.address + ": " + e.getMessage()); Log.e("Keude", Log.getStackTraceString(e)); } } if (!changes) { Log.d("Keude", "Not checking app details or compatibility, because all repos were up to date."); } else { sendStatus(STATUS_INFO, getString(R.string.status_checking_compatibility)); List<App> listOfAppsToUpdate = new ArrayList<App>(); listOfAppsToUpdate.addAll(appsToUpdate.values()); calcApkCompatibilityFlags(this, apksToUpdate); // Need to do this BEFORE updating the apks, otherwise when it continually // calls "get existing apks for repo X" then it will be getting the newly // created apks, rather than those from the fresh, juicy index we just processed. removeApksNoLongerInRepo(apksToUpdate, updatedRepos); int totalInsertsUpdates = listOfAppsToUpdate.size() + apksToUpdate.size(); updateOrInsertApps(listOfAppsToUpdate, totalInsertsUpdates, 0); updateOrInsertApks(apksToUpdate, totalInsertsUpdates, listOfAppsToUpdate.size()); removeApksFromRepos(disabledRepos); removeAppsWithoutApks(); // This will sort out the icon urls, compatibility flags. and suggested version // for each app. It used to happen here in Java code, but was moved to SQL when // it became apparant we don't always have enough info (depending on which repos // were updated). AppProvider.Helper.calcDetailsFromIndex(this); notifyContentProviders(); //we only remember the update if everything has gone well for (RepoUpdater.RepoUpdateRememberer rememberer : repoUpdateRememberers) { rememberer.rememberUpdate(); } if (prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, true)) { performUpdateNotification(appsToUpdate.values()); } } Editor e = prefs.edit(); e.putLong(Preferences.PREF_UPD_LAST, System.currentTimeMillis()); e.commit(); if (errorRepos.isEmpty()) { if (changes) { sendStatus(STATUS_COMPLETE_WITH_CHANGES); } else { sendStatus(STATUS_COMPLETE_AND_SAME); } } else { if (updatedRepos.size() + unchangedRepos.size() == 0) { sendRepoErrorStatus(STATUS_ERROR_LOCAL, repoErrors); } else { sendRepoErrorStatus(STATUS_ERROR_LOCAL_SMALL, repoErrors); } } } catch (Exception e) { Log.e("Keude", "Exception during update processing:\n" + Log.getStackTraceString(e)); sendStatus(STATUS_ERROR_GLOBAL, e.getMessage()); } finally { Log.d("Keude", "Update took " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds."); receiver = null; } } private void notifyContentProviders() { getContentResolver().notifyChange(AppProvider.getContentUri(), null); getContentResolver().notifyChange(ApkProvider.getContentUri(), null); } /** * This cannot be offloaded to the database (as we did with the query which * updates apps, depending on whether their apks are compatible or not). * The reason is that we need to interact with the CompatibilityChecker * in order to see if, and why an apk is not compatible. */ private static void calcApkCompatibilityFlags(Context context, List<Apk> apks) { CompatibilityChecker checker = new CompatibilityChecker(context); for (Apk apk : apks) { List<String> reasons = checker.getIncompatibleReasons(apk); if (reasons.size() > 0) { apk.compatible = false; apk.incompatible_reasons = Utils.CommaSeparatedList.make(reasons); } else { apk.compatible = true; apk.incompatible_reasons = null; } } } private void performUpdateNotification(Collection<App> apps) { int count = AppProvider.Helper.count(this, AppProvider.getCanUpdateUri()); if (count > 0) { showAppUpdatesNotification(count); } } private void showAppUpdatesNotification(int updates) { Log.d("Keude", "Notifying " + updates + " updates."); NotificationCompat.Builder builder = new NotificationCompat.Builder(this).setAutoCancel(true) .setContentTitle(getString(R.string.fdroid_updates_available)); if (Build.VERSION.SDK_INT >= 11) { builder.setSmallIcon(R.drawable.ic_stat_notify_updates); } else { builder.setSmallIcon(R.drawable.ic_launcher); } Intent notifyIntent = new Intent(this, Keude.class).putExtra(Keude.EXTRA_TAB_UPDATE, true); if (updates > 1) { builder.setContentText(getString(R.string.many_updates_available, updates)); } else { builder.setContentText(getString(R.string.one_update_available)); } TaskStackBuilder stackBuilder = TaskStackBuilder.create(this).addParentStack(Keude.class) .addNextIntent(notifyIntent); PendingIntent pi = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(pi); NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(1, builder.build()); } private List<String> getKnownAppIds(List<App> apps) { List<String> knownAppIds = new ArrayList<String>(); if (apps.size() == 0) { // Do nothing } else if (apps.size() > AppProvider.MAX_APPS_TO_QUERY) { int middle = apps.size() / 2; List<App> apps1 = apps.subList(0, middle); List<App> apps2 = apps.subList(middle, apps.size()); knownAppIds.addAll(getKnownAppIds(apps1)); knownAppIds.addAll(getKnownAppIds(apps2)); } else { knownAppIds.addAll(getKnownAppIdsFromProvider(apps)); } return knownAppIds; } /** * Looks in the database to see which apps we already know about. Only * returns ids of apps that are in the database if they are in the "apps" * array. */ private List<String> getKnownAppIdsFromProvider(List<App> apps) { final Uri uri = AppProvider.getContentUri(apps); String[] fields = new String[] { AppProvider.DataColumns.APP_ID }; Cursor cursor = getContentResolver().query(uri, fields, null, null, null); int knownIdCount = cursor != null ? cursor.getCount() : 0; List<String> knownIds = new ArrayList<String>(knownIdCount); if (cursor != null) { if (knownIdCount > 0) { cursor.moveToFirst(); while (!cursor.isAfterLast()) { knownIds.add(cursor.getString(0)); cursor.moveToNext(); } } cursor.close(); } return knownIds; } /** * If you call this with too many apks, then it will likely hit limit of * parameters allowed for sqlite3 query. Rather, you should use * {@link id.ridon.keude.UpdateService#getKnownApks(java.util.List)} * instead, which will only call this with the right number of apks at * a time. * @see id.ridon.keude.UpdateService#getKnownAppIds(java.util.List) */ private List<Apk> getKnownApksFromProvider(List<Apk> apks) { String[] fields = { ApkProvider.DataColumns.APK_ID, ApkProvider.DataColumns.VERSION, ApkProvider.DataColumns.VERSION_CODE }; return ApkProvider.Helper.knownApks(this, apks, fields); } private void updateOrInsertApps(List<App> appsToUpdate, int totalUpdateCount, int currentCount) { ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(); List<String> knownAppIds = getKnownAppIds(appsToUpdate); for (final App a : appsToUpdate) { boolean known = false; for (String knownId : knownAppIds) { if (knownId.equals(a.id)) { known = true; break; } } if (known) { operations.add(updateExistingApp(a)); } else { operations.add(insertNewApp(a)); } } Log.d("Keude", "Updating/inserting " + operations.size() + " apps."); try { executeBatchWithStatus(AppProvider.getAuthority(), operations, currentCount, totalUpdateCount); } catch (RemoteException e) { Log.e("Keude", e.getMessage()); } catch (OperationApplicationException e) { Log.e("Keude", e.getMessage()); } } private void executeBatchWithStatus(String providerAuthority, ArrayList<ContentProviderOperation> operations, int currentCount, int totalUpdateCount) throws RemoteException, OperationApplicationException { int i = 0; while (i < operations.size()) { int count = Math.min(operations.size() - i, 100); ArrayList<ContentProviderOperation> o = new ArrayList<ContentProviderOperation>( operations.subList(i, i + count)); sendStatus(STATUS_INFO, getString(R.string.status_inserting, (int) ((double) (currentCount + i) / totalUpdateCount * 100))); getContentResolver().applyBatch(providerAuthority, o); i += 100; } } /** * Return list of apps from the "apks" argument which are already in the database. */ private List<Apk> getKnownApks(List<Apk> apks) { List<Apk> knownApks = new ArrayList<Apk>(); if (apks.size() > ApkProvider.MAX_APKS_TO_QUERY) { int middle = apks.size() / 2; List<Apk> apks1 = apks.subList(0, middle); List<Apk> apks2 = apks.subList(middle, apks.size()); knownApks.addAll(getKnownApks(apks1)); knownApks.addAll(getKnownApks(apks2)); } else { knownApks.addAll(getKnownApksFromProvider(apks)); } return knownApks; } private void updateOrInsertApks(List<Apk> apksToUpdate, int totalApksAppsCount, int currentCount) { ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(); List<Apk> knownApks = getKnownApks(apksToUpdate); for (final Apk apk : apksToUpdate) { boolean known = false; for (final Apk knownApk : knownApks) { if (knownApk.id.equals(apk.id) && knownApk.vercode == apk.vercode) { known = true; break; } } if (known) { operations.add(updateExistingApk(apk)); } else { operations.add(insertNewApk(apk)); knownApks.add(apk); // In case another repo has the same version/id combo for this apk. } } Log.d("Keude", "Updating/inserting " + operations.size() + " apks."); try { executeBatchWithStatus(ApkProvider.getAuthority(), operations, currentCount, totalApksAppsCount); } catch (RemoteException e) { Log.e("Keude", e.getMessage()); } catch (OperationApplicationException e) { Log.e("Keude", e.getMessage()); } } private ContentProviderOperation updateExistingApk(final Apk apk) { Uri uri = ApkProvider.getContentUri(apk); ContentValues values = apk.toContentValues(); return ContentProviderOperation.newUpdate(uri).withValues(values).build(); } private ContentProviderOperation insertNewApk(final Apk apk) { ContentValues values = apk.toContentValues(); Uri uri = ApkProvider.getContentUri(); return ContentProviderOperation.newInsert(uri).withValues(values).build(); } private ContentProviderOperation updateExistingApp(App app) { Uri uri = AppProvider.getContentUri(app); ContentValues values = app.toContentValues(); for (String toIgnore : APP_FIELDS_TO_IGNORE) { if (values.containsKey(toIgnore)) { values.remove(toIgnore); } } return ContentProviderOperation.newUpdate(uri).withValues(values).build(); } private ContentProviderOperation insertNewApp(App app) { ContentValues values = app.toContentValues(); Uri uri = AppProvider.getContentUri(); return ContentProviderOperation.newInsert(uri).withValues(values).build(); } /** * If a repo was updated (i.e. it is in use, and the index has changed * since last time we did an update), then we want to remove any apks that * belong to the repo which are not in the current list of apks that were * retrieved. */ private void removeApksNoLongerInRepo(List<Apk> apksToUpdate, List<Repo> updatedRepos) { long startTime = System.currentTimeMillis(); List<Apk> toRemove = new ArrayList<Apk>(); String[] fields = { ApkProvider.DataColumns.APK_ID, ApkProvider.DataColumns.VERSION_CODE, ApkProvider.DataColumns.VERSION, }; for (final Repo repo : updatedRepos) { final List<Apk> existingApks = ApkProvider.Helper.findByRepo(this, repo, fields); for (final Apk existingApk : existingApks) { if (!isApkToBeUpdated(existingApk, apksToUpdate)) { toRemove.add(existingApk); } } } long duration = System.currentTimeMillis() - startTime; Log.d("Keude", "Found " + toRemove.size() + " apks no longer in the updated repos (took " + duration + "ms)"); if (toRemove.size() > 0) { ApkProvider.Helper.deleteApks(this, toRemove); } } private static boolean isApkToBeUpdated(Apk existingApk, List<Apk> apksToUpdate) { for (final Apk apkToUpdate : apksToUpdate) { if (apkToUpdate.vercode == existingApk.vercode && apkToUpdate.id.equals(existingApk.id)) { return true; } } return false; } private void removeApksFromRepos(List<Repo> repos) { for (final Repo repo : repos) { Uri uri = ApkProvider.getRepoUri(repo.getId()); int numDeleted = getContentResolver().delete(uri, null, null); Log.d("Keude", "Removing " + numDeleted + " apks from repo " + repo.address); } } private void removeAppsWithoutApks() { int numDeleted = getContentResolver().delete(AppProvider.getNoApksUri(), null, null); Log.d("Keude", "Removing " + numDeleted + " apks that don't have any apks"); } /** * Received progress event from the RepoXMLHandler. It could be progress * downloading from the repo, or perhaps processing the info from the repo. */ @Override public void onProgress(ProgressListener.Event event) { String message = ""; // TODO: Switch to passing through Bundles of data with the event, rather than a repo address. They are // now much more general purpose then just repo downloading. String repoAddress = event.getData().getString(RepoUpdater.PROGRESS_DATA_REPO_ADDRESS); if (event.type.equals(Downloader.EVENT_PROGRESS)) { String downloadedSize = Utils.getFriendlySize(event.progress); String totalSize = Utils.getFriendlySize(event.total); int percent = (int) ((double) event.progress / event.total * 100); message = getString(R.string.status_download, repoAddress, downloadedSize, totalSize, percent); } else if (event.type.equals(RepoUpdater.PROGRESS_TYPE_PROCESS_XML)) { message = getString(R.string.status_processing_xml, repoAddress, event.progress, event.total); } sendStatus(STATUS_INFO, message); } }