Merge branch 'master' of git://gitorious.org/f-droid/fdroidclient

This commit is contained in:
David Black 2013-04-16 13:35:18 +01:00
commit 299596ae58
20 changed files with 622 additions and 291 deletions

View File

@ -15,7 +15,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:ems="20" android:ems="20"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/repo_add_http"/> android:text="https://"/>
</LinearLayout> </LinearLayout>
<!-- <!--
* Copyright (C) 2009 Roberto Jacinto * Copyright (C) 2009 Roberto Jacinto

View File

@ -74,7 +74,6 @@
<string name="download_server">Getting application from</string> <string name="download_server">Getting application from</string>
<string name="repo_add_url">Repository address</string> <string name="repo_add_url">Repository address</string>
<string name="repo_add_http">http://</string>
<string name="isinst">Installed: </string> <string name="isinst">Installed: </string>
@ -153,4 +152,16 @@
<string name="category_whatsnew">What\'s New</string> <string name="category_whatsnew">What\'s New</string>
<string name="category_recentlyupdated">Recently Updated</string> <string name="category_recentlyupdated">Recently Updated</string>
<!--
status_download takes four parameters:
- Repository (url)
- Downloaded size (human readable)
- Total size (human readable)
- Percentage complete (int between 0-100)
-->
<string name="status_download">Downloading\n%2$s / %3$s (%4$d%%) from\n%1$s</string>
<string name="status_processing_xml">Processing application\n%2$d of %3$d from\n%1$s</string>
<string name="status_connecting_to_repo">Connecting to\n%1$s</string>
<string name="status_checking_compatibility">Checking all apps compatibility with your device…</string>
</resources> </resources>

View File

@ -22,9 +22,9 @@ import java.io.File;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Vector;
import android.support.v4.view.MenuItemCompat; import android.support.v4.view.MenuItemCompat;
import org.fdroid.fdroid.compat.MenuManager;
import org.xml.sax.XMLReader; import org.xml.sax.XMLReader;
import android.app.AlertDialog; import android.app.AlertDialog;
@ -127,7 +127,7 @@ public class AppDetails extends ListActivity {
if (apk.detail_size == 0) { if (apk.detail_size == 0) {
size.setText(""); size.setText("");
} else { } else {
size.setText(getFriendlySize(apk.detail_size)); size.setText(Utils.getFriendlySize(apk.detail_size));
} }
TextView buildtype = (TextView) v.findViewById(R.id.buildtype); TextView buildtype = (TextView) v.findViewById(R.id.buildtype);
if (apk.srcname != null) { if (apk.srcname != null) {
@ -153,19 +153,6 @@ public class AppDetails extends ListActivity {
} }
} }
private static final String[] FRIENDLY_SIZE_FORMAT = {
"%.0f B", "%.0f KiB", "%.1f MiB", "%.2f GiB" };
private static String getFriendlySize(int size) {
double s = size;
int i = 0;
while (i < FRIENDLY_SIZE_FORMAT.length - 1 && s >= 1024) {
s = (100 * s / 1024) / 100.0;
i++;
}
return String.format(FRIENDLY_SIZE_FORMAT[i], s);
}
private static final int INSTALL = Menu.FIRST; private static final int INSTALL = Menu.FIRST;
private static final int UNINSTALL = Menu.FIRST + 1; private static final int UNINSTALL = Menu.FIRST + 1;
private static final int WEBSITE = Menu.FIRST + 2; private static final int WEBSITE = Menu.FIRST + 2;
@ -249,7 +236,9 @@ public class AppDetails extends ListActivity {
resetRequired = false; resetRequired = false;
} }
resetViews(); resetViews();
invalidateOptionsMenu();
MenuManager.create(this).invalidateOptionsMenu();
if (downloadHandler != null) { if (downloadHandler != null) {
downloadHandler.startUpdates(); downloadHandler.startUpdates();
} }
@ -302,7 +291,7 @@ public class AppDetails extends ListActivity {
Log.d("FDroid", "Getting application details for " + appid); Log.d("FDroid", "Getting application details for " + appid);
app = null; app = null;
Vector<DB.App> apps = ((FDroidApp) getApplication()).getApps(); List<DB.App> apps = ((FDroidApp) getApplication()).getApps();
for (DB.App tapp : apps) { for (DB.App tapp : apps) {
if (tapp.id.equals(appid)) { if (tapp.id.equals(appid)) {
app = tapp; app = tapp;
@ -797,4 +786,5 @@ public class AppDetails extends ListActivity {
break; break;
} }
} }
} }

View File

@ -15,7 +15,7 @@ import java.util.*;
*/ */
public class AppListManager { public class AppListManager {
private Vector<DB.App> allApps = null; private List<DB.App> allApps = null;
private FDroid fdroidActivity; private FDroid fdroidActivity;
@ -55,7 +55,7 @@ public class AppListManager {
// Needs to be created before createViews(), because that will use the // Needs to be created before createViews(), because that will use the
// getCategoriesAdapter() accessor which expects this object... // getCategoriesAdapter() accessor which expects this object...
categories = new ArrayAdapter<String>(activity, categories = new ArrayAdapter<String>(activity,
android.R.layout.simple_spinner_item, new Vector<String>()); android.R.layout.simple_spinner_item, new ArrayList<String>());
categories categories
.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
} }
@ -185,7 +185,7 @@ public class AppListManager {
Date recentDate = calcMaxHistory(); Date recentDate = calcMaxHistory();
AppFilter appFilter = new AppFilter(fdroidActivity); AppFilter appFilter = new AppFilter(fdroidActivity);
Vector<DB.App> availApps = new Vector<DB.App>(); List<DB.App> availApps = new ArrayList<DB.App>();
for (DB.App app : allApps) { for (DB.App app : allApps) {
boolean isInCategory = isInCategory(app, currentCategory, recentDate); boolean isInCategory = isInCategory(app, currentCategory, recentDate);
@ -215,7 +215,10 @@ public class AppListManager {
} }
public void setCurrentCategory(String currentCategory) { public void setCurrentCategory(String currentCategory) {
this.currentCategory = currentCategory; if (!this.currentCategory.equals(currentCategory)){
this.currentCategory = currentCategory;
repopulateLists();
}
} }
static class WhatsNewComparator implements Comparator<DB.App> { static class WhatsNewComparator implements Comparator<DB.App> {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com * Copyright (C) 2010-13 Ciaran Gultnieks, ciaran@ciarang.com
* Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt * Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -21,6 +21,7 @@ package org.fdroid.fdroid;
import java.io.File; import java.io.File;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
@ -28,7 +29,6 @@ import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Vector;
import java.util.concurrent.Semaphore; import java.util.concurrent.Semaphore;
import android.annotation.TargetApi; import android.annotation.TargetApi;
@ -41,7 +41,6 @@ import android.content.pm.PackageManager;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.text.TextUtils.SimpleStringSplitter; import android.text.TextUtils.SimpleStringSplitter;
@ -116,7 +115,7 @@ public class DB {
updated = false; updated = false;
added = null; added = null;
lastUpdated = null; lastUpdated = null;
apks = new Vector<Apk>(); apks = new ArrayList<Apk>();
detail_Populated = false; detail_Populated = false;
compatible = false; compatible = false;
} }
@ -181,7 +180,7 @@ public class DB {
public boolean updated; public boolean updated;
// List of apks. // List of apks.
public Vector<Apk> apks; public List<Apk> apks;
// Get the current version - this will be one of the Apks from 'apks'. // Get the current version - this will be one of the Apks from 'apks'.
// Can return null if there are no available versions. // Can return null if there are no available versions.
@ -278,28 +277,23 @@ public class DB {
// check if an APK is compatible with the user's device. // check if an APK is compatible with the user's device.
public static abstract class CompatibilityChecker { public static abstract class CompatibilityChecker {
// Because Build.VERSION.SDK_INT requires API level 5
@SuppressWarnings("deprecation")
protected final static int SDK_INT = Integer
.parseInt(Build.VERSION.SDK);
public abstract boolean isCompatible(Apk apk); public abstract boolean isCompatible(Apk apk);
public static CompatibilityChecker getChecker(Context ctx) { public static CompatibilityChecker getChecker(Context ctx) {
CompatibilityChecker checker; CompatibilityChecker checker;
if (SDK_INT >= 5) if (Utils.hasApi(5))
checker = new EclairChecker(ctx); checker = new EclairChecker(ctx);
else else
checker = new BasicChecker(); checker = new BasicChecker();
Log.d("FDroid", "Compatibility checker for API level " Log.d("FDroid", "Compatibility checker for API level "
+ SDK_INT + ": " + checker.getClass().getName()); + Utils.getApi() + ": " + checker.getClass().getName());
return checker; return checker;
} }
} }
private static class BasicChecker extends CompatibilityChecker { private static class BasicChecker extends CompatibilityChecker {
public boolean isCompatible(Apk apk) { public boolean isCompatible(Apk apk) {
return (apk.minSdkVersion <= SDK_INT); return (apk.minSdkVersion <= Utils.getApi());
} }
} }
@ -329,7 +323,7 @@ public class DB {
} }
public boolean isCompatible(Apk apk) { public boolean isCompatible(Apk apk) {
if (apk.minSdkVersion > SDK_INT) if (apk.minSdkVersion > Utils.getApi())
return false; return false;
if (apk.features != null) { if (apk.features != null) {
for (String feat : apk.features) { for (String feat : apk.features) {
@ -379,6 +373,7 @@ public class DB {
public static void resetTransient(SQLiteDatabase db) { public static void resetTransient(SQLiteDatabase db) {
db.execSQL("drop table " + TABLE_APP); db.execSQL("drop table " + TABLE_APP);
db.execSQL("drop table " + TABLE_APK); db.execSQL("drop table " + TABLE_APK);
db.execSQL("update " + TABLE_REPO + " set lastetag = NULL");
createAppApk(db); createAppApk(db);
} }
@ -412,7 +407,7 @@ public class DB {
// Migrate repo list to new structure. (No way to change primary // Migrate repo list to new structure. (No way to change primary
// key in sqlite - table must be recreated) // key in sqlite - table must be recreated)
if (oldVersion < 20) { if (oldVersion < 20) {
Vector<Repo> oldrepos = new Vector<Repo>(); List<Repo> oldrepos = new ArrayList<Repo>();
Cursor c = db.rawQuery("select address, inuse, pubkey from " Cursor c = db.rawQuery("select address, inuse, pubkey from "
+ TABLE_REPO, null); + TABLE_REPO, null);
c.moveToFirst(); c.moveToFirst();
@ -503,7 +498,7 @@ public class DB {
// Get the number of apps that have updates available. This can be a // Get the number of apps that have updates available. This can be a
// time consuming operation. // time consuming operation.
public int getNumUpdates() { public int getNumUpdates() {
Vector<App> apps = getApps(true); List<App> apps = getApps(true);
int count = 0; int count = 0;
for (App app : apps) { for (App app : apps) {
if (app.hasUpdates) if (app.hasUpdates)
@ -512,8 +507,8 @@ public class DB {
return count; return count;
} }
public Vector<String> getCategories() { public List<String> getCategories() {
Vector<String> result = new Vector<String>(); List<String> result = new ArrayList<String>();
Cursor c = null; Cursor c = null;
try { try {
c = db.rawQuery("select distinct category from " + TABLE_APP c = db.rawQuery("select distinct category from " + TABLE_APP
@ -591,14 +586,15 @@ public class DB {
// Return a list of apps matching the given criteria. Filtering is // Return a list of apps matching the given criteria. Filtering is
// also done based on compatibility and anti-features according to // also done based on compatibility and anti-features according to
// the user's current preferences. // the user's current preferences.
public Vector<App> getApps(boolean getinstalledinfo) { public List<App> getApps(boolean getinstalledinfo) {
// If we're going to need it, get info in what's currently installed // If we're going to need it, get info in what's currently installed
Map<String, PackageInfo> systemApks = null; Map<String, PackageInfo> systemApks = null;
if (getinstalledinfo) { if (getinstalledinfo) {
Log.d("FDroid", "Reading installed packages"); Log.d("FDroid", "Reading installed packages");
systemApks = new HashMap<String, PackageInfo>(); systemApks = new HashMap<String, PackageInfo>();
List<PackageInfo> installedPackages = mContext.getPackageManager().getInstalledPackages(0); List<PackageInfo> installedPackages = mContext.getPackageManager()
.getInstalledPackages(0);
for (PackageInfo appInfo : installedPackages) { for (PackageInfo appInfo : installedPackages) {
systemApks.put(appInfo.packageName, appInfo); systemApks.put(appInfo.packageName, appInfo);
} }
@ -696,7 +692,7 @@ public class DB {
+ (System.currentTimeMillis() - startTime) + " ms)"); + (System.currentTimeMillis() - startTime) + " ms)");
} }
Vector<App> result = new Vector<App>(apps.values()); List<App> result = new ArrayList<App>(apps.values());
Collections.sort(result); Collections.sort(result);
// Fill in the hasUpdates fields if we have the necessary information... // Fill in the hasUpdates fields if we have the necessary information...
@ -720,9 +716,9 @@ public class DB {
return result; return result;
} }
public Vector<String> doSearch(String query) { public List<String> doSearch(String query) {
Vector<String> ids = new Vector<String>(); List<String> ids = new ArrayList<String>();
Cursor c = null; Cursor c = null;
try { try {
String filter = "%" + query + "%"; String filter = "%" + query + "%";
@ -770,11 +766,11 @@ public class DB {
} }
} }
private Vector<App> updateApps = null; private List<App> updateApps = null;
// Called before a repo update starts. Returns the number of updates // Called before a repo update starts. Returns the number of updates
// available beforehand. // available beforehand.
public int beginUpdate(Vector<DB.App> apps) { public int beginUpdate(List<DB.App> apps) {
// Get a list of all apps. All the apps and apks in this list will // Get a list of all apps. All the apps and apks in this list will
// have 'updated' set to false at this point, and we will only set // have 'updated' set to false at this point, and we will only set
// it to true when we see the app/apk in a repository. Thus, at the // it to true when we see the app/apk in a repository. Thus, at the
@ -865,7 +861,7 @@ public class DB {
// compatible apk - if it's not, leave it out) // compatible apk - if it's not, leave it out)
// Also keep a list of which were compatible, because they're the // Also keep a list of which were compatible, because they're the
// only ones we'll add, unless the showIncompatible preference is set. // only ones we'll add, unless the showIncompatible preference is set.
Vector<Apk> compatibleapks = new Vector<Apk>(); List<Apk> compatibleapks = new ArrayList<Apk>();
for (Apk apk : upapp.apks) { for (Apk apk : upapp.apks) {
if (compatChecker.isCompatible(apk)) { if (compatChecker.isCompatible(apk)) {
apk.compatible = true; apk.compatible = true;
@ -1016,8 +1012,8 @@ public class DB {
} }
// Get a list of the configured repositories. // Get a list of the configured repositories.
public Vector<Repo> getRepos() { public List<Repo> getRepos() {
Vector<Repo> repos = new Vector<Repo>(); List<Repo> repos = new ArrayList<Repo>();
Cursor c = null; Cursor c = null;
try { try {
c = db.rawQuery( c = db.rawQuery(
@ -1078,10 +1074,27 @@ public class DB {
db.insert(TABLE_REPO, null, values); db.insert(TABLE_REPO, null, values);
} }
public void removeServers(Vector<String> addresses) { public void removeRepos(List<String> addresses) {
db.beginTransaction(); db.beginTransaction();
try { try {
for (String address : addresses) { for (String address : addresses) {
// Before removing the repo, remove any apks that are
// connected to it...
Cursor c = null;
try {
c = db.rawQuery("select id from " + TABLE_REPO
+ " where address = '" + address + "'", null);
c.moveToFirst();
if (!c.isAfterLast()) {
db.delete(TABLE_APK, "repo = ?",
new String[] { Integer.toString(c.getInt(0)) });
}
} finally {
if (c != null) {
c.close();
}
}
db.delete(TABLE_REPO, "address = ?", new String[] { address }); db.delete(TABLE_REPO, "address = ?", new String[] { address });
} }
db.setTransactionSuccessful(); db.setTransactionSuccessful();

View File

@ -19,28 +19,17 @@
package org.fdroid.fdroid; package org.fdroid.fdroid;
import android.app.ActionBar;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Vector;
import android.support.v4.view.MenuItemCompat; import android.support.v4.view.MenuItemCompat;
import org.fdroid.fdroid.DB.App;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import android.R.drawable;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.AlertDialog.Builder; import android.app.AlertDialog.Builder;
import android.app.FragmentTransaction;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.ResultReceiver; import android.os.ResultReceiver;
@ -52,7 +41,7 @@ import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.*; import android.widget.*;
import android.widget.TabHost.TabSpec; import org.fdroid.fdroid.compat.TabManager;
import org.fdroid.fdroid.views.AppListFragmentPageAdapter; import org.fdroid.fdroid.views.AppListFragmentPageAdapter;
public class FDroid extends FragmentActivity { public class FDroid extends FragmentActivity {
@ -75,8 +64,7 @@ public class FDroid extends FragmentActivity {
private AppListManager manager = null; private AppListManager manager = null;
// Used by pre 3.0 devices which don't have an ActionBar... private TabManager tabManager = null;
private TabHost tabHost;
public AppListManager getManager() { public AppListManager getManager() {
return manager; return manager;
@ -89,7 +77,7 @@ public class FDroid extends FragmentActivity {
manager = new AppListManager(this); manager = new AppListManager(this);
setContentView(R.layout.fdroid); setContentView(R.layout.fdroid);
createViews(); createViews();
createTabs(); getTabManager().createTabs();
// Must be done *after* createViews, because it will involve a // Must be done *after* createViews, because it will involve a
// callback to update the tab label for the "update" tab. This // callback to update the tab label for the "update" tab. This
@ -104,7 +92,7 @@ public class FDroid extends FragmentActivity {
} else if (i.hasExtra(EXTRA_TAB_UPDATE)) { } else if (i.hasExtra(EXTRA_TAB_UPDATE)) {
boolean showUpdateTab = i.getBooleanExtra(EXTRA_TAB_UPDATE, false); boolean showUpdateTab = i.getBooleanExtra(EXTRA_TAB_UPDATE, false);
if (showUpdateTab) { if (showUpdateTab) {
selectTab(2); getTabManager().selectTab(2);
} }
} }
} }
@ -250,127 +238,7 @@ public class FDroid extends FragmentActivity {
viewPager.setAdapter(viewPageAdapter); viewPager.setAdapter(viewPageAdapter);
viewPager.setOnPageChangeListener( new ViewPager.SimpleOnPageChangeListener() { viewPager.setOnPageChangeListener( new ViewPager.SimpleOnPageChangeListener() {
public void onPageSelected(int position) { public void onPageSelected(int position) {
selectTab(position); getTabManager().selectTab(position);
}
});
}
private void createTabs() {
if (Build.VERSION.SDK_INT >= 11) {
createActionBarTabs();
} else {
createOldTabs();
}
}
private void selectTab(int index) {
if (Build.VERSION.SDK_INT >= 11) {
getActionBar().setSelectedNavigationItem(index);
} else {
tabHost.setCurrentTab(index);
}
}
public void refreshUpdateTabLabel() {
final int INDEX = 2;
CharSequence text = viewPager.getAdapter().getPageTitle(INDEX);
if ( Build.VERSION.SDK_INT >= 11) {
getActionBar().getTabAt(INDEX).setText(text);
} else {
// Update the count on the 'Updates' tab to show the number available.
// This is quite unpleasant, but seems to be the only way to do it.
TextView textView = (TextView) tabHost.getTabWidget().getChildAt(2)
.findViewById(android.R.id.title);
textView.setText(text);
}
}
private void createActionBarTabs() {
final ActionBar actionBar = getActionBar();
final ViewPager pager = viewPager;
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
for (int i = 0; i < viewPager.getAdapter().getCount(); i ++) {
CharSequence label = viewPager.getAdapter().getPageTitle(i);
actionBar.addTab(
actionBar.newTab()
.setText(label)
.setTabListener(new ActionBar.TabListener() {
public void onTabSelected(ActionBar.Tab tab,
FragmentTransaction ft) {
pager.setCurrentItem(tab.getPosition());
}
@Override
public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) {
}
@Override
public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) {
}
}));
}
}
/**
* There is a bit of boiler-plate code required to get a TabWidget showing,
* which includes creating a TabHost, populating it with the TabWidget,
* and giving it a FrameLayout as a child. This will make the tabs have
* dummy empty contents and then hook them up to our ViewPager.
*/
private void createOldTabs() {
tabHost = new TabHost(this);
tabHost.setLayoutParams(new TabHost.LayoutParams(
TabHost.LayoutParams.MATCH_PARENT, TabHost.LayoutParams.WRAP_CONTENT));
TabWidget tabWidget = new TabWidget(this);
tabWidget.setId(android.R.id.tabs);
tabHost.setLayoutParams(new TabHost.LayoutParams(
TabWidget.LayoutParams.MATCH_PARENT, TabWidget.LayoutParams.WRAP_CONTENT));
FrameLayout layout = new FrameLayout(this);
layout.setId(android.R.id.tabcontent);
layout.setLayoutParams(new TabWidget.LayoutParams(0, 0));
tabHost.addView(tabWidget);
tabHost.addView(layout);
tabHost.setup();
TabHost.TabContentFactory factory = new TabHost.TabContentFactory() {
@Override
public View createTabContent(String tag) {
return new View(FDroid.this);
}
};
TabSpec availableTabSpec = tabHost.newTabSpec("available")
.setIndicator(
getString(R.string.tab_noninstalled),
getResources().getDrawable(android.R.drawable.ic_input_add))
.setContent(factory);
TabSpec installedTabSpec = tabHost.newTabSpec("installed")
.setIndicator(
getString(R.string.tab_installed),
getResources().getDrawable(android.R.drawable.star_off))
.setContent(factory);
TabSpec canUpdateTabSpec = tabHost.newTabSpec("canUpdate")
.setIndicator(
getString(R.string.tab_updates),
getResources().getDrawable(android.R.drawable.star_on))
.setContent(factory);
tabHost.addTab(availableTabSpec);
tabHost.addTab(installedTabSpec);
tabHost.addTab(canUpdateTabSpec);
LinearLayout contentView = (LinearLayout)findViewById(R.id.fdroid_layout);
contentView.addView(tabHost, 0);
tabHost.setOnTabChangedListener( new TabHost.OnTabChangeListener() {
@Override
public void onTabChanged(String tabId) {
viewPager.setCurrentItem(tabHost.getCurrentTab());
} }
}); });
} }
@ -384,13 +252,19 @@ public class FDroid extends FragmentActivity {
@Override @Override
protected void onReceiveResult(int resultCode, Bundle resultData) { protected void onReceiveResult(int resultCode, Bundle resultData) {
if (resultCode == 1) { String message = resultData.getString(UpdateService.RESULT_MESSAGE);
Toast.makeText(FDroid.this, resultData.getString("errmsg"), boolean finished = false;
Toast.LENGTH_LONG).show(); if (resultCode == UpdateService.STATUS_ERROR) {
} else { Toast.makeText(FDroid.this, message, Toast.LENGTH_LONG).show();
finished = true;
} else if (resultCode == UpdateService.STATUS_COMPLETE) {
repopulateViews(); repopulateViews();
finished = true;
} else if (resultCode == UpdateService.STATUS_INFO) {
pd.setMessage(message);
} }
if (pd.isShowing())
if (finished && pd.isShowing())
pd.dismiss(); pd.dismiss();
} }
} }
@ -409,7 +283,7 @@ public class FDroid extends FragmentActivity {
boolean hasTriedEmptyUpdate = getPreferences(MODE_PRIVATE).getBoolean(TRIED_EMPTY_UPDATE, false); boolean hasTriedEmptyUpdate = getPreferences(MODE_PRIVATE).getBoolean(TRIED_EMPTY_UPDATE, false);
if (!hasTriedEmptyUpdate) { if (!hasTriedEmptyUpdate) {
Log.d("FDroid", "Empty app list, and we haven't done an update yet. Forcing repo update."); Log.d("FDroid", "Empty app list, and we haven't done an update yet. Forcing repo update.");
getPreferences(MODE_PRIVATE).edit().putBoolean(TRIED_EMPTY_UPDATE, true).apply(); getPreferences(MODE_PRIVATE).edit().putBoolean(TRIED_EMPTY_UPDATE, true).commit();
updateRepos(); updateRepos();
return true; return true;
} else { } else {
@ -434,4 +308,15 @@ public class FDroid extends FragmentActivity {
startService(intent); startService(intent);
} }
private TabManager getTabManager() {
if (tabManager == null) {
tabManager = TabManager.create(this, viewPager);
}
return tabManager;
}
public void refreshUpdateTabLabel() {
getTabManager().refreshTabLabel(TabManager.INDEX_CAN_UPDATE);
}
} }

View File

@ -19,7 +19,8 @@
package org.fdroid.fdroid; package org.fdroid.fdroid;
import java.io.File; import java.io.File;
import java.util.Vector; import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore; import java.util.concurrent.Semaphore;
import android.app.Application; import android.app.Application;
@ -47,7 +48,7 @@ public class FDroidApp extends Application {
} }
// Global list of all known applications. // Global list of all known applications.
private Vector<DB.App> apps; private List<DB.App> apps;
// Set when something has changed (database or installed apps) so we know // Set when something has changed (database or installed apps) so we know
// we should invalidate the apps. // we should invalidate the apps.
@ -70,7 +71,7 @@ public class FDroidApp extends Application {
// Get a list of all known applications. Should not be called when the // Get a list of all known applications. Should not be called when the
// database is locked (i.e. between DB.getDB() and db.releaseDB(). The // database is locked (i.e. between DB.getDB() and db.releaseDB(). The
// contents should never be modified, it's for reading only. // contents should never be modified, it's for reading only.
public Vector<DB.App> getApps() { public List<DB.App> getApps() {
boolean invalid = false; boolean invalid = false;
try { try {
@ -95,7 +96,7 @@ public class FDroidApp extends Application {
} }
} }
if (apps == null) if (apps == null)
return new Vector<DB.App>(); return new ArrayList<DB.App>();
return apps; return apps;
} }

View File

@ -26,7 +26,6 @@ import java.util.Formatter;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Vector;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.AlertDialog.Builder; import android.app.AlertDialog.Builder;
@ -55,7 +54,7 @@ public class ManageRepo extends ListActivity {
private boolean changed = false; private boolean changed = false;
private Vector<DB.Repo> repos; private List<DB.Repo> repos;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -201,7 +200,7 @@ public class ManageRepo extends ListActivity {
return true; return true;
case REM_REPO: case REM_REPO:
final Vector<String> rem_lst = new Vector<String>(); final List<String> rem_lst = new ArrayList<String>();
CharSequence[] b = new CharSequence[repos.size()]; CharSequence[] b = new CharSequence[repos.size()];
for (int i = 0; i < repos.size(); i++) { for (int i = 0; i < repos.size(); i++) {
b[i] = repos.get(i).address; b[i] = repos.get(i).address;
@ -215,9 +214,9 @@ public class ManageRepo extends ListActivity {
public void onClick(DialogInterface dialog, public void onClick(DialogInterface dialog,
int whichButton, boolean isChecked) { int whichButton, boolean isChecked) {
if (isChecked) { if (isChecked) {
rem_lst.addElement(repos.get(whichButton).address); rem_lst.add(repos.get(whichButton).address);
} else { } else {
rem_lst.removeElement(repos.get(whichButton).address); rem_lst.remove(repos.get(whichButton).address);
} }
} }
}); });
@ -227,7 +226,7 @@ public class ManageRepo extends ListActivity {
int whichButton) { int whichButton) {
try { try {
DB db = DB.getDB(); DB db = DB.getDB();
db.removeServers(rem_lst); db.removeRepos(rem_lst);
} finally { } finally {
DB.releaseDB(); DB.releaseDB();
} }

View File

@ -0,0 +1,54 @@
package org.fdroid.fdroid;
import android.os.Bundle;
public interface ProgressListener {
public void onProgress(Event event);
// I went a bit overboard with the overloaded constructors, but they all
// seemed potentially useful and unambiguous, so I just put them in there
// while I'm here.
public static class Event {
public static final int NO_VALUE = Integer.MIN_VALUE;
public final int type;
public final Bundle data;
// These two are not final, so that you can create a template Event,
// pass it into a function which performs something over time, and
// that function can initialize "total" and progressively
// update "progress"
public int progress;
public int total;
public Event(int type) {
this(type, NO_VALUE, NO_VALUE, null);
}
public Event(int type, Bundle data) {
this(type, NO_VALUE, NO_VALUE, data);
}
public Event(int type, int progress) {
this(type, progress, NO_VALUE, null);
}
public Event(int type, int progress, Bundle data) {
this(type, NO_VALUE, NO_VALUE, data);
}
public Event(int type, int progress, int total) {
this(type, progress, total, null);
}
public Event(int type, int progress, int total, Bundle data) {
this.type = type;
this.progress = progress;
this.total = total;
this.data = data == null ? new Bundle() : data;
}
}
}

View File

@ -26,14 +26,13 @@ import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.Reader;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Vector; import java.util.List;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
@ -41,6 +40,7 @@ import javax.net.ssl.SSLHandshakeException;
import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory; import javax.xml.parsers.SAXParserFactory;
import android.os.Bundle;
import org.xml.sax.Attributes; import org.xml.sax.Attributes;
import org.xml.sax.InputSource; import org.xml.sax.InputSource;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
@ -54,10 +54,10 @@ import android.util.Log;
public class RepoXMLHandler extends DefaultHandler { public class RepoXMLHandler extends DefaultHandler {
// The ID of the repo we're processing. // The repo we're processing.
private int repo; private DB.Repo repo;
private Vector<DB.App> apps; private List<DB.App> apps;
private DB.App curapp = null; private DB.App curapp = null;
private DB.Apk curapk = null; private DB.Apk curapk = null;
@ -66,13 +66,23 @@ public class RepoXMLHandler extends DefaultHandler {
private String pubkey; private String pubkey;
private String hashType; private String hashType;
private int progressCounter = 0;
private ProgressListener progressListener;
public static final int PROGRESS_TYPE_DOWNLOAD = 1;
public static final int PROGRESS_TYPE_PROCESS_XML = 2;
public static final String PROGRESS_DATA_REPO = "repo";
// The date format used in the repo XML file. // The date format used in the repo XML file.
private SimpleDateFormat mXMLDateFormat = new SimpleDateFormat("yyyy-MM-dd"); private SimpleDateFormat mXMLDateFormat = new SimpleDateFormat("yyyy-MM-dd");
private int totalAppCount;
public RepoXMLHandler(int repo, Vector<DB.App> apps) { public RepoXMLHandler(DB.Repo repo, List<DB.App> apps, ProgressListener listener) {
this.repo = repo; this.repo = repo;
this.apps = apps; this.apps = apps;
pubkey = null; pubkey = null;
progressListener = listener;
} }
@Override @Override
@ -219,27 +229,37 @@ public class RepoXMLHandler extends DefaultHandler {
curapp.requirements = DB.CommaSeparatedList.make(str); curapp.requirements = DB.CommaSeparatedList.make(str);
} }
} }
} }
private static Bundle createProgressData(String repoAddress) {
Bundle data = new Bundle();
data.putString(PROGRESS_DATA_REPO, repoAddress);
return data;
}
@Override @Override
public void startElement(String uri, String localName, String qName, public void startElement(String uri, String localName, String qName,
Attributes attributes) throws SAXException { Attributes attributes) throws SAXException {
super.startElement(uri, localName, qName, attributes); super.startElement(uri, localName, qName, attributes);
if (localName == "repo") { if (localName.equals("repo")) {
String pk = attributes.getValue("", "pubkey"); String pk = attributes.getValue("", "pubkey");
if (pk != null) if (pk != null)
pubkey = pk; pubkey = pk;
} else if (localName == "application" && curapp == null) { } else if (localName.equals("application") && curapp == null) {
curapp = new DB.App(); curapp = new DB.App();
curapp.detail_Populated = true; curapp.detail_Populated = true;
} else if (localName == "package" && curapp != null && curapk == null) { Bundle progressData = createProgressData(repo.address);
progressCounter ++;
progressListener.onProgress(
new ProgressListener.Event(
RepoXMLHandler.PROGRESS_TYPE_PROCESS_XML, progressCounter,
totalAppCount, progressData));
} else if (localName.equals("package") && curapp != null && curapk == null) {
curapk = new DB.Apk(); curapk = new DB.Apk();
curapk.id = curapp.id; curapk.id = curapp.id;
curapk.repo = repo; curapk.repo = repo.id;
hashType = null; hashType = null;
} else if (localName == "hash" && curapk != null) { } else if (localName.equals("hash") && curapk != null) {
hashType = attributes.getValue("", "type"); hashType = attributes.getValue("", "type");
} }
curchars.setLength(0); curchars.setLength(0);
@ -252,29 +272,39 @@ public class RepoXMLHandler extends DefaultHandler {
// empty) may contain an etag value for the response, or it may be left // empty) may contain an etag value for the response, or it may be left
// empty if none was available. // empty if none was available.
private static int getRemoteFile(Context ctx, String url, String dest, private static int getRemoteFile(Context ctx, String url, String dest,
String etag, StringBuilder retag) throws MalformedURLException, String etag, StringBuilder retag,
ProgressListener progressListener,
ProgressListener.Event progressEvent) throws MalformedURLException,
IOException { IOException {
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
URL u = new URL(url); URL u = new URL(url);
HttpURLConnection uc = (HttpURLConnection) u.openConnection(); HttpURLConnection connection = (HttpURLConnection) u.openConnection();
if (etag != null) if (etag != null)
uc.setRequestProperty("If-None-Match", etag); connection.setRequestProperty("If-None-Match", etag);
int totalBytes = 0; int totalBytes = 0;
int code = uc.getResponseCode(); int code = connection.getResponseCode();
if (code == 200) { if (code == 200) {
// Testing in the emulator for me, showed that figuring out the filesize took about 1 to 1.5 seconds.
// To put this in context, downloading a repo of:
// - 400k takes ~6 seconds
// - 5k takes ~3 seconds
// on my connection. I think the 1/1.5 seconds is worth it, because as the repo grows, the tradeoff will
// become more worth it.
progressEvent.total = connection.getContentLength();
Log.d("FDroid", "Downloading " + progressEvent.total + " bytes from " + url);
InputStream input = null; InputStream input = null;
OutputStream output = null; OutputStream output = null;
try { try {
input = new URL(url).openStream(); input = connection.getInputStream();
output = ctx.openFileOutput(dest, Context.MODE_PRIVATE); output = ctx.openFileOutput(dest, Context.MODE_PRIVATE);
Utils.copy(input, output); Utils.copy(input, output, progressListener, progressEvent);
} finally { } finally {
Utils.closeQuietly(output); Utils.closeQuietly(output);
Utils.closeQuietly(input); Utils.closeQuietly(input);
} }
String et = uc.getHeaderField("ETag"); String et = connection.getHeaderField("ETag");
if (et != null) if (et != null)
retag.append(et); retag.append(et);
} }
@ -293,7 +323,8 @@ public class RepoXMLHandler extends DefaultHandler {
// value for the index that was successfully processed, or it may contain // value for the index that was successfully processed, or it may contain
// null if none was available. // null if none was available.
public static String doUpdate(Context ctx, DB.Repo repo, public static String doUpdate(Context ctx, DB.Repo repo,
Vector<DB.App> apps, StringBuilder newetag, Vector<Integer> keeprepos) { List<DB.App> apps, StringBuilder newetag, List<Integer> keeprepos,
ProgressListener progressListener) {
try { try {
int code = 0; int code = 0;
@ -309,8 +340,11 @@ public class RepoXMLHandler extends DefaultHandler {
address += "?" + pi.versionName; address += "?" + pi.versionName;
} catch (Exception e) { } catch (Exception e) {
} }
Bundle progressData = createProgressData(repo.address);
ProgressListener.Event event = new ProgressListener.Event(
RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD, progressData);
code = getRemoteFile(ctx, address, "tempindex.jar", code = getRemoteFile(ctx, address, "tempindex.jar",
repo.lastetag, newetag); repo.lastetag, newetag, progressListener, event );
if (code == 200) { if (code == 200) {
String jarpath = ctx.getFilesDir() + "/tempindex.jar"; String jarpath = ctx.getFilesDir() + "/tempindex.jar";
JarFile jar = null; JarFile jar = null;
@ -365,8 +399,12 @@ public class RepoXMLHandler extends DefaultHandler {
// It's an old-fashioned unsigned repo... // It's an old-fashioned unsigned repo...
Log.d("FDroid", "Getting unsigned index from " + repo.address); Log.d("FDroid", "Getting unsigned index from " + repo.address);
Bundle eventData = createProgressData(repo.address);
ProgressListener.Event event = new ProgressListener.Event(
RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD, eventData);
code = getRemoteFile(ctx, repo.address + "/index.xml", code = getRemoteFile(ctx, repo.address + "/index.xml",
"tempindex.xml", repo.lastetag, newetag); "tempindex.xml", repo.lastetag, newetag,
progressListener, event);
} }
if (code == 200) { if (code == 200) {
@ -374,11 +412,22 @@ public class RepoXMLHandler extends DefaultHandler {
SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser sp = spf.newSAXParser(); SAXParser sp = spf.newSAXParser();
XMLReader xr = sp.getXMLReader(); XMLReader xr = sp.getXMLReader();
RepoXMLHandler handler = new RepoXMLHandler(repo.id, apps); RepoXMLHandler handler = new RepoXMLHandler(repo, apps, progressListener);
xr.setContentHandler(handler); xr.setContentHandler(handler);
Reader r = new BufferedReader(new FileReader(new File( File tempIndex = new File(ctx.getFilesDir() + "/tempindex.xml");
ctx.getFilesDir() + "/tempindex.xml"))); BufferedReader r = new BufferedReader(new FileReader(tempIndex));
// A bit of a hack, this might return false positives if an apps description
// or some other part of the XML file contains this, but it is a pretty good
// estimate and makes the progress counter more informative.
// As with asking the server about the size of the index before downloading,
// this also has a time tradeoff. It takes about three seconds to iterate
// through the file and count 600 apps on a slow emulator (v17), but if it is
// taking two minutes to update, the three second wait may be worth it.
final String APPLICATION = "<application";
handler.setTotalAppCount(Utils.countSubstringOccurrence(tempIndex, APPLICATION));
InputSource is = new InputSource(r); InputSource is = new InputSource(r);
xr.parse(is); xr.parse(is);
@ -427,4 +476,7 @@ public class RepoXMLHandler extends DefaultHandler {
return null; return null;
} }
public void setTotalAppCount(int totalAppCount) {
this.totalAppCount = totalAppCount;
}
} }

View File

@ -18,7 +18,8 @@
package org.fdroid.fdroid; package org.fdroid.fdroid;
import java.util.Vector; import java.util.ArrayList;
import java.util.List;
import android.app.ListActivity; import android.app.ListActivity;
import android.app.SearchManager; import android.app.SearchManager;
@ -68,7 +69,7 @@ public class SearchResults extends ListActivity {
private void updateView() { private void updateView() {
Vector<String> matchingids = new Vector<String>(); List<String> matchingids = new ArrayList<String>();
try { try {
DB db = DB.getDB(); DB db = DB.getDB();
matchingids = db.doSearch(mQuery); matchingids = db.doSearch(mQuery);
@ -78,9 +79,9 @@ public class SearchResults extends ListActivity {
DB.releaseDB(); DB.releaseDB();
} }
Vector<DB.App> apps = new Vector<DB.App>(); List<DB.App> apps = new ArrayList<DB.App>();
AppFilter appfilter = new AppFilter(this); AppFilter appfilter = new AppFilter(this);
Vector<DB.App> tapps = ((FDroidApp) getApplication()).getApps(); List<DB.App> tapps = ((FDroidApp) getApplication()).getApps();
for (DB.App tapp : tapps) { for (DB.App tapp : tapps) {
boolean include = false; boolean include = false;
for (String tid : matchingids) { for (String tid : matchingids) {

View File

@ -24,7 +24,8 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.util.Vector; import java.util.ArrayList;
import java.util.List;
import android.app.AlarmManager; import android.app.AlarmManager;
import android.app.IntentService; import android.app.IntentService;
@ -41,7 +42,14 @@ import android.os.SystemClock;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.util.Log; import android.util.Log;
public class UpdateService extends IntentService { public class UpdateService extends IntentService implements ProgressListener {
public static final String RESULT_MESSAGE = "msg";
public static final int STATUS_COMPLETE = 0;
public static final int STATUS_ERROR = 1;
public static final int STATUS_INFO = 2;
private ResultReceiver receiver = null;
public UpdateService() { public UpdateService() {
super("UpdateService"); super("UpdateService");
@ -71,24 +79,41 @@ public class UpdateService extends IntentService {
} }
} }
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 );
}
}
/**
* 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;
}
protected void onHandleIntent(Intent intent) { protected void onHandleIntent(Intent intent) {
// We might be doing a scheduled run, or we might have been launched by receiver = intent.getParcelableExtra("receiver");
// the app in response to a user's request. If we get this receiver,
// it's
// the latter...
ResultReceiver receiver = intent.getParcelableExtra("receiver");
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
String errmsg = ""; String errmsg = "";
try { try {
SharedPreferences prefs = PreferenceManager SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(getBaseContext()); .getDefaultSharedPreferences(getBaseContext());
// See if it's time to actually do anything yet... // See if it's time to actually do anything yet...
if (receiver == null) { if (isScheduledRun()) {
long lastUpdate = prefs.getLong("lastUpdateCheck", 0); long lastUpdate = prefs.getLong("lastUpdateCheck", 0);
String sint = prefs.getString("updateInterval", "0"); String sint = prefs.getString("updateInterval", "0");
int interval = Integer.parseInt(sint); int interval = Integer.parseInt(sint);
@ -110,7 +135,7 @@ public class UpdateService extends IntentService {
// database while we do all the downloading, etc... // database while we do all the downloading, etc...
int prevUpdates = 0; int prevUpdates = 0;
int newUpdates = 0; int newUpdates = 0;
Vector<DB.Repo> repos; List<DB.Repo> repos;
try { try {
DB db = DB.getDB(); DB db = DB.getDB();
repos = db.getRepos(); repos = db.getRepos();
@ -119,14 +144,17 @@ public class UpdateService extends IntentService {
} }
// Process each repo... // Process each repo...
Vector<DB.App> apps = new Vector<DB.App>(); List<DB.App> apps = new ArrayList<DB.App>();
Vector<Integer> keeprepos = new Vector<Integer>(); List<Integer> keeprepos = new ArrayList<Integer>();
boolean success = true; boolean success = true;
for (DB.Repo repo : repos) { for (DB.Repo repo : repos) {
if (repo.inuse) { if (repo.inuse) {
sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address));
StringBuilder newetag = new StringBuilder(); StringBuilder newetag = new StringBuilder();
String err = RepoXMLHandler.doUpdate(getBaseContext(), String err = RepoXMLHandler.doUpdate(getBaseContext(),
repo, apps, newetag, keeprepos); repo, apps, newetag, keeprepos, this);
if (err == null) { if (err == null) {
repo.lastetag = newetag.toString(); repo.lastetag = newetag.toString();
} else { } else {
@ -142,9 +170,9 @@ public class UpdateService extends IntentService {
} }
if (success) { if (success) {
Vector<DB.App> acceptedapps = new Vector<DB.App>(); sendStatus(STATUS_INFO, getString(R.string.status_checking_compatibility));
Vector<DB.App> prevapps = ((FDroidApp) getApplication()) List<DB.App> acceptedapps = new ArrayList<DB.App>();
.getApps(); List<DB.App> prevapps = ((FDroidApp) getApplication()).getApps();
DB db = DB.getDB(); DB db = DB.getDB();
try { try {
@ -234,17 +262,12 @@ public class UpdateService extends IntentService {
} }
} }
if (receiver != null) { if (!success) {
Bundle resultData = new Bundle(); if (errmsg.length() == 0)
if (!success) { errmsg = "Unknown error";
if (errmsg.length() == 0) sendStatus(STATUS_ERROR, errmsg);
errmsg = "Unknown error"; } else {
resultData.putString("errmsg", errmsg); sendStatus(STATUS_COMPLETE);
receiver.send(1, resultData);
} else {
receiver.send(0, resultData);
}
} }
if(success) { if(success) {
@ -257,22 +280,18 @@ public class UpdateService extends IntentService {
Log.e("FDroid", Log.e("FDroid",
"Exception during update processing:\n" "Exception during update processing:\n"
+ Log.getStackTraceString(e)); + Log.getStackTraceString(e));
if (receiver != null) { if (errmsg.length() == 0)
Bundle resultData = new Bundle(); errmsg = "Unknown error";
if (errmsg.length() == 0) sendStatus(STATUS_ERROR, errmsg);
errmsg = "Unknown error";
resultData.putString("errmsg", errmsg);
receiver.send(1, resultData);
}
} finally { } finally {
Log.d("FDroid", "Update took " Log.d("FDroid", "Update took "
+ ((System.currentTimeMillis() - startTime) / 1000) + ((System.currentTimeMillis() - startTime) / 1000)
+ " seconds."); + " seconds.");
receiver = null;
} }
} }
private void getIcon(DB.App app, Vector<DB.Repo> repos) { private void getIcon(DB.App app, List<DB.Repo> repos) {
try { try {
File f = new File(DB.getIconsPath(), app.icon); File f = new File(DB.getIconsPath(), app.icon);
@ -306,4 +325,25 @@ public class UpdateService extends IntentService {
} }
} }
/**
* 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 = "";
if (event.type == RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD) {
String repoAddress = event.data.getString(RepoXMLHandler.PROGRESS_DATA_REPO);
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 == RepoXMLHandler.PROGRESS_TYPE_PROCESS_XML) {
String repoAddress = event.data.getString(RepoXMLHandler.PROGRESS_DATA_REPO);
message = getString(R.string.status_processing_xml, repoAddress, event.progress, event.total);
}
sendStatus(STATUS_INFO, message);
}
} }

View File

@ -18,25 +18,45 @@
package org.fdroid.fdroid; package org.fdroid.fdroid;
import android.os.Build;
import java.io.BufferedReader;
import java.io.Closeable; import java.io.Closeable;
import java.io.File;
import java.io.FileReader;
import java.io.InputStream; import java.io.InputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
public final class Utils { public final class Utils {
private Utils() {
}
public static final int BUFFER_SIZE = 4096; public static final int BUFFER_SIZE = 4096;
private static final String[] FRIENDLY_SIZE_FORMAT = {
"%.0f B", "%.0f KiB", "%.1f MiB", "%.2f GiB" };
public static void copy(InputStream input, OutputStream output) public static void copy(InputStream input, OutputStream output)
throws IOException { throws IOException {
copy(input, output, null, null);
}
public static void copy(InputStream input, OutputStream output,
ProgressListener progressListener,
ProgressListener.Event templateProgressEvent)
throws IOException {
byte[] buffer = new byte[BUFFER_SIZE]; byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead = 0;
while (true) { while (true) {
int count = input.read(buffer); int count = input.read(buffer);
if (count == -1) { if (count == -1) {
break; break;
} }
if (progressListener != null) {
bytesRead += count;
templateProgressEvent.progress = bytesRead;
progressListener.onProgress(templateProgressEvent);
}
output.write(buffer, 0, count); output.write(buffer, 0, count);
} }
output.flush(); output.flush();
@ -52,4 +72,60 @@ public final class Utils {
// ignore // ignore
} }
} }
public static boolean hasApi(int apiLevel) {
return Build.VERSION.SDK_INT >= apiLevel;
}
public static int getApi() {
return Build.VERSION.SDK_INT;
}
public static String getFriendlySize(int size) {
double s = size;
int i = 0;
while (i < FRIENDLY_SIZE_FORMAT.length - 1 && s >= 1024) {
s = (100 * s / 1024) / 100.0;
i++;
}
return String.format(FRIENDLY_SIZE_FORMAT[i], s);
}
public static int countSubstringOccurrence(File file, String substring) throws IOException {
int count = 0;
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(file));
while(true) {
String line = reader.readLine();
if (line == null) {
break;
}
count += countSubstringOccurrence(line, substring);
}
} finally {
closeQuietly(reader);
}
return count;
}
/**
* Thanks to http://stackoverflow.com/a/767910
*/
public static int countSubstringOccurrence(String toSearch, String substring) {
int count = 0;
int index = 0;
while (true) {
index = toSearch.indexOf(substring, index);
if (index == -1){
break;
}
count ++;
index += substring.length();
}
return count;
}
} }

View File

@ -0,0 +1,48 @@
package org.fdroid.fdroid.compat;
import android.app.Activity;
import org.fdroid.fdroid.Utils;
abstract public class MenuManager {
public static MenuManager create(Activity activity) {
if (Utils.hasApi(11)) {
return new HoneycombMenuManagerImpl(activity);
} else {
return new OldMenuManagerImpl(activity);
}
}
protected final Activity activity;
protected MenuManager(Activity activity) {
this.activity = activity;
}
abstract public void invalidateOptionsMenu();
}
class OldMenuManagerImpl extends MenuManager {
protected OldMenuManagerImpl(Activity activity) {
super(activity);
}
@Override
public void invalidateOptionsMenu() {
}
}
class HoneycombMenuManagerImpl extends MenuManager {
protected HoneycombMenuManagerImpl(Activity activity) {
super(activity);
}
@Override
public void invalidateOptionsMenu() {
activity.invalidateOptionsMenu();
}
}

View File

@ -0,0 +1,173 @@
package org.fdroid.fdroid.compat;
import android.app.ActionBar;
import android.app.FragmentTransaction;
import android.support.v4.view.ViewPager;
import android.view.View;
import android.widget.*;
import org.fdroid.fdroid.FDroid;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
public abstract class TabManager {
public static final int INDEX_AVAILABLE = 0;
public static final int INDEX_INSTALLED = 1;
public static final int INDEX_CAN_UPDATE = 2;
public static TabManager create(FDroid parent, ViewPager pager) {
if (Utils.hasApi(11)) {
return new HoneycombTabManagerImpl(parent, pager);
} else {
return new OldTabManagerImpl(parent, pager);
}
}
protected final ViewPager pager;
protected final FDroid parent;
protected TabManager(FDroid parent, ViewPager pager) {
this.parent = parent;
this.pager = pager;
}
abstract public void createTabs();
abstract public void selectTab(int index);
abstract public void refreshTabLabel(int index);
protected CharSequence getLabel(int index) {
return pager.getAdapter().getPageTitle(index);
}
}
class OldTabManagerImpl extends TabManager {
private TabHost tabHost;
public OldTabManagerImpl(FDroid parent, ViewPager pager) {
super(parent, pager);
}
/**
* There is a bit of boiler-plate code required to get a TabWidget showing,
* which includes creating a TabHost, populating it with the TabWidget,
* and giving it a FrameLayout as a child. This will make the tabs have
* dummy empty contents and then hook them up to our ViewPager.
*/
public void createTabs() {
tabHost = new TabHost(parent);
tabHost.setLayoutParams(new TabHost.LayoutParams(
TabHost.LayoutParams.MATCH_PARENT, TabHost.LayoutParams.WRAP_CONTENT));
TabWidget tabWidget = new TabWidget(parent);
tabWidget.setId(android.R.id.tabs);
tabHost.setLayoutParams(new TabHost.LayoutParams(
TabWidget.LayoutParams.MATCH_PARENT, TabWidget.LayoutParams.WRAP_CONTENT));
FrameLayout layout = new FrameLayout(parent);
layout.setId(android.R.id.tabcontent);
layout.setLayoutParams(new TabWidget.LayoutParams(0, 0));
tabHost.addView(tabWidget);
tabHost.addView(layout);
tabHost.setup();
TabHost.TabContentFactory factory = new TabHost.TabContentFactory() {
@Override
public View createTabContent(String tag) {
return new View(parent);
}
};
TabHost.TabSpec availableTabSpec = tabHost.newTabSpec("available")
.setIndicator(
parent.getString(R.string.tab_noninstalled),
parent.getResources().getDrawable(android.R.drawable.ic_input_add))
.setContent(factory);
TabHost.TabSpec installedTabSpec = tabHost.newTabSpec("installed")
.setIndicator(
parent.getString(R.string.tab_installed),
parent.getResources().getDrawable(android.R.drawable.star_off))
.setContent(factory);
TabHost.TabSpec canUpdateTabSpec = tabHost.newTabSpec("canUpdate")
.setIndicator(
parent.getString(R.string.tab_updates),
parent.getResources().getDrawable(android.R.drawable.star_on))
.setContent(factory);
tabHost.addTab(availableTabSpec);
tabHost.addTab(installedTabSpec);
tabHost.addTab(canUpdateTabSpec);
LinearLayout contentView = (LinearLayout)parent.findViewById(R.id.fdroid_layout);
contentView.addView(tabHost, 0);
tabHost.setOnTabChangedListener( new TabHost.OnTabChangeListener() {
@Override
public void onTabChanged(String tabId) {
pager.setCurrentItem(tabHost.getCurrentTab());
}
});
}
public void selectTab(int index) {
tabHost.setCurrentTab(index);
}
public void refreshTabLabel(int index) {
CharSequence text = getLabel(index);
// Update the count on the 'Updates' tab to show the number available.
// This is quite unpleasant, but seems to be the only way to do it.
TextView textView = (TextView) tabHost.getTabWidget().getChildAt(index)
.findViewById(android.R.id.title);
textView.setText(text);
}
}
class HoneycombTabManagerImpl extends TabManager {
protected final ActionBar actionBar;
public HoneycombTabManagerImpl(FDroid parent, ViewPager pager) {
super(parent, pager);
actionBar = parent.getActionBar();
}
public void createTabs() {
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
for (int i = 0; i < pager.getAdapter().getCount(); i ++) {
CharSequence label = pager.getAdapter().getPageTitle(i);
actionBar.addTab(
actionBar.newTab()
.setText(label)
.setTabListener(new ActionBar.TabListener() {
public void onTabSelected(ActionBar.Tab tab,
FragmentTransaction ft) {
pager.setCurrentItem(tab.getPosition());
}
@Override
public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) {
}
@Override
public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) {
}
}));
}
}
public void selectTab(int index) {
actionBar.setSelectedNavigationItem(index);
}
public void refreshTabLabel(int index) {
CharSequence text = getLabel(index);
actionBar.getTabAt(index).setText(text);
}
}

View File

@ -1,14 +1,7 @@
package org.fdroid.fdroid.views; package org.fdroid.fdroid.views;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentPagerAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.fdroid.fdroid.AppListManager;
import org.fdroid.fdroid.FDroid; import org.fdroid.fdroid.FDroid;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.views.fragments.AvailableAppsFragment; import org.fdroid.fdroid.views.fragments.AvailableAppsFragment;

View File

@ -27,10 +27,6 @@ public class AppListView extends LinearLayout {
super(context, attrs); super(context, attrs);
} }
public AppListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setAppList(ListView appList) { public void setAppList(ListView appList) {
this.appList = appList; this.appList = appList;
} }

View File

@ -3,7 +3,6 @@ package org.fdroid.fdroid.views.fragments;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
@ -13,7 +12,6 @@ import org.fdroid.fdroid.views.AppListView;
abstract class AppListFragment extends Fragment implements AdapterView.OnItemClickListener { abstract class AppListFragment extends Fragment implements AdapterView.OnItemClickListener {
private AppListManager appListManager;
private FDroid parent; private FDroid parent;
protected abstract AppListAdapter getAppListAdapter(); protected abstract AppListAdapter getAppListAdapter();

View File

@ -1,13 +1,12 @@
package org.fdroid.fdroid.views.fragments; package org.fdroid.fdroid.views.fragments;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.*; import android.widget.*;
import org.fdroid.fdroid.AppListAdapter; import org.fdroid.fdroid.AppListAdapter;
import org.fdroid.fdroid.AppListManager;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.views.AppListView; import org.fdroid.fdroid.views.AppListView;
@ -51,8 +50,8 @@ public class AvailableAppsFragment extends AppListFragment implements AdapterVie
public void onItemSelected(AdapterView<?> parent, View view, int pos, public void onItemSelected(AdapterView<?> parent, View view, int pos,
long id) { long id) {
getAppListManager().setCurrentCategory(parent.getItemAtPosition(pos).toString()); String category = parent.getItemAtPosition(pos).toString();
getAppListManager().repopulateLists(); getAppListManager().setCurrentCategory(category);
} }
@Override @Override

View File

@ -4,7 +4,6 @@ import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView;
import org.fdroid.fdroid.AppListAdapter; import org.fdroid.fdroid.AppListAdapter;
public class InstalledAppsFragment extends AppListFragment { public class InstalledAppsFragment extends AppListFragment {