Removed DB, implemented AppProvider.
Yay! As expected, a lot of the stuff in DB class is due to UpdateService requiring it to process the downloaded indexes and insert data into the database. Thus, this change is about removing that stuff from the DB class and migrating to ContentProviders. This required a bit of a change to the way that UpdateService decides what to do with the data from indexes, but I hope it will make understanding and changing UpdateService easier in the long term. For example, it used to read the app details from database, then if a repo wasn't updated (due to unchanged index) then it would take the app details for that repo from the list of apps, and re-update the database (or something like that). Now, it has been refactored into the following methods: * updateOrInsertApps(appsToUpdate); * updateOrInsertApks(apksToUpdate); * removeApksFromRepos(disabledRepos); * removeApksNoLongerInRepo(appsToUpdate, updatedRepos); * removeAppsWithoutApks(); * and probably some others... Which hopefully are self-explanitory. The recent change to implement single repo updates was re-implemented with in light of the methods above. The interface to UpdateService for scheduling a single repo update is the same as it was before, but the implementation is completely different. Still works though. Using batch content provider operations for repo updates, but they suffer from the problem of not all being under the same transaction, so if an insert/update stuffs up half way through, we are left with only half of the update being complete. In the future, if there is some way to implement notifications from the content provider's applyBatch method, then we can do it all in the one transaction, and still have notifications. Currently we break it into several calls to applyBatch (and hence several transactions) to inform the user of the progress. Also adding the beginnings of some tests for AppProvider. In the future, I'll work on adding better coverage, including instrumentation to test UI features. ========================== Below is a list of many of the minor changes that also happened along the way ========================== Make "Can update" tab stay up to date using content observer, rather than manually deciding when to refresh the tab label as before. The installed app list is now cached in Utils, because it is invoked quite a few times, especially when rendering the app lists. The cache is invalidated when PackageReceiver is notified of new apps. The content providers don't notify changes if we are in batch mode. I've left the notification at the end of the batch updates as the responsibility of the UpdateService. However, it would be nice if this was somehow handled by the content, as they are really the ones who should worry about it. Made curVersion, curVercode and curApk work with providers. This was done by removing curApk (otherwise we'd need to query the db each time we fetched one app to get a reference to that apk (resulting in hundreds of queries). Instead, UpdateService now calculates curVercode and curVersion and saves them to the database. We then use these where possible. If we really need curApk (because we want info other than its version and code) we still have the option of ApkProvider.Helper.find(app.id, app.curVercode). I considered putting this inside the app value object, e.g. in getCurApk() but thought better of it as it will likely result in people invoking it all the time, without realising it causes a DB query. incompatibleReasons required a minor UI tweak, removing the "min sdk" ui element from the Apk list. It is replaced by the "Requires: %s" view (which only appears when the app is incompatible). In the process, and in response to some feedback from mvdan, I left the min sdk in there, but only made it show when in "expert mode", just like the architecture. In order to make the "installed apps" query work under test conditions, needed to change the way the InstalledApkCache be replaceable with a mock object. Pause UIL loading on fast scroll of list, as the list was very choppy for some reason. Re-added "Last repo scan" info to the Manage Repo list view. Fixed up some misc TODO's, removed some unused/empty functions.
This commit is contained in:
parent
b3773a1561
commit
da8e41249b
@ -35,6 +35,11 @@
|
||||
android:theme="@style/AppThemeDark"
|
||||
android:supportsRtl="false" >
|
||||
|
||||
<provider
|
||||
android:authorities="org.fdroid.fdroid.data.AppProvider"
|
||||
android:name="org.fdroid.fdroid.data.AppProvider"
|
||||
android:exported="false"/>
|
||||
|
||||
<provider
|
||||
android:authorities="org.fdroid.fdroid.data.RepoProvider"
|
||||
android:name="org.fdroid.fdroid.data.RepoProvider"
|
||||
|
3
TODO
3
TODO
@ -1,3 +0,0 @@
|
||||
incompatible_reasons needs to be implemented correctly for the data.Apk class (rather than DB.Apk).
|
||||
Currently, it is set during a CompatabilityChecker call to isCompatible(), which means we don't really
|
||||
know whether the field has been set or not.
|
@ -58,4 +58,11 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content" />
|
||||
|
||||
<TextView android:id="@+id/incompatible_reasons"
|
||||
android:textSize="13sp"
|
||||
android:layout_below="@id/added"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
@ -12,10 +12,4 @@
|
||||
<item>Dark</item>
|
||||
<item>Light</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="dbSyncModeNames">
|
||||
<item>Off (unsafe)</item>
|
||||
<item>Normal</item>
|
||||
<item>Full</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
@ -127,8 +127,6 @@
|
||||
|
||||
<string name="search_hint">Search applications</string>
|
||||
|
||||
<string name="db_sync_mode">Database sync mode</string>
|
||||
|
||||
<string name="appcompatibility">Application compatibility</string>
|
||||
<string name="show_incompat_versions">Incompatible versions</string>
|
||||
<string name="show_incompat_versions_on">Show app versions incompatible with the device</string>
|
||||
@ -155,6 +153,7 @@
|
||||
<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 apps compatibility with your device…</string>
|
||||
<string name="status_inserting">Saving application details (%1$d%%)</string>
|
||||
<string name="no_permissions">No permissions are used.</string>
|
||||
<string name="permissions_for_long">Permissions for version %s</string>
|
||||
<string name="showPermissions">Show permissions</string>
|
||||
@ -195,7 +194,8 @@
|
||||
<string name="repo_disabled_notification">Disabled "%1$s".\n\nYou will
|
||||
need to re-enable this repository to install apps from it.
|
||||
</string>
|
||||
<string name="minsdk_or_later">%s or later</string>
|
||||
<string name="minsdk_or_later">Android %s or later</string>
|
||||
<string name="not_on_same_wifi">Your device is not on the same WiFi as the local repo you just added! Try joining this network: %s</string>
|
||||
<string name="requires_features">Requires: %1$s</string>
|
||||
|
||||
</resources>
|
||||
|
@ -49,10 +49,5 @@
|
||||
<CheckBoxPreference android:title="@string/expert"
|
||||
android:defaultValue="false"
|
||||
android:key="expert" />
|
||||
<ListPreference android:title="@string/db_sync_mode"
|
||||
android:key="dbSyncMode"
|
||||
android:dependency="expert"
|
||||
android:defaultValue="full" android:entries="@array/dbSyncModeNames"
|
||||
android:entryValues="@array/dbSyncModeValues" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
|
@ -21,13 +21,12 @@ package org.fdroid.fdroid;
|
||||
|
||||
import java.io.File;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.data.RepoProvider;
|
||||
import android.content.*;
|
||||
import android.widget.*;
|
||||
import org.fdroid.fdroid.data.*;
|
||||
import org.xml.sax.XMLReader;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
@ -38,19 +37,10 @@ import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.Signature;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.Html.TagHandler;
|
||||
@ -64,7 +54,6 @@ import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import android.support.v4.app.NavUtils;
|
||||
@ -73,7 +62,7 @@ import android.support.v4.view.MenuItemCompat;
|
||||
import org.fdroid.fdroid.compat.PackageManagerCompat;
|
||||
import org.fdroid.fdroid.compat.ActionBarCompat;
|
||||
import org.fdroid.fdroid.compat.MenuManager;
|
||||
import org.fdroid.fdroid.DB.CommaSeparatedList;
|
||||
import org.fdroid.fdroid.Utils.CommaSeparatedList;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
@ -83,63 +72,40 @@ public class AppDetails extends ListActivity {
|
||||
|
||||
private static final int REQUEST_INSTALL = 0;
|
||||
private static final int REQUEST_UNINSTALL = 1;
|
||||
private ApkListAdapter adapter;
|
||||
|
||||
private static class ViewHolder {
|
||||
TextView version;
|
||||
TextView status;
|
||||
TextView size;
|
||||
TextView api;
|
||||
TextView incompatibleReasons;
|
||||
TextView buildtype;
|
||||
TextView added;
|
||||
TextView nativecode;
|
||||
}
|
||||
|
||||
private class ApkListAdapter extends BaseAdapter {
|
||||
private class ApkListAdapter extends ArrayAdapter<Apk> {
|
||||
|
||||
private List<Apk> items;
|
||||
private LayoutInflater mInflater;
|
||||
private LayoutInflater mInflater = (LayoutInflater) mctx.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE);
|
||||
|
||||
public ApkListAdapter(Context context, List<Apk> items) {
|
||||
this.items = new ArrayList<Apk>();
|
||||
if (items != null) {
|
||||
for (Apk apk : items) {
|
||||
this.addItem(apk);
|
||||
public ApkListAdapter(Context context, App app) {
|
||||
super(context, 0);
|
||||
List<Apk> apks = ApkProvider.Helper.findByApp(context.getContentResolver(), app.id);
|
||||
for (Apk apk : apks ) {
|
||||
if (apk.compatible || pref_incompatibleVersions) {
|
||||
add(apk);
|
||||
}
|
||||
}
|
||||
mInflater = (LayoutInflater) mctx.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE);
|
||||
}
|
||||
|
||||
public void addItem(Apk apk) {
|
||||
if (apk.compatible || pref_incompatibleVersions) {
|
||||
items.add(apk);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Apk> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return items.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return items.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
|
||||
java.text.DateFormat df = DateFormat.getDateFormat(mctx);
|
||||
Apk apk = items.get(position);
|
||||
Apk apk = getItem(position);
|
||||
ViewHolder holder;
|
||||
|
||||
if (convertView == null) {
|
||||
@ -150,6 +116,7 @@ public class AppDetails extends ListActivity {
|
||||
holder.status = (TextView) convertView.findViewById(R.id.status);
|
||||
holder.size = (TextView) convertView.findViewById(R.id.size);
|
||||
holder.api = (TextView) convertView.findViewById(R.id.api);
|
||||
holder.incompatibleReasons = (TextView) convertView.findViewById(R.id.incompatible_reasons);
|
||||
holder.buildtype = (TextView) convertView.findViewById(R.id.buildtype);
|
||||
holder.added = (TextView) convertView.findViewById(R.id.added);
|
||||
holder.nativecode = (TextView) convertView.findViewById(R.id.nativecode);
|
||||
@ -161,9 +128,9 @@ public class AppDetails extends ListActivity {
|
||||
|
||||
holder.version.setText(getString(R.string.version)
|
||||
+ " " + apk.version
|
||||
+ (apk == app.curApk ? " ☆" : ""));
|
||||
+ (apk.vercode == app.curVercode ? " ☆" : ""));
|
||||
|
||||
if (apk.vercode == app.installedVerCode
|
||||
if (apk.vercode == app.getInstalledVerCode(getContext())
|
||||
&& mInstalledSigID != null && apk.sig != null
|
||||
&& apk.sig.equals(mInstalledSigID)) {
|
||||
holder.status.setText(getString(R.string.inst));
|
||||
@ -171,14 +138,14 @@ public class AppDetails extends ListActivity {
|
||||
holder.status.setText(getString(R.string.not_inst));
|
||||
}
|
||||
|
||||
if (apk.detail_size > 0) {
|
||||
holder.size.setText(Utils.getFriendlySize(apk.detail_size));
|
||||
if (apk.size > 0) {
|
||||
holder.size.setText(Utils.getFriendlySize(apk.size));
|
||||
holder.size.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.size.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (apk.minSdkVersion > 0) {
|
||||
if (pref_expert && apk.minSdkVersion > 0) {
|
||||
holder.api.setText(getString(R.string.minsdk_or_later,
|
||||
Utils.getAndroidVersionName(apk.minSdkVersion)));
|
||||
holder.api.setVisibility(View.VISIBLE);
|
||||
@ -208,8 +175,13 @@ public class AppDetails extends ListActivity {
|
||||
}
|
||||
|
||||
if (apk.incompatible_reasons != null) {
|
||||
holder.api.setText(apk.incompatible_reasons.toString());
|
||||
holder.api.setVisibility(View.VISIBLE);
|
||||
holder.incompatibleReasons.setText(
|
||||
getResources().getString(
|
||||
R.string.requires_features,
|
||||
apk.incompatible_reasons.toPrettyString()));
|
||||
holder.incompatibleReasons.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.incompatibleReasons.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Disable it all if it isn't compatible...
|
||||
@ -219,13 +191,14 @@ public class AppDetails extends ListActivity {
|
||||
holder.status,
|
||||
holder.size,
|
||||
holder.api,
|
||||
holder.incompatibleReasons,
|
||||
holder.buildtype,
|
||||
holder.added,
|
||||
holder.nativecode
|
||||
};
|
||||
|
||||
for (View view : views) {
|
||||
view.setEnabled(apk.compatible);
|
||||
for (View v : views) {
|
||||
v.setEnabled(apk.compatible);
|
||||
}
|
||||
|
||||
return convertView;
|
||||
@ -248,7 +221,7 @@ public class AppDetails extends ListActivity {
|
||||
private static final int FLATTR = Menu.FIRST + 13;
|
||||
private static final int DONATE_URL = Menu.FIRST + 14;
|
||||
|
||||
private DB.App app;
|
||||
private App app;
|
||||
private String appid;
|
||||
private PackageManager mPm;
|
||||
private DownloadHandler downloadHandler;
|
||||
@ -336,8 +309,8 @@ public class AppDetails extends ListActivity {
|
||||
headerView = new LinearLayout(this);
|
||||
ListView lv = (ListView) findViewById(android.R.id.list);
|
||||
lv.addHeaderView(headerView);
|
||||
ApkListAdapter la = new ApkListAdapter(this, app.apks);
|
||||
setListAdapter(la);
|
||||
adapter = new ApkListAdapter(this, app);
|
||||
setListAdapter(adapter);
|
||||
|
||||
startViews();
|
||||
|
||||
@ -378,17 +351,24 @@ public class AppDetails extends ListActivity {
|
||||
}
|
||||
if (app != null && (app.ignoreAllUpdates != startingIgnoreAll
|
||||
|| app.ignoreThisUpdate != startingIgnoreThis)) {
|
||||
try {
|
||||
DB db = DB.getDB();
|
||||
db.setIgnoreUpdates(app.id,
|
||||
app.ignoreAllUpdates, app.ignoreThisUpdate);
|
||||
} finally {
|
||||
DB.releaseDB();
|
||||
}
|
||||
setIgnoreUpdates(app.id, app.ignoreAllUpdates, app.ignoreThisUpdate);
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
public void setIgnoreUpdates(String appId, boolean ignoreAll, int ignoreVersionCode) {
|
||||
|
||||
Uri uri = AppProvider.getContentUri(appId);
|
||||
|
||||
ContentValues values = new ContentValues(2);
|
||||
values.put(AppProvider.DataColumns.IGNORE_ALLUPDATES, ignoreAll ? 1 : 0);
|
||||
values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, ignoreVersionCode);
|
||||
|
||||
getContentResolver().update(uri, values, null, null);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object onRetainNonConfigurationInstance() {
|
||||
stateRetained = true;
|
||||
@ -423,15 +403,11 @@ public class AppDetails extends ListActivity {
|
||||
|
||||
Log.d("FDroid", "Getting application details for " + appid);
|
||||
app = null;
|
||||
|
||||
if (appid != null && appid.length() > 0) {
|
||||
List<DB.App> apps = ((FDroidApp) getApplication()).getApps();
|
||||
for (DB.App tapp : apps) {
|
||||
if (tapp.id.equals(appid)) {
|
||||
app = tapp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
app = AppProvider.Helper.findById(getContentResolver(), appid);
|
||||
}
|
||||
|
||||
if (app == null) {
|
||||
Toast toast = Toast.makeText(this,
|
||||
getString(R.string.no_such_app), Toast.LENGTH_LONG);
|
||||
@ -440,23 +416,13 @@ public class AppDetails extends ListActivity {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure the app is populated.
|
||||
try {
|
||||
DB db = DB.getDB();
|
||||
db.populateDetails(app, 0);
|
||||
} catch (Exception ex) {
|
||||
Log.d("FDroid", "Failed to populate app - " + ex.getMessage());
|
||||
} finally {
|
||||
DB.releaseDB();
|
||||
}
|
||||
|
||||
startingIgnoreAll = app.ignoreAllUpdates;
|
||||
startingIgnoreThis = app.ignoreThisUpdate;
|
||||
|
||||
// Get the signature of the installed package...
|
||||
mInstalledSignature = null;
|
||||
mInstalledSigID = null;
|
||||
if (app.installedVersion != null) {
|
||||
if (app.getInstalledVersion(this) != null) {
|
||||
PackageManager pm = getBaseContext().getPackageManager();
|
||||
try {
|
||||
PackageInfo pi = pm.getPackageInfo(appid,
|
||||
@ -545,7 +511,7 @@ public class AppDetails extends ListActivity {
|
||||
}
|
||||
}
|
||||
Spanned desc = Html.fromHtml(
|
||||
app.detail_description, null, new HtmlTagHandler());
|
||||
app.description, null, new HtmlTagHandler());
|
||||
tv.setText(desc.subSequence(0, desc.length() - 2));
|
||||
|
||||
tv = (TextView) infoView.findViewById(R.id.appid);
|
||||
@ -557,11 +523,20 @@ public class AppDetails extends ListActivity {
|
||||
tv = (TextView) infoView.findViewById(R.id.summary);
|
||||
tv.setText(app.summary);
|
||||
|
||||
if (pref_permissions && app.curApk != null &&
|
||||
(app.curApk.compatible || pref_incompatibleVersions)) {
|
||||
Apk curApk = null;
|
||||
for (int i = 0; i < adapter.getCount(); i ++) {
|
||||
Apk apk = adapter.getItem(i);
|
||||
if (apk.vercode == app.curVercode) {
|
||||
curApk = apk;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pref_permissions && !adapter.isEmpty() &&
|
||||
((curApk != null && curApk.compatible) || pref_incompatibleVersions)) {
|
||||
tv = (TextView) infoView.findViewById(R.id.permissions_list);
|
||||
|
||||
CommaSeparatedList permsList = app.curApk.detail_permissions;
|
||||
CommaSeparatedList permsList = adapter.getItem(0).permissions;
|
||||
if (permsList == null) {
|
||||
tv.setText(getString(R.string.no_permissions));
|
||||
} else {
|
||||
@ -586,7 +561,7 @@ public class AppDetails extends ListActivity {
|
||||
}
|
||||
tv = (TextView) infoView.findViewById(R.id.permissions);
|
||||
tv.setText(getString(
|
||||
R.string.permissions_for_long, app.apks.get(0).version));
|
||||
R.string.permissions_for_long, adapter.getItem(0).version));
|
||||
} else {
|
||||
infoView.findViewById(R.id.permissions).setVisibility(View.GONE);
|
||||
infoView.findViewById(R.id.permissions_list).setVisibility(View.GONE);
|
||||
@ -631,15 +606,14 @@ public class AppDetails extends ListActivity {
|
||||
private void updateViews() {
|
||||
|
||||
// Refresh the list...
|
||||
ApkListAdapter la = (ApkListAdapter) getListAdapter();
|
||||
la.notifyDataSetChanged();
|
||||
adapter.notifyDataSetChanged();
|
||||
|
||||
TextView tv = (TextView) findViewById(R.id.status);
|
||||
if (app.installedVersion == null)
|
||||
if (app.getInstalledVersion(this) == null)
|
||||
tv.setText(getString(R.string.details_notinstalled));
|
||||
else
|
||||
tv.setText(getString(R.string.details_installed,
|
||||
app.installedVersion));
|
||||
app.getInstalledVersion(this)));
|
||||
|
||||
tv = (TextView) infoView.findViewById(R.id.signature);
|
||||
if (pref_expert && mInstalledSignature != null) {
|
||||
@ -653,10 +627,10 @@ public class AppDetails extends ListActivity {
|
||||
|
||||
@Override
|
||||
protected void onListItemClick(ListView l, View v, int position, long id) {
|
||||
app.curApk = app.apks.get(position - l.getHeaderViewsCount());
|
||||
if (app.installedVerCode == app.curApk.vercode)
|
||||
final Apk apk = adapter.getItem(position - l.getHeaderViewsCount());
|
||||
if (app.getInstalledVerCode(this) == apk.vercode)
|
||||
removeApk(app.id);
|
||||
else if (app.installedVerCode > app.curApk.vercode) {
|
||||
else if (app.getInstalledVerCode(this) > apk.vercode) {
|
||||
AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this);
|
||||
ask_alrt.setMessage(getString(R.string.installDowngrade));
|
||||
ask_alrt.setPositiveButton(getString(R.string.yes),
|
||||
@ -664,7 +638,7 @@ public class AppDetails extends ListActivity {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog,
|
||||
int whichButton) {
|
||||
install();
|
||||
install(apk);
|
||||
}
|
||||
});
|
||||
ask_alrt.setNegativeButton(getString(R.string.no),
|
||||
@ -677,7 +651,7 @@ public class AppDetails extends ListActivity {
|
||||
AlertDialog alert = ask_alrt.create();
|
||||
alert.show();
|
||||
} else
|
||||
install();
|
||||
install(apk);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -687,20 +661,23 @@ public class AppDetails extends ListActivity {
|
||||
menu.clear();
|
||||
if (app == null)
|
||||
return true;
|
||||
if (app.toUpdate) {
|
||||
if (app.canAndWantToUpdate(this)) {
|
||||
MenuItemCompat.setShowAsAction(menu.add(
|
||||
Menu.NONE, INSTALL, 0, R.string.menu_upgrade)
|
||||
.setIcon(R.drawable.ic_menu_refresh),
|
||||
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
|
||||
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
|
||||
}
|
||||
if (app.installedVersion == null && app.curApk != null) {
|
||||
|
||||
// Check count > 0 due to incompatible apps resulting in an empty list.
|
||||
if (app.getInstalledVersion(this) == null && app.curVercode > 0 &&
|
||||
adapter.getCount() > 0) {
|
||||
MenuItemCompat.setShowAsAction(menu.add(
|
||||
Menu.NONE, INSTALL, 1, R.string.menu_install)
|
||||
.setIcon(android.R.drawable.ic_menu_add),
|
||||
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
|
||||
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
|
||||
} else if (app.installedVersion != null) {
|
||||
} else if (app.getInstalledVersion(this) != null) {
|
||||
MenuItemCompat.setShowAsAction(menu.add(
|
||||
Menu.NONE, UNINSTALL, 1, R.string.menu_uninstall)
|
||||
.setIcon(android.R.drawable.ic_menu_delete),
|
||||
@ -727,40 +704,40 @@ public class AppDetails extends ListActivity {
|
||||
.setCheckable(true)
|
||||
.setChecked(app.ignoreAllUpdates);
|
||||
|
||||
if (app.hasUpdates) {
|
||||
if (app.hasUpdates(this)) {
|
||||
menu.add(Menu.NONE, IGNORETHIS, 2, R.string.menu_ignore_this)
|
||||
.setIcon(android.R.drawable.ic_menu_close_clear_cancel)
|
||||
.setCheckable(true)
|
||||
.setChecked(app.ignoreThisUpdate >= app.curApk.vercode);
|
||||
.setChecked(app.ignoreThisUpdate >= app.curVercode);
|
||||
}
|
||||
if (app.detail_webURL.length() > 0) {
|
||||
if (app.webURL.length() > 0) {
|
||||
menu.add(Menu.NONE, WEBSITE, 3, R.string.menu_website).setIcon(
|
||||
android.R.drawable.ic_menu_view);
|
||||
}
|
||||
if (app.detail_trackerURL.length() > 0) {
|
||||
if (app.trackerURL.length() > 0) {
|
||||
menu.add(Menu.NONE, ISSUES, 4, R.string.menu_issues).setIcon(
|
||||
android.R.drawable.ic_menu_view);
|
||||
}
|
||||
if (app.detail_sourceURL.length() > 0) {
|
||||
if (app.sourceURL.length() > 0) {
|
||||
menu.add(Menu.NONE, SOURCE, 5, R.string.menu_source).setIcon(
|
||||
android.R.drawable.ic_menu_view);
|
||||
}
|
||||
|
||||
if (app.detail_bitcoinAddr != null || app.detail_litecoinAddr != null ||
|
||||
app.detail_dogecoinAddr != null ||
|
||||
app.detail_flattrID != null || app.detail_donateURL != null) {
|
||||
if (app.bitcoinAddr != null || app.litecoinAddr != null ||
|
||||
app.dogecoinAddr != null ||
|
||||
app.flattrID != null || app.donateURL != null) {
|
||||
SubMenu donate = menu.addSubMenu(Menu.NONE, DONATE, 7,
|
||||
R.string.menu_donate).setIcon(
|
||||
android.R.drawable.ic_menu_send);
|
||||
if (app.detail_bitcoinAddr != null)
|
||||
if (app.bitcoinAddr != null)
|
||||
donate.add(Menu.NONE, BITCOIN, 8, R.string.menu_bitcoin);
|
||||
if (app.detail_litecoinAddr != null)
|
||||
if (app.litecoinAddr != null)
|
||||
donate.add(Menu.NONE, LITECOIN, 8, R.string.menu_litecoin);
|
||||
if (app.detail_dogecoinAddr != null)
|
||||
if (app.dogecoinAddr != null)
|
||||
donate.add(Menu.NONE, DOGECOIN, 8, R.string.menu_dogecoin);
|
||||
if (app.detail_flattrID != null)
|
||||
if (app.flattrID != null)
|
||||
donate.add(Menu.NONE, FLATTR, 9, R.string.menu_flattr);
|
||||
if (app.detail_donateURL != null)
|
||||
if (app.donateURL != null)
|
||||
donate.add(Menu.NONE, DONATE_URL, 10, R.string.menu_website);
|
||||
}
|
||||
|
||||
@ -798,8 +775,10 @@ public class AppDetails extends ListActivity {
|
||||
|
||||
case INSTALL:
|
||||
// Note that this handles updating as well as installing.
|
||||
if (app.curApk != null)
|
||||
install();
|
||||
if (app.curVercode > 0) {
|
||||
final Apk apkToInstall = ApkProvider.Helper.find(this, app.id, app.curVercode);
|
||||
install(apkToInstall);
|
||||
}
|
||||
return true;
|
||||
|
||||
case UNINSTALL:
|
||||
@ -812,43 +791,43 @@ public class AppDetails extends ListActivity {
|
||||
return true;
|
||||
|
||||
case IGNORETHIS:
|
||||
if (app.ignoreThisUpdate >= app.curApk.vercode)
|
||||
if (app.ignoreThisUpdate >= app.curVercode)
|
||||
app.ignoreThisUpdate = 0;
|
||||
else
|
||||
app.ignoreThisUpdate = app.curApk.vercode;
|
||||
app.ignoreThisUpdate = app.curVercode;
|
||||
item.setChecked(app.ignoreThisUpdate > 0);
|
||||
return true;
|
||||
|
||||
case WEBSITE:
|
||||
tryOpenUri(app.detail_webURL);
|
||||
tryOpenUri(app.webURL);
|
||||
return true;
|
||||
|
||||
case ISSUES:
|
||||
tryOpenUri(app.detail_trackerURL);
|
||||
tryOpenUri(app.trackerURL);
|
||||
return true;
|
||||
|
||||
case SOURCE:
|
||||
tryOpenUri(app.detail_sourceURL);
|
||||
tryOpenUri(app.sourceURL);
|
||||
return true;
|
||||
|
||||
case BITCOIN:
|
||||
tryOpenUri("bitcoin:" + app.detail_bitcoinAddr);
|
||||
tryOpenUri("bitcoin:" + app.bitcoinAddr);
|
||||
return true;
|
||||
|
||||
case LITECOIN:
|
||||
tryOpenUri("litecoin:" + app.detail_litecoinAddr);
|
||||
tryOpenUri("litecoin:" + app.litecoinAddr);
|
||||
return true;
|
||||
|
||||
case DOGECOIN:
|
||||
tryOpenUri("dogecoin:" + app.detail_dogecoinAddr);
|
||||
tryOpenUri("dogecoin:" + app.dogecoinAddr);
|
||||
return true;
|
||||
|
||||
case FLATTR:
|
||||
tryOpenUri("https://flattr.com/thing/" + app.detail_flattrID);
|
||||
tryOpenUri("https://flattr.com/thing/" + app.flattrID);
|
||||
return true;
|
||||
|
||||
case DONATE_URL:
|
||||
tryOpenUri(app.detail_donateURL);
|
||||
tryOpenUri(app.donateURL);
|
||||
return true;
|
||||
|
||||
}
|
||||
@ -856,17 +835,16 @@ public class AppDetails extends ListActivity {
|
||||
}
|
||||
|
||||
// Install the version of this app denoted by 'app.curApk'.
|
||||
private void install() {
|
||||
|
||||
private void install(final Apk apk) {
|
||||
String [] projection = { RepoProvider.DataColumns.ADDRESS };
|
||||
Repo repo = RepoProvider.Helper.findById(
|
||||
getContentResolver(), app.curApk.repo, projection);
|
||||
getContentResolver(), apk.repo, projection);
|
||||
if (repo == null || repo.address == null) {
|
||||
return;
|
||||
}
|
||||
final String repoaddress = repo.address;
|
||||
|
||||
if (!app.curApk.compatible) {
|
||||
if (!apk.compatible) {
|
||||
AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this);
|
||||
ask_alrt.setMessage(getString(R.string.installIncompatible));
|
||||
ask_alrt.setPositiveButton(getString(R.string.yes),
|
||||
@ -874,7 +852,7 @@ public class AppDetails extends ListActivity {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog,
|
||||
int whichButton) {
|
||||
downloadHandler = new DownloadHandler(app.curApk,
|
||||
downloadHandler = new DownloadHandler(apk,
|
||||
repoaddress, Utils
|
||||
.getApkCacheDir(getBaseContext()));
|
||||
}
|
||||
@ -890,8 +868,8 @@ public class AppDetails extends ListActivity {
|
||||
alert.show();
|
||||
return;
|
||||
}
|
||||
if (mInstalledSigID != null && app.curApk.sig != null
|
||||
&& !app.curApk.sig.equals(mInstalledSigID)) {
|
||||
if (mInstalledSigID != null && apk.sig != null
|
||||
&& !apk.sig.equals(mInstalledSigID)) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(R.string.SignatureMismatch).setPositiveButton(
|
||||
getString(R.string.ok),
|
||||
@ -905,7 +883,7 @@ public class AppDetails extends ListActivity {
|
||||
alert.show();
|
||||
return;
|
||||
}
|
||||
downloadHandler = new DownloadHandler(app.curApk, repoaddress,
|
||||
downloadHandler = new DownloadHandler(apk, repoaddress,
|
||||
Utils.getApkCacheDir(getBaseContext()));
|
||||
}
|
||||
|
||||
@ -937,7 +915,7 @@ public class AppDetails extends ListActivity {
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void shareApp(DB.App app) {
|
||||
private void shareApp(App app) {
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
shareIntent.setType("text/plain");
|
||||
|
||||
|
@ -21,28 +21,25 @@ package org.fdroid.fdroid;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
|
||||
public class AppFilter {
|
||||
|
||||
boolean pref_rooted;
|
||||
|
||||
public AppFilter(Context ctx) {
|
||||
|
||||
// Read preferences and cache them so we can do quick lookups.
|
||||
SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(ctx);
|
||||
pref_rooted = prefs.getBoolean(Preferences.PREF_ROOTED, true);
|
||||
}
|
||||
|
||||
// Return true if the given app should be filtered out based on user
|
||||
// preferences, and false otherwise.
|
||||
public boolean filter(DB.App app) {
|
||||
if (app.requirements == null) return false;
|
||||
public boolean filter(App app) {
|
||||
|
||||
boolean filterRequiringRoot = Preferences.get().filterAppsRequiringRoot();
|
||||
|
||||
if (app.requirements == null || !filterRequiringRoot) return false;
|
||||
|
||||
for (String r : app.requirements) {
|
||||
if (r.equals("root") && !pref_rooted)
|
||||
if (r.equals("root"))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,248 +0,0 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.os.Build;
|
||||
|
||||
import org.fdroid.fdroid.views.AppListAdapter;
|
||||
import org.fdroid.fdroid.views.AvailableAppListAdapter;
|
||||
import org.fdroid.fdroid.views.CanUpdateAppListAdapter;
|
||||
import org.fdroid.fdroid.views.InstalledAppListAdapter;
|
||||
|
||||
/**
|
||||
* Should be owned by the FDroid Activity, but used by the AppListFragments.
|
||||
* The idea is that it takes a non-trivial amount of time to work this stuff
|
||||
* out, and it is quicker if we only do it once for each view, rather than
|
||||
* each fragment figuring out their own list independently.
|
||||
*/
|
||||
public class AppListManager {
|
||||
|
||||
private List<DB.App> allApps = null;
|
||||
|
||||
private FDroid fdroidActivity;
|
||||
|
||||
private AppListAdapter availableApps;
|
||||
private AppListAdapter installedApps;
|
||||
private AppListAdapter canUpgradeApps;
|
||||
private ArrayAdapter<String> categories;
|
||||
|
||||
private String currentCategory = null;
|
||||
private String categoryAll = null;
|
||||
private String categoryWhatsNew = null;
|
||||
private String categoryRecentlyUpdated = null;
|
||||
|
||||
public AppListAdapter getAvailableAdapter() {
|
||||
return availableApps;
|
||||
}
|
||||
|
||||
public AppListAdapter getInstalledAdapter() {
|
||||
return installedApps;
|
||||
}
|
||||
|
||||
public AppListAdapter getCanUpdateAdapter() {
|
||||
return canUpgradeApps;
|
||||
}
|
||||
|
||||
public ArrayAdapter<String> getCategoriesAdapter() {
|
||||
return categories;
|
||||
}
|
||||
|
||||
public AppListManager(FDroid activity) {
|
||||
this.fdroidActivity = activity;
|
||||
|
||||
availableApps = new AvailableAppListAdapter(fdroidActivity);
|
||||
installedApps = new InstalledAppListAdapter(fdroidActivity);
|
||||
canUpgradeApps = new CanUpdateAppListAdapter(fdroidActivity);
|
||||
|
||||
// Needs to be created before createViews(), because that will use the
|
||||
// getCategoriesAdapter() accessor which expects this object...
|
||||
categories = new ArrayAdapter<String>(activity,
|
||||
android.R.layout.simple_spinner_item, new ArrayList<String>());
|
||||
categories
|
||||
.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
installedApps.clear();
|
||||
availableApps.clear();
|
||||
canUpgradeApps.clear();
|
||||
categories.clear();
|
||||
}
|
||||
|
||||
private void notifyLists() {
|
||||
// Tell the lists that the data behind the adapter has changed, so
|
||||
// they can refresh...
|
||||
availableApps.notifyDataSetChanged();
|
||||
installedApps.notifyDataSetChanged();
|
||||
canUpgradeApps.notifyDataSetChanged();
|
||||
categories.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void updateCategories() {
|
||||
try {
|
||||
DB db = DB.getDB();
|
||||
|
||||
// Populate the category list with the real categories, and the
|
||||
// locally generated meta-categories for "All", "What's New" and
|
||||
// "Recently Updated"...
|
||||
categoryAll = fdroidActivity
|
||||
.getString(R.string.category_all);
|
||||
categoryWhatsNew = fdroidActivity
|
||||
.getString(R.string.category_whatsnew);
|
||||
categoryRecentlyUpdated = fdroidActivity
|
||||
.getString(R.string.category_recentlyupdated);
|
||||
|
||||
categories.add(categoryWhatsNew);
|
||||
categories.add(categoryRecentlyUpdated);
|
||||
categories.add(categoryAll);
|
||||
if (Build.VERSION.SDK_INT >= 11) {
|
||||
categories.addAll(db.getCategories());
|
||||
} else {
|
||||
List<String> categs = db.getCategories();
|
||||
for (String category : categs) {
|
||||
categories.add(category);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentCategory == null)
|
||||
currentCategory = categoryWhatsNew;
|
||||
|
||||
} finally {
|
||||
DB.releaseDB();
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the FDroid activity to update its "Update (x)" tab to correctly
|
||||
// reflect the number of updates available.
|
||||
private void notifyActivity() {
|
||||
fdroidActivity.refreshUpdateTabLabel();
|
||||
}
|
||||
|
||||
public void repopulateLists() {
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
clear();
|
||||
|
||||
updateCategories();
|
||||
updateApps();
|
||||
notifyLists();
|
||||
notifyActivity();
|
||||
|
||||
Log.d("FDroid", "Updated lists - " + allApps.size() + " in total"
|
||||
+ " (update took " + (System.currentTimeMillis() - startTime)
|
||||
+ " ms)");
|
||||
}
|
||||
|
||||
// Calculate the cutoff date we'll use for What's New and Recently
|
||||
// Updated...
|
||||
private Date calcMaxHistory() {
|
||||
SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(fdroidActivity.getBaseContext());
|
||||
String daysPreference = prefs.getString(Preferences.PREF_UPD_HISTORY, "14");
|
||||
int maxHistoryDays = Integer.parseInt(daysPreference);
|
||||
Calendar recent = Calendar.getInstance();
|
||||
recent.add(Calendar.DAY_OF_YEAR, -maxHistoryDays);
|
||||
return recent.getTime();
|
||||
}
|
||||
|
||||
// recentDate could really be calculated here, but this is just a hack so
|
||||
// it doesn't need to be calculated for every single app. The reason it
|
||||
// isn't an instance variable is because the preferences may change, and
|
||||
// we wouldn't know.
|
||||
private boolean isInCategory(DB.App app, String category, Date recentDate) {
|
||||
if (category.equals(categoryAll)) {
|
||||
return true;
|
||||
}
|
||||
if (category.equals(categoryWhatsNew)) {
|
||||
if (app.added == null)
|
||||
return false;
|
||||
if (app.added.compareTo(recentDate) < 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
if (category.equals(categoryRecentlyUpdated)) {
|
||||
if (app.lastUpdated == null)
|
||||
return false;
|
||||
// Don't include in the recently updated category if the
|
||||
// 'update' was actually it being added.
|
||||
if (app.lastUpdated.compareTo(app.added) == 0)
|
||||
return false;
|
||||
if (app.lastUpdated.compareTo(recentDate) < 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
if (app.categories == null) return false;
|
||||
return app.categories.contains(category);
|
||||
}
|
||||
|
||||
// Returns false if the app list is empty and the fdroid activity decided
|
||||
// to attempt updating it.
|
||||
private boolean updateApps() {
|
||||
|
||||
allApps = ((FDroidApp)fdroidActivity.getApplication()).getApps();
|
||||
|
||||
if (allApps.isEmpty()) {
|
||||
// If its the first time we've run the app, this should update
|
||||
// the repos. If not, it will do nothing, presuming that the repos
|
||||
// are invalid, the internet is stuffed, the sky has fallen, etc...
|
||||
return fdroidActivity.updateEmptyRepos();
|
||||
}
|
||||
|
||||
Date recentDate = calcMaxHistory();
|
||||
List<DB.App> availApps = new ArrayList<DB.App>();
|
||||
for (DB.App app : allApps) {
|
||||
|
||||
// Add it to the list(s). Always to installed and updates, but
|
||||
// only to available if it's not filtered.
|
||||
if (isInCategory(app, currentCategory, recentDate)) {
|
||||
availApps.add(app);
|
||||
}
|
||||
if (app.installedVersion != null) {
|
||||
installedApps.addItem(app);
|
||||
if (app.toUpdate) {
|
||||
canUpgradeApps.addItem(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentCategory.equals(categoryWhatsNew)) {
|
||||
Collections.sort(availApps, new WhatsNewComparator());
|
||||
} else if (currentCategory.equals(categoryRecentlyUpdated)) {
|
||||
Collections.sort(availApps, new RecentlyUpdatedComparator());
|
||||
}
|
||||
|
||||
availableApps.addItems(availApps);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setCurrentCategory(String currentCategory) {
|
||||
if (!this.currentCategory.equals(currentCategory)){
|
||||
this.currentCategory = currentCategory;
|
||||
repopulateLists();
|
||||
}
|
||||
}
|
||||
|
||||
public String getCurrentCategory() {
|
||||
return this.currentCategory;
|
||||
}
|
||||
|
||||
static class WhatsNewComparator implements Comparator<DB.App> {
|
||||
@Override
|
||||
public int compare(DB.App lhs, DB.App rhs) {
|
||||
return rhs.added.compareTo(lhs.added);
|
||||
}
|
||||
}
|
||||
|
||||
static class RecentlyUpdatedComparator implements Comparator<DB.App> {
|
||||
@Override
|
||||
public int compare(DB.App lhs, DB.App rhs) {
|
||||
return rhs.lastUpdated.compareTo(lhs.lastUpdated);
|
||||
}
|
||||
}
|
||||
}
|
103
src/org/fdroid/fdroid/CompatibilityChecker.java
Normal file
103
src/org/fdroid/fdroid/CompatibilityChecker.java
Normal file
@ -0,0 +1,103 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.FeatureInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.compat.Compatibility;
|
||||
import org.fdroid.fdroid.compat.SupportedArchitectures;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
// Call getIncompatibleReasons(apk) on an instance of this class to
|
||||
// find reasons why an apk may be incompatible with the user's device.
|
||||
public class CompatibilityChecker extends Compatibility {
|
||||
|
||||
private Context context;
|
||||
private Set<String> features;
|
||||
private Set<String> cpuAbis;
|
||||
private String cpuAbisDesc;
|
||||
private boolean ignoreTouchscreen;
|
||||
|
||||
public CompatibilityChecker(Context ctx) {
|
||||
|
||||
context = ctx.getApplicationContext();
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
|
||||
ignoreTouchscreen = prefs.getBoolean(Preferences.PREF_IGN_TOUCH, false);
|
||||
|
||||
PackageManager pm = ctx.getPackageManager();
|
||||
StringBuilder logMsg = new StringBuilder();
|
||||
logMsg.append("Available device features:");
|
||||
features = new HashSet<String>();
|
||||
if (pm != null) {
|
||||
for (FeatureInfo fi : pm.getSystemAvailableFeatures()) {
|
||||
features.add(fi.name);
|
||||
logMsg.append('\n');
|
||||
logMsg.append(fi.name);
|
||||
}
|
||||
}
|
||||
|
||||
cpuAbis = SupportedArchitectures.getAbis();
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
boolean first = true;
|
||||
for (String abi : cpuAbis) {
|
||||
if (first) first = false;
|
||||
else builder.append(", ");
|
||||
builder.append(abi);
|
||||
}
|
||||
cpuAbisDesc = builder.toString();
|
||||
|
||||
Log.d("FDroid", logMsg.toString());
|
||||
}
|
||||
|
||||
private boolean compatibleApi(Utils.CommaSeparatedList nativecode) {
|
||||
if (nativecode == null) return true;
|
||||
for (String abi : nativecode) {
|
||||
if (cpuAbis.contains(abi)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<String> getIncompatibleReasons(final Apk apk) {
|
||||
|
||||
List<String> incompatibleReasons = new ArrayList<String>();
|
||||
|
||||
if (!hasApi(apk.minSdkVersion)) {
|
||||
incompatibleReasons.add(
|
||||
context.getResources().getString(
|
||||
R.string.minsdk_or_later,
|
||||
Utils.getAndroidVersionName(apk.minSdkVersion)));
|
||||
}
|
||||
|
||||
if (apk.features != null) {
|
||||
for (String feat : apk.features) {
|
||||
if (ignoreTouchscreen
|
||||
&& feat.equals("android.hardware.touchscreen")) {
|
||||
// Don't check it!
|
||||
} else if (!features.contains(feat)) {
|
||||
Collections.addAll(incompatibleReasons, feat.split(","));
|
||||
Log.d("FDroid", apk.id + " vercode " + apk.vercode
|
||||
+ " is incompatible based on lack of "
|
||||
+ feat);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!compatibleApi(apk.nativecode)) {
|
||||
for (String code : apk.nativecode) {
|
||||
incompatibleReasons.add(code);
|
||||
}
|
||||
Log.d("FDroid", apk.id + " vercode " + apk.vercode
|
||||
+ " only supports " + Utils.CommaSeparatedList.str(apk.nativecode)
|
||||
+ " while your architectures are " + cpuAbisDesc);
|
||||
}
|
||||
|
||||
return incompatibleReasons;
|
||||
}
|
||||
}
|
@ -1,943 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2010-13 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;
|
||||
|
||||
import java.io.File;
|
||||
import java.security.MessageDigest;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Formatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.FeatureInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextUtils.SimpleStringSplitter;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
|
||||
import org.fdroid.fdroid.compat.Compatibility;
|
||||
import org.fdroid.fdroid.compat.ContextCompat;
|
||||
import org.fdroid.fdroid.compat.SupportedArchitectures;
|
||||
import org.fdroid.fdroid.data.*;
|
||||
|
||||
public class DB {
|
||||
|
||||
private static Semaphore dbSync = new Semaphore(1, true);
|
||||
private static DB dbInstance = null;
|
||||
|
||||
// Initialise the database. Called once when the application starts up.
|
||||
static void initDB(Context ctx) {
|
||||
dbInstance = new DB(ctx);
|
||||
}
|
||||
|
||||
// Get access to the database. Must be called before any database activity,
|
||||
// and releaseDB must be called subsequently. Returns null in the event of
|
||||
// failure.
|
||||
public static DB getDB() {
|
||||
try {
|
||||
dbSync.acquire();
|
||||
return dbInstance;
|
||||
} catch (InterruptedException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Release database access lock acquired via getDB().
|
||||
public static void releaseDB() {
|
||||
dbSync.release();
|
||||
}
|
||||
|
||||
// Possible values of the SQLite flag "synchronous"
|
||||
public static final int SYNC_OFF = 0;
|
||||
public static final int SYNC_NORMAL = 1;
|
||||
public static final int SYNC_FULL = 2;
|
||||
|
||||
private SQLiteDatabase db;
|
||||
|
||||
public static final String TABLE_APP = "fdroid_app";
|
||||
|
||||
public static class App implements Comparable<App> {
|
||||
|
||||
public App() {
|
||||
name = "Unknown";
|
||||
summary = "Unknown application";
|
||||
icon = null;
|
||||
id = "unknown";
|
||||
license = "Unknown";
|
||||
detail_trackerURL = null;
|
||||
detail_sourceURL = null;
|
||||
detail_donateURL = null;
|
||||
detail_bitcoinAddr = null;
|
||||
detail_litecoinAddr = null;
|
||||
detail_dogecoinAddr = null;
|
||||
detail_webURL = null;
|
||||
categories = null;
|
||||
antiFeatures = null;
|
||||
requirements = null;
|
||||
hasUpdates = false;
|
||||
toUpdate = false;
|
||||
updated = false;
|
||||
added = null;
|
||||
lastUpdated = null;
|
||||
apks = new ArrayList<Apk>();
|
||||
detail_Populated = false;
|
||||
compatible = false;
|
||||
ignoreAllUpdates = false;
|
||||
ignoreThisUpdate = 0;
|
||||
filtered = false;
|
||||
iconUrl = null;
|
||||
}
|
||||
|
||||
// True when all the detail fields are populated, False otherwise.
|
||||
public boolean detail_Populated;
|
||||
|
||||
// True if compatible with the device (i.e. if at least one apk is)
|
||||
public boolean compatible;
|
||||
|
||||
public String id;
|
||||
public String name;
|
||||
public String summary;
|
||||
public String icon;
|
||||
|
||||
// Null when !detail_Populated
|
||||
public String detail_description;
|
||||
|
||||
public String license;
|
||||
|
||||
// Null when !detail_Populated
|
||||
public String detail_webURL;
|
||||
|
||||
// Null when !detail_Populated
|
||||
public String detail_trackerURL;
|
||||
|
||||
// Null when !detail_Populated
|
||||
public String detail_sourceURL;
|
||||
|
||||
// Donate link, or null
|
||||
// Null when !detail_Populated
|
||||
public String detail_donateURL;
|
||||
|
||||
// Bitcoin donate address, or null
|
||||
// Null when !detail_Populated
|
||||
public String detail_bitcoinAddr;
|
||||
|
||||
// Litecoin donate address, or null
|
||||
// Null when !detail_Populated
|
||||
public String detail_litecoinAddr;
|
||||
|
||||
// Dogecoin donate address, or null
|
||||
// Null when !detail_Populated
|
||||
public String detail_dogecoinAddr;
|
||||
|
||||
// Flattr donate ID, or null
|
||||
// Null when !detail_Populated
|
||||
public String detail_flattrID;
|
||||
|
||||
public String curVersion;
|
||||
public int curVercode;
|
||||
public Apk curApk;
|
||||
public Date added;
|
||||
public Date lastUpdated;
|
||||
|
||||
// Installed version (or null), version code and whether it was
|
||||
// installed by the user or bundled with the system. These are valid
|
||||
// only when getApps() has been called with getinstalledinfo=true.
|
||||
public String installedVersion;
|
||||
public int installedVerCode;
|
||||
public boolean userInstalled;
|
||||
|
||||
// List of categories (as defined in the metadata
|
||||
// documentation) or null if there aren't any.
|
||||
public CommaSeparatedList categories;
|
||||
|
||||
// List of anti-features (as defined in the metadata
|
||||
// documentation) or null if there aren't any.
|
||||
public CommaSeparatedList antiFeatures;
|
||||
|
||||
// List of special requirements (such as root privileges) or
|
||||
// null if there aren't any.
|
||||
public CommaSeparatedList requirements;
|
||||
|
||||
// Whether the app is filtered or not based on AntiFeatures and root
|
||||
// permission (set in the Settings page)
|
||||
public boolean filtered;
|
||||
|
||||
// True if there are new versions (apks) available, regardless of
|
||||
// any filtering
|
||||
public boolean hasUpdates;
|
||||
|
||||
// True if there are new versions (apks) available and the user wants
|
||||
// to be notified about them
|
||||
public boolean toUpdate;
|
||||
|
||||
// True if all updates for this app are to be ignored
|
||||
public boolean ignoreAllUpdates;
|
||||
|
||||
// True if the current update for this app is to be ignored
|
||||
public int ignoreThisUpdate;
|
||||
|
||||
// Used internally for tracking during repo updates.
|
||||
public boolean updated;
|
||||
|
||||
// List of apks.
|
||||
public List<Apk> apks;
|
||||
|
||||
public String iconUrl;
|
||||
|
||||
// Get the current version - this will be one of the Apks from 'apks'.
|
||||
// Can return null if there are no available versions.
|
||||
// This should be the 'current' version, as in the most recent stable
|
||||
// one, that most users would want by default. It might not be the
|
||||
// most recent, if for example there are betas etc.
|
||||
public Apk getCurrentVersion() {
|
||||
|
||||
// Try and return the real current version first. It will find the
|
||||
// closest version smaller than the curVercode, being the same
|
||||
// vercode if it exists.
|
||||
if (curVercode > 0) {
|
||||
int latestcode = -1;
|
||||
Apk latestapk = null;
|
||||
for (Apk apk : apks) {
|
||||
if ((!this.compatible || apk.compatible)
|
||||
&& apk.vercode <= curVercode
|
||||
&& apk.vercode > latestcode) {
|
||||
latestapk = apk;
|
||||
latestcode = apk.vercode;
|
||||
}
|
||||
}
|
||||
return latestapk;
|
||||
}
|
||||
|
||||
// If the current version was not set we return the most recent apk.
|
||||
if (curVercode == -1) {
|
||||
int latestcode = -1;
|
||||
Apk latestapk = null;
|
||||
for (Apk apk : apks) {
|
||||
if ((!this.compatible || apk.compatible)
|
||||
&& apk.vercode > latestcode) {
|
||||
latestapk = apk;
|
||||
latestcode = apk.vercode;
|
||||
}
|
||||
}
|
||||
return latestapk;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(App arg0) {
|
||||
return name.compareToIgnoreCase(arg0.name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static String calcFingerprint(String keyHexString) {
|
||||
if (TextUtils.isEmpty(keyHexString))
|
||||
return null;
|
||||
else
|
||||
return calcFingerprint(Hasher.unhex(keyHexString));
|
||||
}
|
||||
|
||||
public static String calcFingerprint(Certificate cert) {
|
||||
try {
|
||||
return calcFingerprint(cert.getEncoded());
|
||||
} catch (CertificateEncodingException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String calcFingerprint(byte[] key) {
|
||||
String ret = null;
|
||||
try {
|
||||
// keytool -list -v gives you the SHA-256 fingerprint
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
digest.update(key);
|
||||
byte[] fingerprint = digest.digest();
|
||||
Formatter formatter = new Formatter(new StringBuilder());
|
||||
for (int i = 1; i < fingerprint.length; i++) {
|
||||
formatter.format("%02X", fingerprint[i]);
|
||||
}
|
||||
ret = formatter.toString();
|
||||
formatter.close();
|
||||
} catch (Exception e) {
|
||||
Log.w("FDroid", "Unable to get certificate fingerprint.\n"
|
||||
+ Log.getStackTraceString(e));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the local storage (cache) path. This will also create it if
|
||||
* it doesn't exist. It can return null if it's currently unavailable.
|
||||
*/
|
||||
public static File getDataPath(Context ctx) {
|
||||
return ContextCompat.create(ctx).getExternalCacheDir();
|
||||
}
|
||||
|
||||
private Context mContext;
|
||||
private Apk.CompatibilityChecker compatChecker = null;
|
||||
|
||||
// The date format used for storing dates (e.g. lastupdated, added) in the
|
||||
// database.
|
||||
public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
|
||||
|
||||
private DB(Context ctx) {
|
||||
|
||||
mContext = ctx;
|
||||
DBHelper h = new DBHelper(ctx);
|
||||
db = h.getWritableDatabase();
|
||||
SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(mContext);
|
||||
String sync_mode = prefs.getString(Preferences.PREF_DB_SYNC, null);
|
||||
if ("off".equals(sync_mode))
|
||||
setSynchronizationMode(SYNC_OFF);
|
||||
else if ("normal".equals(sync_mode))
|
||||
setSynchronizationMode(SYNC_NORMAL);
|
||||
else if ("full".equals(sync_mode))
|
||||
setSynchronizationMode(SYNC_FULL);
|
||||
else
|
||||
sync_mode = null;
|
||||
if (sync_mode != null)
|
||||
Log.d("FDroid", "Database synchronization mode: " + sync_mode);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
db.close();
|
||||
db = null;
|
||||
}
|
||||
|
||||
// Delete the database, which should cause it to be re-created next time
|
||||
// it's used.
|
||||
public static void delete(Context ctx) {
|
||||
try {
|
||||
ctx.deleteDatabase(DBHelper.DATABASE_NAME);
|
||||
// Also try and delete the old one, from versions 0.13 and earlier.
|
||||
ctx.deleteDatabase("fdroid_db");
|
||||
} catch (Exception ex) {
|
||||
Log.e("FDroid",
|
||||
"Exception in DB.delete:\n" + Log.getStackTraceString(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getCategories() {
|
||||
List<String> result = new ArrayList<String>();
|
||||
Cursor c = null;
|
||||
try {
|
||||
c = db.query(true, TABLE_APP, new String[] { "categories" },
|
||||
null, null, null, null, null, null);
|
||||
c.moveToFirst();
|
||||
while (!c.isAfterLast()) {
|
||||
CommaSeparatedList categories = CommaSeparatedList
|
||||
.make(c.getString(0));
|
||||
if (categories != null) {
|
||||
for (String category : categories) {
|
||||
if (!result.contains(category)) {
|
||||
result.add(category);
|
||||
}
|
||||
}
|
||||
}
|
||||
c.moveToNext();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("FDroid",
|
||||
"Exception during database reading:\n"
|
||||
+ Log.getStackTraceString(e));
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
Collections.sort(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final String[] POPULATE_APP_COLS = new String[] {
|
||||
"description", "webURL", "trackerURL", "sourceURL",
|
||||
"donateURL", "bitcoinAddr", "flattrID", "litecoinAddr", "dogecoinAddr" };
|
||||
|
||||
private void populateAppDetails(App app) {
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = db.query(TABLE_APP, POPULATE_APP_COLS, "id = ?",
|
||||
new String[] { app.id }, null, null, null, null);
|
||||
cursor.moveToFirst();
|
||||
app.detail_description = cursor.getString(0);
|
||||
app.detail_webURL = cursor.getString(1);
|
||||
app.detail_trackerURL = cursor.getString(2);
|
||||
app.detail_sourceURL = cursor.getString(3);
|
||||
app.detail_donateURL = cursor.getString(4);
|
||||
app.detail_bitcoinAddr = cursor.getString(5);
|
||||
app.detail_flattrID = cursor.getString(6);
|
||||
app.detail_litecoinAddr = cursor.getString(7);
|
||||
app.detail_dogecoinAddr = cursor.getString(8);
|
||||
app.detail_Populated = true;
|
||||
} catch (Exception e) {
|
||||
Log.d("FDroid", "Error populating app details " + app.id );
|
||||
Log.d("FDroid", e.getMessage());
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final String[] POPULATE_APK_COLS = new String[] {
|
||||
ApkProvider.DataColumns.HASH,
|
||||
ApkProvider.DataColumns.HASH_TYPE,
|
||||
ApkProvider.DataColumns.SIZE,
|
||||
ApkProvider.DataColumns.PERMISSIONS
|
||||
};
|
||||
|
||||
private void populateApkDetails(Apk apk, long repo) {
|
||||
if (repo == 0 || repo == apk.repo) {
|
||||
Apk loadedApk = ApkProvider.Helper.find(
|
||||
mContext, apk.id, apk.vercode, POPULATE_APK_COLS);
|
||||
apk.detail_hash = loadedApk.detail_hash;
|
||||
apk.detail_hashType = loadedApk.detail_hashType;
|
||||
apk.detail_size = loadedApk.detail_size;
|
||||
apk.detail_permissions = loadedApk.detail_permissions;
|
||||
} else {
|
||||
Log.d("FDroid", "Not setting details for apk '" + apk.id + "' (version " + apk.version +") because it belongs to a different repo.");
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the details for the given app, if necessary.
|
||||
// If 'apkrepo' is non-zero, only apks from that repo are
|
||||
// populated (this is used during the update process)
|
||||
public void populateDetails(App app, long apkRepo) {
|
||||
if (!app.detail_Populated) {
|
||||
populateAppDetails(app);
|
||||
}
|
||||
|
||||
for (Apk apk : app.apks) {
|
||||
if (apk.detail_hash == null) {
|
||||
populateApkDetails(apk, apkRepo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return a list of apps matching the given criteria. Filtering is
|
||||
// also done based on compatibility and anti-features according to
|
||||
// the user's current preferences.
|
||||
public List<App> getApps(boolean getinstalledinfo) {
|
||||
|
||||
// If we're going to need it, get info in what's currently installed
|
||||
Map<String, PackageInfo> systemApks = null;
|
||||
if (getinstalledinfo) {
|
||||
Log.d("FDroid", "Reading installed packages");
|
||||
systemApks = new HashMap<String, PackageInfo>();
|
||||
List<PackageInfo> installedPackages = mContext.getPackageManager()
|
||||
.getInstalledPackages(0);
|
||||
for (PackageInfo appInfo : installedPackages) {
|
||||
systemApks.put(appInfo.packageName, appInfo);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, App> apps = new HashMap<String, App>();
|
||||
Cursor c = null;
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
|
||||
String cols[] = new String[] { "antiFeatures", "requirements",
|
||||
"categories", "id", "name", "summary", "icon", "license",
|
||||
"curVersion", "curVercode", "added", "lastUpdated",
|
||||
"compatible", "ignoreAllUpdates", "ignoreThisUpdate" };
|
||||
c = db.query(TABLE_APP, cols, null, null, null, null, null);
|
||||
c.moveToFirst();
|
||||
while (!c.isAfterLast()) {
|
||||
|
||||
App app = new App();
|
||||
app.antiFeatures = DB.CommaSeparatedList.make(c.getString(0));
|
||||
app.requirements = DB.CommaSeparatedList.make(c.getString(1));
|
||||
app.categories = DB.CommaSeparatedList.make(c.getString(2));
|
||||
app.id = c.getString(3);
|
||||
app.name = c.getString(4);
|
||||
app.summary = c.getString(5);
|
||||
app.icon = c.getString(6);
|
||||
app.license = c.getString(7);
|
||||
app.curVersion = c.getString(8);
|
||||
app.curVercode = c.getInt(9);
|
||||
String sAdded = c.getString(10);
|
||||
app.added = (sAdded == null || sAdded.length() == 0) ? null
|
||||
: DATE_FORMAT.parse(sAdded);
|
||||
String sLastUpdated = c.getString(11);
|
||||
app.lastUpdated = (sLastUpdated == null || sLastUpdated
|
||||
.length() == 0) ? null : DATE_FORMAT
|
||||
.parse(sLastUpdated);
|
||||
app.compatible = c.getInt(12) == 1;
|
||||
app.ignoreAllUpdates = c.getInt(13) == 1;
|
||||
app.ignoreThisUpdate = c.getInt(14);
|
||||
app.hasUpdates = false;
|
||||
|
||||
if (getinstalledinfo && systemApks.containsKey(app.id)) {
|
||||
PackageInfo sysapk = systemApks.get(app.id);
|
||||
app.installedVersion = sysapk.versionName;
|
||||
if (app.installedVersion == null)
|
||||
app.installedVersion = "null";
|
||||
app.installedVerCode = sysapk.versionCode;
|
||||
if (sysapk.applicationInfo != null) {
|
||||
app.userInstalled = ((sysapk.applicationInfo.flags
|
||||
& ApplicationInfo.FLAG_SYSTEM) != 1);
|
||||
}
|
||||
} else {
|
||||
app.installedVersion = null;
|
||||
app.installedVerCode = 0;
|
||||
app.userInstalled = false;
|
||||
}
|
||||
|
||||
apps.put(app.id, app);
|
||||
|
||||
c.moveToNext();
|
||||
}
|
||||
c.close();
|
||||
c = null;
|
||||
|
||||
Log.d("FDroid", "Read app data from database " + " (took "
|
||||
+ (System.currentTimeMillis() - startTime) + " ms)");
|
||||
|
||||
DisplayMetrics metrics = mContext.getResources()
|
||||
.getDisplayMetrics();
|
||||
String iconsDir = null;
|
||||
if (metrics.densityDpi >= 640) {
|
||||
iconsDir = "/icons-640/";
|
||||
} else if (metrics.densityDpi >= 480) {
|
||||
iconsDir = "/icons-480/";
|
||||
} else if (metrics.densityDpi >= 320) {
|
||||
iconsDir = "/icons-320/";
|
||||
} else if (metrics.densityDpi >= 240) {
|
||||
iconsDir = "/icons-240/";
|
||||
} else if (metrics.densityDpi >= 160) {
|
||||
iconsDir = "/icons-160/";
|
||||
} else {
|
||||
iconsDir = "/icons-120/";
|
||||
}
|
||||
metrics = null;
|
||||
Log.d("FDroid", "Density-specific icons dir is " + iconsDir);
|
||||
|
||||
List<Apk> apks = ApkProvider.Helper.all(mContext);
|
||||
for (Apk apk : apks) {
|
||||
App app = apps.get(apk.id);
|
||||
if (app == null) {
|
||||
continue;
|
||||
}
|
||||
app.apks.add(apk);
|
||||
if (app.iconUrl == null && app.icon != null) {
|
||||
if (apk.repoVersion >= 11) {
|
||||
app.iconUrl = apk.repoAddress + iconsDir + app.icon;
|
||||
} else {
|
||||
app.iconUrl = apk.repoAddress + "/icons/" + app.icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e("FDroid",
|
||||
"Exception during database reading:\n"
|
||||
+ Log.getStackTraceString(e));
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
|
||||
Log.d("FDroid", "Read app and apk data from database " + " (took "
|
||||
+ (System.currentTimeMillis() - startTime) + " ms)");
|
||||
}
|
||||
|
||||
List<App> result = new ArrayList<App>(apps.values());
|
||||
Collections.sort(result);
|
||||
|
||||
// Fill in the hasUpdates fields if we have the necessary information...
|
||||
if (getinstalledinfo) {
|
||||
|
||||
// We'll say an application has updates if it's installed AND the
|
||||
// version is older than the current one
|
||||
for (App app : result) {
|
||||
app.curApk = app.getCurrentVersion();
|
||||
if (app.curApk != null
|
||||
&& app.installedVerCode > 0
|
||||
&& app.installedVerCode < app.curApk.vercode) {
|
||||
app.hasUpdates = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// Alternative to getApps() that only refreshes the installation details
|
||||
// of those apps in invalidApps. Much faster when returning from
|
||||
// installs/uninstalls, where getApps() was already called before.
|
||||
public List<App> refreshApps(List<App> apps, List<String> invalidApps) {
|
||||
|
||||
List<PackageInfo> installedPackages = mContext.getPackageManager()
|
||||
.getInstalledPackages(0);
|
||||
long startTime = System.currentTimeMillis();
|
||||
List<String> refreshedApps = new ArrayList<String>();
|
||||
for (String appid : invalidApps) {
|
||||
if (refreshedApps.contains(appid)) continue;
|
||||
App app = null;
|
||||
int index = -1;
|
||||
for (App oldapp : apps) {
|
||||
index++;
|
||||
if (oldapp.id.equals(appid)) {
|
||||
app = oldapp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (app == null) continue;
|
||||
|
||||
PackageInfo installed = null;
|
||||
|
||||
for (PackageInfo appInfo : installedPackages) {
|
||||
if (appInfo.packageName.equals(appid)) {
|
||||
installed = appInfo;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (installed != null) {
|
||||
app.installedVersion = installed.versionName;
|
||||
if (app.installedVersion == null)
|
||||
app.installedVersion = "null";
|
||||
app.installedVerCode = installed.versionCode;
|
||||
} else {
|
||||
app.installedVersion = null;
|
||||
app.installedVerCode = 0;
|
||||
}
|
||||
|
||||
app.hasUpdates = false;
|
||||
app.curApk = app.getCurrentVersion();
|
||||
if (app.curApk != null
|
||||
&& app.installedVersion != null
|
||||
&& app.installedVerCode < app.curApk.vercode) {
|
||||
app.hasUpdates = true;
|
||||
}
|
||||
|
||||
apps.set(index, app);
|
||||
refreshedApps.add(appid);
|
||||
}
|
||||
Log.d("FDroid", "Refreshing " + refreshedApps.size() + " apps took "
|
||||
+ (System.currentTimeMillis() - startTime) + " ms");
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
public List<String> doSearch(String query) {
|
||||
|
||||
List<String> ids = new ArrayList<String>();
|
||||
Cursor c = null;
|
||||
try {
|
||||
String filter = "%" + query + "%";
|
||||
c = db.query(TABLE_APP, new String[] { "id" },
|
||||
"id like ? or name like ? or summary like ? or description like ?",
|
||||
new String[] { filter, filter, filter, filter }, null, null, null);
|
||||
c.moveToFirst();
|
||||
while (!c.isAfterLast()) {
|
||||
ids.add(c.getString(0));
|
||||
c.moveToNext();
|
||||
}
|
||||
} finally {
|
||||
if (c != null)
|
||||
c.close();
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
public static class CommaSeparatedList implements Iterable<String> {
|
||||
private String value;
|
||||
|
||||
private CommaSeparatedList(String list) {
|
||||
value = list;
|
||||
}
|
||||
|
||||
public static CommaSeparatedList make(String list) {
|
||||
if (list == null || list.length() == 0)
|
||||
return null;
|
||||
else
|
||||
return new CommaSeparatedList(list);
|
||||
}
|
||||
|
||||
public static String str(CommaSeparatedList instance) {
|
||||
return (instance == null ? null : instance.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<String> iterator() {
|
||||
SimpleStringSplitter splitter = new SimpleStringSplitter(',');
|
||||
splitter.setString(value);
|
||||
return splitter.iterator();
|
||||
}
|
||||
|
||||
public boolean contains(String v) {
|
||||
for (String s : this) {
|
||||
if (s.equals(v))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<App> updateApps = null;
|
||||
|
||||
// Called before a repo update starts.
|
||||
public void beginUpdate(List<DB.App> apps) {
|
||||
// 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
|
||||
// it to true when we see the app/apk in a repository. Thus, at the
|
||||
// end, any that are still false can be removed.
|
||||
updateApps = apps;
|
||||
Log.d("FDroid", "AppUpdate: " + updateApps.size() + " apps before starting.");
|
||||
// Wrap the whole update in a transaction. Make sure to call
|
||||
// either endUpdate or cancelUpdate to commit or discard it,
|
||||
// respectively.
|
||||
db.beginTransaction();
|
||||
}
|
||||
|
||||
// Called when a repo update ends. Any applications that have not been
|
||||
// updated (by a call to updateApplication) are assumed to be no longer
|
||||
// in the repos.
|
||||
public void endUpdate() {
|
||||
if (updateApps == null)
|
||||
return;
|
||||
Log.d("FDroid", "Processing endUpdate - " + updateApps.size()
|
||||
+ " apps before");
|
||||
for (App app : updateApps) {
|
||||
if (!app.updated) {
|
||||
// The application hasn't been updated, so it's no longer
|
||||
// in the repos.
|
||||
Log.d("FDroid", "AppUpdate: " + app.name
|
||||
+ " is no longer in any repository - removing");
|
||||
db.delete(TABLE_APP, "id = ?", new String[]{app.id});
|
||||
ApkProvider.Helper.deleteApksByApp(mContext, app);
|
||||
} else {
|
||||
for (Apk apk : app.apks) {
|
||||
if (!apk.updated) {
|
||||
// The package hasn't been updated, so this is a
|
||||
// version that's no longer available.
|
||||
Log.d("FDroid", "AppUpdate: Package " + apk.id + "/"
|
||||
+ apk.version
|
||||
+ " is no longer in any repository - removing");
|
||||
ApkProvider.Helper.delete(mContext, app.id, apk.vercode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Commit updates to the database.
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
Log.d("FDroid", "AppUpdate: " + updateApps.size()
|
||||
+ " apps on completion.");
|
||||
updateApps = null;
|
||||
}
|
||||
|
||||
// Called instead of endUpdate if the update failed.
|
||||
public void cancelUpdate() {
|
||||
if (updateApps != null) {
|
||||
db.endTransaction();
|
||||
updateApps = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Called during update to supply new details for an application (or
|
||||
// details of a completely new one). Calls to this must be wrapped by
|
||||
// a call to beginUpdate and a call to endUpdate.
|
||||
public void updateApplication(App upapp) {
|
||||
|
||||
if (updateApps == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Lazy initialise this...
|
||||
if (compatChecker == null) {
|
||||
compatChecker = new Apk.CompatibilityChecker(mContext);
|
||||
}
|
||||
|
||||
// See if it's compatible (by which we mean if it has at least one
|
||||
// compatible apk)
|
||||
upapp.compatible = false;
|
||||
for (Apk apk : upapp.apks) {
|
||||
if (compatChecker.isCompatible(apk)) {
|
||||
apk.compatible = true;
|
||||
upapp.compatible = true;
|
||||
}
|
||||
}
|
||||
|
||||
boolean found = false;
|
||||
for (App app : updateApps) {
|
||||
if (app.id.equals(upapp.id)) {
|
||||
updateApp(app, upapp);
|
||||
app.updated = true;
|
||||
found = true;
|
||||
for (Apk upapk : upapp.apks) {
|
||||
boolean afound = false;
|
||||
for (Apk apk : app.apks) {
|
||||
if (apk.vercode == upapk.vercode) {
|
||||
|
||||
ApkProvider.Helper.update(
|
||||
mContext,
|
||||
upapk,
|
||||
apk.id,
|
||||
apk.vercode);
|
||||
|
||||
apk.updated = true;
|
||||
afound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!afound) {
|
||||
// A new version of this application.
|
||||
ApkProvider.Helper.insert(mContext, upapk);
|
||||
upapk.updated = true;
|
||||
app.apks.add(upapk);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// It's a brand new application...
|
||||
updateApp(null, upapp);
|
||||
for (Apk upapk : upapp.apks) {
|
||||
ApkProvider.Helper.insert(mContext, upapk);
|
||||
upapk.updated = true;
|
||||
}
|
||||
upapp.updated = true;
|
||||
updateApps.add(upapp);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Update application details in the database.
|
||||
// 'oldapp' - previous details - i.e. what's in the database.
|
||||
// If null, this app is not in the database at all and
|
||||
// should be added.
|
||||
// 'upapp' - updated details
|
||||
private void updateApp(App oldapp, App upapp) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("id", upapp.id);
|
||||
values.put("name", upapp.name);
|
||||
values.put("summary", upapp.summary);
|
||||
values.put("icon", upapp.icon);
|
||||
values.put("description", upapp.detail_description);
|
||||
values.put("license", upapp.license);
|
||||
values.put("webURL", upapp.detail_webURL);
|
||||
values.put("trackerURL", upapp.detail_trackerURL);
|
||||
values.put("sourceURL", upapp.detail_sourceURL);
|
||||
values.put("donateURL", upapp.detail_donateURL);
|
||||
values.put("bitcoinAddr", upapp.detail_bitcoinAddr);
|
||||
values.put("litecoinAddr", upapp.detail_litecoinAddr);
|
||||
values.put("dogecoinAddr", upapp.detail_dogecoinAddr);
|
||||
values.put("flattrID", upapp.detail_flattrID);
|
||||
values.put("added",
|
||||
upapp.added == null ? "" : DATE_FORMAT.format(upapp.added));
|
||||
values.put(
|
||||
"lastUpdated",
|
||||
upapp.added == null ? "" : DATE_FORMAT
|
||||
.format(upapp.lastUpdated));
|
||||
values.put("curVersion", upapp.curVersion);
|
||||
values.put("curVercode", upapp.curVercode);
|
||||
values.put("categories", CommaSeparatedList.str(upapp.categories));
|
||||
values.put("antiFeatures", CommaSeparatedList.str(upapp.antiFeatures));
|
||||
values.put("requirements", CommaSeparatedList.str(upapp.requirements));
|
||||
values.put("compatible", upapp.compatible ? 1 : 0);
|
||||
|
||||
// Values to keep if already present
|
||||
if (oldapp == null) {
|
||||
values.put("ignoreAllUpdates", upapp.ignoreAllUpdates ? 1 : 0);
|
||||
values.put("ignoreThisUpdate", upapp.ignoreThisUpdate);
|
||||
} else {
|
||||
values.put("ignoreAllUpdates", oldapp.ignoreAllUpdates ? 1 : 0);
|
||||
values.put("ignoreThisUpdate", oldapp.ignoreThisUpdate);
|
||||
}
|
||||
|
||||
if (oldapp != null) {
|
||||
db.update(TABLE_APP, values, "id = ?", new String[] { oldapp.id });
|
||||
} else {
|
||||
db.insert(TABLE_APP, null, values);
|
||||
}
|
||||
}
|
||||
|
||||
public void setIgnoreUpdates(String appid, boolean All, int This) {
|
||||
db.execSQL("update " + TABLE_APP + " set"
|
||||
+ " ignoreAllUpdates=" + (All ? '1' : '0')
|
||||
+ ", ignoreThisUpdate="+This
|
||||
+ " where id = ?", new String[] { appid });
|
||||
}
|
||||
|
||||
public void purgeApps(Repo repo, FDroidApp fdroid) {
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
ApkProvider.Helper.deleteApksByRepo(mContext, repo);
|
||||
List<App> apps = getApps(false);
|
||||
for (App app : apps) {
|
||||
if (app.apks.isEmpty()) {
|
||||
db.delete(TABLE_APP, "id = ?", new String[] { app.id });
|
||||
}
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
fdroid.invalidateAllApps();
|
||||
}
|
||||
|
||||
public int getSynchronizationMode() {
|
||||
Cursor cursor = db.rawQuery("PRAGMA synchronous", null);
|
||||
cursor.moveToFirst();
|
||||
int mode = cursor.getInt(0);
|
||||
cursor.close();
|
||||
return mode;
|
||||
}
|
||||
|
||||
public void setSynchronizationMode(int mode) {
|
||||
db.execSQL("PRAGMA synchronous = " + mode);
|
||||
}
|
||||
}
|
@ -108,8 +108,8 @@ public class Downloader extends Thread {
|
||||
// See if we already have this apk cached...
|
||||
if (localfile.exists()) {
|
||||
// We do - if its hash matches, we'll use it...
|
||||
Hasher hash = new Hasher(curapk.detail_hashType, localfile);
|
||||
if (hash.match(curapk.detail_hash)) {
|
||||
Hasher hash = new Hasher(curapk.hashType, localfile);
|
||||
if (hash.match(curapk.hash)) {
|
||||
Log.d("FDroid", "Using cached apk at " + localfile);
|
||||
synchronized (this) {
|
||||
progress = 1;
|
||||
@ -130,7 +130,7 @@ public class Downloader extends Thread {
|
||||
synchronized (this) {
|
||||
filename = remotefile;
|
||||
progress = 0;
|
||||
max = curapk.detail_size;
|
||||
max = curapk.size;
|
||||
status = Status.RUNNING;
|
||||
}
|
||||
|
||||
@ -159,11 +159,11 @@ public class Downloader extends Thread {
|
||||
}
|
||||
return;
|
||||
}
|
||||
Hasher hash = new Hasher(curapk.detail_hashType, localfile);
|
||||
if (!hash.match(curapk.detail_hash)) {
|
||||
Hasher hash = new Hasher(curapk.hashType, localfile);
|
||||
if (!hash.match(curapk.hash)) {
|
||||
synchronized (this) {
|
||||
Log.d("FDroid", "Downloaded file hash of " + hash.getHash()
|
||||
+ " did not match repo's " + curapk.detail_hash);
|
||||
+ " did not match repo's " + curapk.hash);
|
||||
// No point keeping a bad file, whether we're
|
||||
// caching or not.
|
||||
localfile.delete();
|
||||
|
@ -25,9 +25,11 @@ import android.app.NotificationManager;
|
||||
import android.content.*;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.database.ContentObserver;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
@ -41,6 +43,7 @@ import android.support.v4.view.MenuItemCompat;
|
||||
import android.support.v4.view.ViewPager;
|
||||
|
||||
import org.fdroid.fdroid.compat.TabManager;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.views.AppListFragmentPageAdapter;
|
||||
|
||||
public class FDroid extends FragmentActivity {
|
||||
@ -58,21 +61,14 @@ public class FDroid extends FragmentActivity {
|
||||
|
||||
private ViewPager viewPager;
|
||||
|
||||
private AppListManager manager = null;
|
||||
|
||||
private TabManager tabManager = null;
|
||||
|
||||
public AppListManager getManager() {
|
||||
return manager;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
||||
((FDroidApp) getApplication()).applyTheme(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
manager = new AppListManager(this);
|
||||
setContentView(R.layout.fdroid);
|
||||
createViews();
|
||||
getTabManager().createTabs();
|
||||
@ -99,21 +95,9 @@ public class FDroid extends FragmentActivity {
|
||||
call.putExtra("appid", appid);
|
||||
startActivityForResult(call, REQUEST_APPDETAILS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
repopulateViews();
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be done *after* createViews, because it will involve a
|
||||
* callback to update the tab label for the "update" tab. This
|
||||
* will fail unless the tabs have actually been created.
|
||||
*/
|
||||
protected void repopulateViews() {
|
||||
manager.repopulateLists();
|
||||
Uri uri = AppProvider.getContentUri();
|
||||
getContentResolver().registerContentObserver(uri, true, new AppObserver());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -253,8 +237,6 @@ public class FDroid extends FragmentActivity {
|
||||
|
||||
if ((resultCode & PreferencesActivity.RESULT_RELOAD) != 0) {
|
||||
((FDroidApp) getApplication()).invalidateAllApps();
|
||||
} else if ((resultCode & PreferencesActivity.RESULT_REFILTER) != 0) {
|
||||
((FDroidApp) getApplication()).filterApps();
|
||||
}
|
||||
|
||||
if ((resultCode & PreferencesActivity.RESULT_RESTART) != 0) {
|
||||
@ -308,14 +290,7 @@ public class FDroid extends FragmentActivity {
|
||||
// is told to do the update, which will result in the database changing. The
|
||||
// UpdateReceiver class should get told when this is finished.
|
||||
public void updateRepos() {
|
||||
UpdateService.updateNow(this).setListener(new ProgressListener() {
|
||||
@Override
|
||||
public void onProgress(Event event) {
|
||||
if (event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES){
|
||||
repopulateViews();
|
||||
}
|
||||
}
|
||||
});
|
||||
UpdateService.updateNow(this);
|
||||
}
|
||||
|
||||
private TabManager getTabManager() {
|
||||
@ -335,4 +310,27 @@ public class FDroid extends FragmentActivity {
|
||||
nMgr.cancel(id);
|
||||
}
|
||||
|
||||
private class AppObserver extends ContentObserver {
|
||||
|
||||
public AppObserver() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(boolean selfChange, Uri uri) {
|
||||
FDroid.this.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
refreshUpdateTabLabel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
onChange(selfChange, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -38,23 +38,19 @@ import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.fdroid.fdroid.Utils;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
import com.nostra13.universalimageloader.cache.disc.impl.LimitedAgeDiscCache;
|
||||
import com.nostra13.universalimageloader.cache.disc.impl.UnlimitedDiscCache;
|
||||
import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator;
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
||||
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
||||
import com.nostra13.universalimageloader.utils.StorageUtils;
|
||||
|
||||
import de.duenndns.ssl.MemorizingTrustManager;
|
||||
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.thoughtcrime.ssl.pinning.PinningTrustManager;
|
||||
import org.thoughtcrime.ssl.pinning.SystemKeyStore;
|
||||
|
||||
@ -90,6 +86,20 @@ public class FDroidApp extends Application {
|
||||
// it is more deterministic as to when this gets called...
|
||||
Preferences.setup(this);
|
||||
|
||||
// Set this up here, and the testing framework will override it when
|
||||
// it gets fired up.
|
||||
Utils.setupInstalledApkCache(new Utils.InstalledApkCache());
|
||||
|
||||
// If the user changes the preference to do with filtering rooted apps,
|
||||
// it is easier to just notify a change in the app provider,
|
||||
// so that the newly updated list will correctly filter relevant apps.
|
||||
Preferences.get().registerAppsRequiringRootChangeListener(new Preferences.ChangeListener() {
|
||||
@Override
|
||||
public void onPreferenceChange() {
|
||||
getContentResolver().notifyChange(AppProvider.getContentUri(), null);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear cached apk files. We used to just remove them after they'd
|
||||
// been installed, but this causes problems for proprietary gapps
|
||||
// users since the introduction of verification (on pre-4.2 Android),
|
||||
@ -117,7 +127,6 @@ public class FDroidApp extends Application {
|
||||
apps = null;
|
||||
invalidApps = new ArrayList<String>();
|
||||
ctx = getApplicationContext();
|
||||
DB.initDB(ctx);
|
||||
UpdateService.schedule(ctx);
|
||||
|
||||
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(ctx)
|
||||
@ -179,7 +188,7 @@ public class FDroidApp extends Application {
|
||||
private Context ctx;
|
||||
|
||||
// Global list of all known applications.
|
||||
private List<DB.App> apps;
|
||||
private List<App> apps;
|
||||
|
||||
// Set when something has changed (database or installed apps) so we know
|
||||
// we should invalidate the apps.
|
||||
@ -206,59 +215,4 @@ public class FDroidApp extends Application {
|
||||
invalidApps.add(id);
|
||||
}
|
||||
|
||||
// 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
|
||||
// contents should never be modified, it's for reading only.
|
||||
public List<DB.App> getApps() {
|
||||
|
||||
boolean invalid = false;
|
||||
try {
|
||||
appsInvalidLock.acquire();
|
||||
invalid = appsAllInvalid;
|
||||
if (invalid) {
|
||||
appsAllInvalid = false;
|
||||
Log.d("FDroid", "Dropping cached app data");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Don't care
|
||||
} finally {
|
||||
appsInvalidLock.release();
|
||||
}
|
||||
|
||||
if (apps == null || invalid) {
|
||||
try {
|
||||
DB db = DB.getDB();
|
||||
apps = db.getApps(true);
|
||||
|
||||
} finally {
|
||||
DB.releaseDB();
|
||||
}
|
||||
} else if (!invalidApps.isEmpty()) {
|
||||
try {
|
||||
DB db = DB.getDB();
|
||||
apps = db.refreshApps(apps, invalidApps);
|
||||
|
||||
invalidApps.clear();
|
||||
} finally {
|
||||
DB.releaseDB();
|
||||
}
|
||||
}
|
||||
if (apps == null)
|
||||
return new ArrayList<DB.App>();
|
||||
filterApps();
|
||||
return apps;
|
||||
}
|
||||
|
||||
public void filterApps() {
|
||||
AppFilter appFilter = new AppFilter(ctx);
|
||||
for (DB.App app : apps) {
|
||||
app.filtered = appFilter.filter(app);
|
||||
|
||||
app.toUpdate = (app.hasUpdates
|
||||
&& !app.ignoreAllUpdates
|
||||
&& app.curApk.vercode > app.ignoreThisUpdate
|
||||
&& !app.filtered);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -21,13 +21,11 @@ package org.fdroid.fdroid;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.*;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v4.app.ListFragment;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.net.wifi.WifiInfo;
|
||||
@ -38,6 +36,7 @@ import android.support.v4.app.NavUtils;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v4.view.MenuItemCompat;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateFormat;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@ -55,6 +54,7 @@ import org.fdroid.fdroid.views.fragments.RepoDetailsFragment;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public class ManageRepo extends FragmentActivity {
|
||||
@ -178,7 +178,7 @@ class RepoListFragment extends ListFragment
|
||||
changed = true;
|
||||
} else {
|
||||
FDroidApp app = (FDroidApp)getActivity().getApplication();
|
||||
RepoProvider.Helper.purgeApps(repo, app);
|
||||
RepoProvider.Helper.purgeApps(getActivity(), repo, app);
|
||||
String notification = getString(R.string.repo_disabled_notification, repo.name);
|
||||
Toast.makeText(getActivity(), notification, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
@ -200,6 +200,43 @@ class RepoListFragment extends ListFragment
|
||||
*/
|
||||
private boolean isImportingRepo = false;
|
||||
|
||||
private View createHeaderView() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
TextView textLastUpdate = new TextView(getActivity());
|
||||
long lastUpdate = prefs.getLong(Preferences.PREF_UPD_LAST, 0);
|
||||
String lastUpdateCheck = "";
|
||||
if (lastUpdate == 0) {
|
||||
lastUpdateCheck = getString(R.string.never);
|
||||
} else {
|
||||
Date d = new Date(lastUpdate);
|
||||
lastUpdateCheck = DateFormat.getDateFormat(getActivity()).format(d) +
|
||||
" " + DateFormat.getTimeFormat(getActivity()).format(d);
|
||||
}
|
||||
textLastUpdate.setText(getString(R.string.last_update_check, lastUpdateCheck));
|
||||
|
||||
int sidePadding = (int)getResources().getDimension(R.dimen.padding_side);
|
||||
int topPadding = (int)getResources().getDimension(R.dimen.padding_top);
|
||||
|
||||
textLastUpdate.setPadding(sidePadding, topPadding, sidePadding, topPadding);
|
||||
return textLastUpdate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
// Can't do this in the onCreate view, because "onCreateView" which
|
||||
// returns the list view is "called between onCreate and
|
||||
// onActivityCreated" according to the docs.
|
||||
getListView().addHeaderView(createHeaderView());
|
||||
|
||||
// This could go in onCreate (and used to) but it needs to be called
|
||||
// after addHeaderView, which can only be called after onCreate...
|
||||
repoAdapter = new RepoAdapter(getActivity(), null);
|
||||
repoAdapter.setEnabledListener(this);
|
||||
setListAdapter(repoAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
|
||||
@ -207,29 +244,6 @@ class RepoListFragment extends ListFragment
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
repoAdapter = new RepoAdapter(getActivity(), null);
|
||||
repoAdapter.setEnabledListener(this);
|
||||
setListAdapter(repoAdapter);
|
||||
|
||||
/*
|
||||
TODO: Find some other way to display this info, now that we use the ListView widgets...
|
||||
SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(getBaseContext());
|
||||
|
||||
TextView tv_lastCheck = (TextView)findViewById(R.id.lastUpdateCheck);
|
||||
long lastUpdate = prefs.getLong(Preferences.PREF_UPD_LAST, 0);
|
||||
String s_lastUpdateCheck = "";
|
||||
if (lastUpdate == 0) {
|
||||
s_lastUpdateCheck = getString(R.string.never);
|
||||
} else {
|
||||
Date d = new Date(lastUpdate);
|
||||
s_lastUpdateCheck = DateFormat.getDateFormat(this).format(d) +
|
||||
" " + DateFormat.getTimeFormat(this).format(d);
|
||||
}
|
||||
tv_lastCheck.setText(getString(R.string.last_update_check,s_lastUpdateCheck));
|
||||
|
||||
*/
|
||||
|
||||
/* let's see if someone is trying to send us a new repo */
|
||||
Intent intent = getActivity().getIntent();
|
||||
/* an URL from a click, NFC, QRCode scan, etc */
|
||||
@ -342,9 +356,12 @@ class RepoListFragment extends ListFragment
|
||||
UpdateService.updateNow(getActivity()).setListener(new ProgressListener() {
|
||||
@Override
|
||||
public void onProgress(Event event) {
|
||||
if (event.type == UpdateService.STATUS_COMPLETE_AND_SAME ||
|
||||
event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES) {
|
||||
// No need to prompt to update any more, we just did it!
|
||||
changed = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@ public class PackageReceiver extends BroadcastReceiver {
|
||||
String appid = intent.getData().getSchemeSpecificPart();
|
||||
Log.d("FDroid", "PackageReceiver received "+appid);
|
||||
((FDroidApp) ctx.getApplicationContext()).invalidateApp(appid);
|
||||
Utils.clearInstalledApksCache();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import android.app.LoaderManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
@ -38,15 +36,20 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
|
||||
public static final String PREF_IGN_TOUCH = "ignoreTouchscreen";
|
||||
public static final String PREF_CACHE_APK = "cacheDownloaded";
|
||||
public static final String PREF_EXPERT = "expert";
|
||||
public static final String PREF_DB_SYNC = "dbSyncMode";
|
||||
public static final String PREF_UPD_LAST = "lastUpdateCheck";
|
||||
|
||||
private static final boolean DEFAULT_COMPACT_LAYOUT = false;
|
||||
private static final boolean DEFAULT_ROOTED = true;
|
||||
private static final int DEFAULT_UPD_HISTORY = 14;
|
||||
|
||||
private boolean compactLayout = DEFAULT_COMPACT_LAYOUT;
|
||||
private boolean filterAppsRequiringRoot = DEFAULT_ROOTED;
|
||||
|
||||
private Map<String,Boolean> initialized = new HashMap<String,Boolean>();
|
||||
|
||||
private List<ChangeListener> compactLayoutListeners = new ArrayList<ChangeListener>();
|
||||
private List<ChangeListener> filterAppsRequiringRootListeners = new ArrayList<ChangeListener>();
|
||||
private List<ChangeListener> updateHistoryListeners = new ArrayList<ChangeListener>();
|
||||
|
||||
private boolean isInitialized(String key) {
|
||||
return initialized.containsKey(key) && initialized.get(key);
|
||||
@ -76,6 +79,45 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
|
||||
compactLayoutListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the cutoff date we'll use for What's New and Recently
|
||||
* Updated...
|
||||
*/
|
||||
public Date calcMaxHistory() {
|
||||
String daysString = preferences.getString(PREF_UPD_HISTORY, Integer.toString(DEFAULT_UPD_HISTORY));
|
||||
int maxHistoryDays;
|
||||
try {
|
||||
maxHistoryDays = Integer.parseInt(daysString);
|
||||
} catch (NumberFormatException e) {
|
||||
maxHistoryDays = DEFAULT_UPD_HISTORY;
|
||||
}
|
||||
Calendar recent = Calendar.getInstance();
|
||||
recent.add(Calendar.DAY_OF_YEAR, -maxHistoryDays);
|
||||
return recent.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is cached as it is called several times inside the AppListAdapter.
|
||||
* Providing it here means sthe shared preferences file only needs to be
|
||||
* read once, and we will keep our copy up to date by listening to changes
|
||||
* in PREF_ROOTED.
|
||||
*/
|
||||
public boolean filterAppsRequiringRoot() {
|
||||
if (!isInitialized(PREF_ROOTED)) {
|
||||
initialize(PREF_ROOTED);
|
||||
filterAppsRequiringRoot = preferences.getBoolean(PREF_ROOTED, DEFAULT_ROOTED);
|
||||
}
|
||||
return filterAppsRequiringRoot;
|
||||
}
|
||||
|
||||
public void registerAppsRequiringRootChangeListener(ChangeListener listener) {
|
||||
filterAppsRequiringRootListeners.add(listener);
|
||||
}
|
||||
|
||||
public void unregisterAppsRequiringRootChangeListener(ChangeListener listener) {
|
||||
filterAppsRequiringRootListeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
Log.d("FDroid", "Invalidating preference '" + key + "'.");
|
||||
@ -85,9 +127,29 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
|
||||
for ( ChangeListener listener : compactLayoutListeners ) {
|
||||
listener.onPreferenceChange();
|
||||
}
|
||||
} else if (key.equals(PREF_ROOTED)) {
|
||||
for ( ChangeListener listener : filterAppsRequiringRootListeners ) {
|
||||
listener.onPreferenceChange();
|
||||
}
|
||||
} else if (key.equals(PREF_UPD_HISTORY)) {
|
||||
for ( ChangeListener listener : updateHistoryListeners ) {
|
||||
listener.onPreferenceChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void registerUpdateHistoryListener(ChangeListener listener) {
|
||||
updateHistoryListeners.add(listener);
|
||||
}
|
||||
|
||||
public void unregisterUpdateHistoryListener(ChangeListener listener) {
|
||||
updateHistoryListeners.remove(listener);
|
||||
}
|
||||
|
||||
public static interface ChangeListener {
|
||||
public void onPreferenceChange();
|
||||
}
|
||||
|
||||
private static Preferences instance;
|
||||
|
||||
public static void setup(Context context) {
|
||||
@ -110,8 +172,4 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static interface ChangeListener {
|
||||
public void onPreferenceChange();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -37,7 +37,6 @@ public class PreferencesActivity extends PreferenceActivity implements
|
||||
OnSharedPreferenceChangeListener {
|
||||
|
||||
public static final int RESULT_RELOAD = 1;
|
||||
public static final int RESULT_REFILTER = 2;
|
||||
public static final int RESULT_RESTART = 4;
|
||||
private int result = 0;
|
||||
|
||||
@ -53,8 +52,7 @@ public class PreferencesActivity extends PreferenceActivity implements
|
||||
Preferences.PREF_COMPACT_LAYOUT,
|
||||
Preferences.PREF_IGN_TOUCH,
|
||||
Preferences.PREF_CACHE_APK,
|
||||
Preferences.PREF_EXPERT,
|
||||
Preferences.PREF_DB_SYNC
|
||||
Preferences.PREF_EXPERT
|
||||
};
|
||||
|
||||
@Override
|
||||
@ -133,10 +131,6 @@ public class PreferencesActivity extends PreferenceActivity implements
|
||||
} else if (key.equals(Preferences.PREF_ROOTED)) {
|
||||
onoffSummary(key, R.string.rooted_on,
|
||||
R.string.rooted_off);
|
||||
if (changing) {
|
||||
result ^= RESULT_REFILTER;
|
||||
setResult(result);
|
||||
}
|
||||
|
||||
} else if (key.equals(Preferences.PREF_IGN_TOUCH)) {
|
||||
onoffSummary(key, R.string.ignoreTouch_on,
|
||||
@ -150,8 +144,6 @@ public class PreferencesActivity extends PreferenceActivity implements
|
||||
onoffSummary(key, R.string.expert_on,
|
||||
R.string.expert_off);
|
||||
|
||||
} else if (key.equals(Preferences.PREF_DB_SYNC)) {
|
||||
entrySummary(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ package org.fdroid.fdroid;
|
||||
|
||||
import android.os.Bundle;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.updater.RepoUpdater;
|
||||
import org.xml.sax.Attributes;
|
||||
@ -28,19 +29,17 @@ import org.xml.sax.SAXException;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
public class RepoXMLHandler extends DefaultHandler {
|
||||
|
||||
// The repo we're processing.
|
||||
private Repo repo;
|
||||
|
||||
private Map<String, DB.App> apps;
|
||||
private List<DB.App> appsList;
|
||||
private List<App> apps = new ArrayList<App>();
|
||||
private List<Apk> apksList = new ArrayList<Apk>();
|
||||
|
||||
private DB.App curapp = null;
|
||||
private App curapp = null;
|
||||
private Apk curapk = null;
|
||||
private StringBuilder curchars = new StringBuilder();
|
||||
|
||||
@ -64,17 +63,22 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
|
||||
private int totalAppCount;
|
||||
|
||||
public RepoXMLHandler(Repo repo, List<DB.App> appsList, ProgressListener listener) {
|
||||
public RepoXMLHandler(Repo repo, ProgressListener listener) {
|
||||
this.repo = repo;
|
||||
this.apps = new HashMap<String, DB.App>();
|
||||
for (DB.App app : appsList) this.apps.put(app.id, app);
|
||||
this.appsList = appsList;
|
||||
pubkey = null;
|
||||
name = null;
|
||||
description = null;
|
||||
progressListener = listener;
|
||||
}
|
||||
|
||||
public List<App> getApps() {
|
||||
return apps;
|
||||
}
|
||||
|
||||
public List<Apk> getApks() {
|
||||
return apksList;
|
||||
}
|
||||
|
||||
public int getMaxAge() { return maxage; }
|
||||
|
||||
public int getVersion() { return version; }
|
||||
@ -104,21 +108,18 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
}
|
||||
|
||||
if (curel.equals("application") && curapp != null) {
|
||||
|
||||
// If we already have this application (must be from scanning a
|
||||
// different repo) then just merge in the apks.
|
||||
DB.App app = apps.get(curapp.id);
|
||||
if (app != null) {
|
||||
app.apks.addAll(curapp.apks);
|
||||
} else {
|
||||
appsList.add(curapp);
|
||||
apps.put(curapp.id, curapp);
|
||||
}
|
||||
|
||||
apps.add(curapp);
|
||||
curapp = null;
|
||||
|
||||
// If the app id is already present in this apps list, then it
|
||||
// means the same index file has a duplicate app, which should
|
||||
// not be allowed.
|
||||
// However, I'm thinking that it should be unefined behaviour,
|
||||
// because it is probably a bug in the fdroid server that made it
|
||||
// happen, and I don't *think* it will crash the client, because
|
||||
// the first app will insert, the second one will update the newly
|
||||
// inserted one.
|
||||
} else if (curel.equals("package") && curapk != null && curapp != null) {
|
||||
curapp.apks.add(curapk);
|
||||
apksList.add(curapk);
|
||||
curapk = null;
|
||||
} else if (curapk != null && str != null) {
|
||||
if (curel.equals("version")) {
|
||||
@ -131,19 +132,19 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
}
|
||||
} else if (curel.equals("size")) {
|
||||
try {
|
||||
curapk.detail_size = Integer.parseInt(str);
|
||||
curapk.size = Integer.parseInt(str);
|
||||
} catch (NumberFormatException ex) {
|
||||
curapk.detail_size = 0;
|
||||
curapk.size = 0;
|
||||
}
|
||||
} else if (curel.equals("hash")) {
|
||||
if (hashType == null || hashType.equals("md5")) {
|
||||
if (curapk.detail_hash == null) {
|
||||
curapk.detail_hash = str;
|
||||
curapk.detail_hashType = "MD5";
|
||||
if (curapk.hash == null) {
|
||||
curapk.hash = str;
|
||||
curapk.hashType = "MD5";
|
||||
}
|
||||
} else if (hashType.equals("sha256")) {
|
||||
curapk.detail_hash = str;
|
||||
curapk.detail_hashType = "SHA-256";
|
||||
curapk.hash = str;
|
||||
curapk.hashType = "SHA-256";
|
||||
}
|
||||
} else if (curel.equals("sig")) {
|
||||
curapk.sig = str;
|
||||
@ -159,17 +160,17 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
}
|
||||
} else if (curel.equals("added")) {
|
||||
try {
|
||||
curapk.added = str.length() == 0 ? null : DB.DATE_FORMAT
|
||||
curapk.added = str.length() == 0 ? null : Utils.DATE_FORMAT
|
||||
.parse(str);
|
||||
} catch (ParseException e) {
|
||||
curapk.added = null;
|
||||
}
|
||||
} else if (curel.equals("permissions")) {
|
||||
curapk.detail_permissions = DB.CommaSeparatedList.make(str);
|
||||
curapk.permissions = Utils.CommaSeparatedList.make(str);
|
||||
} else if (curel.equals("features")) {
|
||||
curapk.features = DB.CommaSeparatedList.make(str);
|
||||
curapk.features = Utils.CommaSeparatedList.make(str);
|
||||
} else if (curel.equals("nativecode")) {
|
||||
curapk.nativecode = DB.CommaSeparatedList.make(str);
|
||||
curapk.nativecode = Utils.CommaSeparatedList.make(str);
|
||||
}
|
||||
} else if (curapp != null && str != null) {
|
||||
if (curel.equals("name")) {
|
||||
@ -180,33 +181,33 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
// This is the old-style description. We'll read it
|
||||
// if present, to support old repos, but in newer
|
||||
// repos it will get overwritten straight away!
|
||||
curapp.detail_description = "<p>" + str + "</p>";
|
||||
curapp.description = "<p>" + str + "</p>";
|
||||
} else if (curel.equals("desc")) {
|
||||
// New-style description.
|
||||
curapp.detail_description = str;
|
||||
curapp.description = str;
|
||||
} else if (curel.equals("summary")) {
|
||||
curapp.summary = str;
|
||||
} else if (curel.equals("license")) {
|
||||
curapp.license = str;
|
||||
} else if (curel.equals("source")) {
|
||||
curapp.detail_sourceURL = str;
|
||||
curapp.sourceURL = str;
|
||||
} else if (curel.equals("donate")) {
|
||||
curapp.detail_donateURL = str;
|
||||
curapp.donateURL = str;
|
||||
} else if (curel.equals("bitcoin")) {
|
||||
curapp.detail_bitcoinAddr = str;
|
||||
curapp.bitcoinAddr = str;
|
||||
} else if (curel.equals("litecoin")) {
|
||||
curapp.detail_litecoinAddr = str;
|
||||
curapp.litecoinAddr = str;
|
||||
} else if (curel.equals("dogecoin")) {
|
||||
curapp.detail_dogecoinAddr = str;
|
||||
curapp.dogecoinAddr = str;
|
||||
} else if (curel.equals("flattr")) {
|
||||
curapp.detail_flattrID = str;
|
||||
curapp.flattrID = str;
|
||||
} else if (curel.equals("web")) {
|
||||
curapp.detail_webURL = str;
|
||||
curapp.webURL = str;
|
||||
} else if (curel.equals("tracker")) {
|
||||
curapp.detail_trackerURL = str;
|
||||
curapp.trackerURL = str;
|
||||
} else if (curel.equals("added")) {
|
||||
try {
|
||||
curapp.added = str.length() == 0 ? null : DB.DATE_FORMAT
|
||||
curapp.added = str.length() == 0 ? null : Utils.DATE_FORMAT
|
||||
.parse(str);
|
||||
} catch (ParseException e) {
|
||||
curapp.added = null;
|
||||
@ -214,7 +215,7 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
} else if (curel.equals("lastupdated")) {
|
||||
try {
|
||||
curapp.lastUpdated = str.length() == 0 ? null
|
||||
: DB.DATE_FORMAT.parse(str);
|
||||
: Utils.DATE_FORMAT.parse(str);
|
||||
} catch (ParseException e) {
|
||||
curapp.lastUpdated = null;
|
||||
}
|
||||
@ -227,11 +228,11 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
curapp.curVercode = -1;
|
||||
}
|
||||
} else if (curel.equals("categories")) {
|
||||
curapp.categories = DB.CommaSeparatedList.make(str);
|
||||
curapp.categories = Utils.CommaSeparatedList.make(str);
|
||||
} else if (curel.equals("antifeatures")) {
|
||||
curapp.antiFeatures = DB.CommaSeparatedList.make(str);
|
||||
curapp.antiFeatures = Utils.CommaSeparatedList.make(str);
|
||||
} else if (curel.equals("requirements")) {
|
||||
curapp.requirements = DB.CommaSeparatedList.make(str);
|
||||
curapp.requirements = Utils.CommaSeparatedList.make(str);
|
||||
}
|
||||
} else if (curel.equals("description")) {
|
||||
description = str;
|
||||
@ -270,8 +271,7 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
description = dc;
|
||||
|
||||
} else if (localName.equals("application") && curapp == null) {
|
||||
curapp = new DB.App();
|
||||
curapp.detail_Populated = true;
|
||||
curapp = new App();
|
||||
curapp.id = attributes.getValue("", "id");
|
||||
Bundle progressData = RepoUpdater.createProgressData(repo.address);
|
||||
progressCounter ++;
|
||||
|
@ -18,12 +18,10 @@
|
||||
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import android.app.ListActivity;
|
||||
import android.app.SearchManager;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
@ -37,8 +35,11 @@ import android.support.v4.app.NavUtils;
|
||||
import android.support.v4.view.MenuItemCompat;
|
||||
|
||||
import org.fdroid.fdroid.compat.ActionBarCompat;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.views.AppListAdapter;
|
||||
import org.fdroid.fdroid.views.AvailableAppListAdapter;
|
||||
import org.fdroid.fdroid.views.fragments.AppListFragment;
|
||||
|
||||
public class SearchResults extends ListActivity {
|
||||
|
||||
@ -46,7 +47,7 @@ public class SearchResults extends ListActivity {
|
||||
|
||||
private static final int SEARCH = Menu.FIRST;
|
||||
|
||||
private AppListAdapter applist;
|
||||
private AppListAdapter adapter;
|
||||
|
||||
protected String getQuery() {
|
||||
Intent intent = getIntent();
|
||||
@ -73,7 +74,6 @@ public class SearchResults extends ListActivity {
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true);
|
||||
applist = new AvailableAppListAdapter(this);
|
||||
setContentView(R.layout.searchresults);
|
||||
|
||||
// Start a search by just typing
|
||||
@ -102,53 +102,32 @@ public class SearchResults extends ListActivity {
|
||||
if (query == null || query.length() == 0)
|
||||
finish();
|
||||
|
||||
List<String> matchingids = new ArrayList<String>();
|
||||
try {
|
||||
DB db = DB.getDB();
|
||||
matchingids = db.doSearch(query.trim());
|
||||
} catch (Exception ex) {
|
||||
Log.d("FDroid", "Search failed - " + ex.getMessage());
|
||||
} finally {
|
||||
DB.releaseDB();
|
||||
}
|
||||
|
||||
List<DB.App> apps = new ArrayList<DB.App>();
|
||||
List<DB.App> allApps = ((FDroidApp) getApplication()).getApps();
|
||||
for (DB.App app : allApps) {
|
||||
for (String id : matchingids) {
|
||||
if (id.equals(app.id)) {
|
||||
apps.add(app);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Cursor cursor = getContentResolver().query(
|
||||
AppProvider.getSearchUri(query), AppListFragment.APP_PROJECTION,
|
||||
null, null, AppListFragment.APP_SORT);
|
||||
|
||||
TextView tv = (TextView) findViewById(R.id.description);
|
||||
String headertext;
|
||||
if (apps.size() == 0) {
|
||||
int count = cursor != null ? cursor.getCount() : 0;
|
||||
if (count == 0) {
|
||||
headertext = getString(R.string.searchres_noapps, query);
|
||||
} else if (apps.size() == 1) {
|
||||
} else if (count == 1) {
|
||||
headertext = getString(R.string.searchres_oneapp, query);
|
||||
} else {
|
||||
headertext = getString(R.string.searchres_napps, apps.size(), query);
|
||||
headertext = getString(R.string.searchres_napps, count, query);
|
||||
}
|
||||
tv.setText(headertext);
|
||||
Log.d("FDroid", "Search for '" + query + "' returned " + apps.size()
|
||||
+ " results");
|
||||
applist.clear();
|
||||
for (DB.App app : apps) {
|
||||
applist.addItem(app);
|
||||
}
|
||||
getListView().setFastScrollEnabled(true);
|
||||
applist.notifyDataSetChanged();
|
||||
setListAdapter(applist);
|
||||
Log.d("FDroid", "Search for '" + query + "' returned " + count + " results");
|
||||
|
||||
adapter = new AvailableAppListAdapter(this, cursor);
|
||||
getListView().setFastScrollEnabled(true);
|
||||
setListAdapter(adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onListItemClick(ListView l, View v, int position, long id) {
|
||||
final DB.App app;
|
||||
app = (DB.App) applist.getItem(position);
|
||||
final App app;
|
||||
app = new App((Cursor) adapter.getItem(position));
|
||||
|
||||
Intent intent = new Intent(this, AppDetails.class);
|
||||
intent.putExtra("appid", app.id);
|
||||
|
@ -18,42 +18,26 @@
|
||||
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.*;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.IntentService;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.app.*;
|
||||
import android.content.*;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.database.Cursor;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Parcelable;
|
||||
import android.os.ResultReceiver;
|
||||
import android.os.SystemClock;
|
||||
import android.net.Uri;
|
||||
import android.os.*;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.data.*;
|
||||
import org.fdroid.fdroid.updater.RepoUpdater;
|
||||
|
||||
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 org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.data.RepoProvider;
|
||||
import org.fdroid.fdroid.updater.RepoUpdater;
|
||||
|
||||
public class UpdateService extends IntentService implements ProgressListener {
|
||||
|
||||
public static final String RESULT_MESSAGE = "msg";
|
||||
@ -203,16 +187,6 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
return receiver == null;
|
||||
}
|
||||
|
||||
// Get the number of apps that have updates available.
|
||||
public int getNumUpdates(List<DB.App> apps) {
|
||||
int count = 0;
|
||||
for (DB.App app : apps) {
|
||||
if (app.toUpdate)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
|
||||
@ -256,20 +230,113 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
} else {
|
||||
Log.d("FDroid", "Unscheduled (manually requested) update");
|
||||
}
|
||||
errmsg = updateRepos(address);
|
||||
if (TextUtils.isEmpty(errmsg)) {
|
||||
|
||||
// Grab some preliminary information, then we can release the
|
||||
// database while we do all the downloading, etc...
|
||||
int updates = 0;
|
||||
List<Repo> repos = RepoProvider.Helper.all(getContentResolver());
|
||||
|
||||
// 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>();
|
||||
boolean success = true;
|
||||
boolean changes = false;
|
||||
for (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 (App app : updater.getApps()) {
|
||||
appsToUpdate.put(app.id, app);
|
||||
}
|
||||
apksToUpdate.addAll(updater.getApks());
|
||||
updatedRepos.add(repo);
|
||||
changes = true;
|
||||
} else {
|
||||
unchangedRepos.add(repo);
|
||||
}
|
||||
} catch (RepoUpdater.UpdateException e) {
|
||||
errmsg += (errmsg.length() == 0 ? "" : "\n") + e.getMessage();
|
||||
Log.e("FDroid", "Error updating repository " + repo.address + ": " + e.getMessage());
|
||||
Log.e("FDroid", Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
|
||||
if (!changes && success) {
|
||||
Log.d("FDroid",
|
||||
"Not checking app details or compatibility, " +
|
||||
"because all repos were up to date.");
|
||||
} else if (changes && success) {
|
||||
|
||||
sendStatus(STATUS_INFO,
|
||||
getString(R.string.status_checking_compatibility));
|
||||
|
||||
List<App> listOfAppsToUpdate = new ArrayList<App>();
|
||||
listOfAppsToUpdate.addAll(appsToUpdate.values());
|
||||
|
||||
calcCompatibilityFlags(this, apksToUpdate, appsToUpdate);
|
||||
calcIconUrls(this, apksToUpdate, appsToUpdate, repos);
|
||||
calcCurrentApk(apksToUpdate, appsToUpdate);
|
||||
|
||||
int totalInsertsUpdates = listOfAppsToUpdate.size() + apksToUpdate.size();
|
||||
updateOrInsertApps(listOfAppsToUpdate, totalInsertsUpdates, 0);
|
||||
updateOrInsertApks(apksToUpdate, totalInsertsUpdates, listOfAppsToUpdate.size());
|
||||
removeApksFromRepos(disabledRepos);
|
||||
removeApksNoLongerInRepo(listOfAppsToUpdate, updatedRepos);
|
||||
removeAppsWithoutApks();
|
||||
notifyContentProviders();
|
||||
}
|
||||
|
||||
if (success && changes && prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, false)) {
|
||||
int updateCount = 0;
|
||||
for (App app : appsToUpdate.values()) {
|
||||
if (app.hasUpdates(this)) {
|
||||
updateCount ++;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateCount > 0) {
|
||||
showAppUpdatesNotification(updateCount);
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
if (errmsg.length() == 0)
|
||||
errmsg = "Unknown error";
|
||||
sendStatus(STATUS_ERROR, errmsg);
|
||||
} else {
|
||||
Editor e = prefs.edit();
|
||||
e.putLong(Preferences.PREF_UPD_LAST, System.currentTimeMillis());
|
||||
e.commit();
|
||||
if (changes) {
|
||||
sendStatus(STATUS_COMPLETE_WITH_CHANGES);
|
||||
} else {
|
||||
sendStatus(STATUS_COMPLETE_AND_SAME);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e("FDroid",
|
||||
"Exception during update processing:\n"
|
||||
+ Log.getStackTraceString(e));
|
||||
if (TextUtils.isEmpty(errmsg))
|
||||
if (errmsg.length() == 0)
|
||||
errmsg = "Unknown error";
|
||||
sendStatus(STATUS_ERROR, errmsg);
|
||||
} finally {
|
||||
} finally {
|
||||
Log.d("FDroid", "Update took "
|
||||
+ ((System.currentTimeMillis() - startTime) / 1000)
|
||||
+ " seconds.");
|
||||
@ -277,147 +344,112 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
}
|
||||
}
|
||||
|
||||
protected String updateRepos(String address) throws Exception {
|
||||
SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(getBaseContext());
|
||||
boolean notify = prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, false);
|
||||
String errmsg = "";
|
||||
// Grab some preliminary information, then we can release the
|
||||
// database while we do all the downloading, etc...
|
||||
int updates = 0;
|
||||
List<Repo> repos;
|
||||
List<DB.App> apps;
|
||||
try {
|
||||
DB db = DB.getDB();
|
||||
apps = db.getApps(false);
|
||||
} finally {
|
||||
DB.releaseDB();
|
||||
}
|
||||
private void notifyContentProviders() {
|
||||
getContentResolver().notifyChange(AppProvider.getContentUri(), null);
|
||||
getContentResolver().notifyChange(ApkProvider.getContentUri(), null);
|
||||
}
|
||||
|
||||
repos = RepoProvider.Helper.all(getContentResolver());
|
||||
|
||||
// Process each repo...
|
||||
List<DB.App> updatingApps = new ArrayList<DB.App>();
|
||||
Set<Long> keeprepos = new TreeSet<Long>();
|
||||
boolean changes = false;
|
||||
boolean update;
|
||||
for (Repo repo : repos) {
|
||||
if (!repo.inuse)
|
||||
continue;
|
||||
// are we updating all repos, or just one?
|
||||
if (TextUtils.isEmpty(address)) {
|
||||
update = true;
|
||||
private static void calcCompatibilityFlags(Context context, List<Apk> apks,
|
||||
Map<String, App> apps) {
|
||||
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 {
|
||||
// if only updating one repo, mark the rest as keepers
|
||||
if (address.equals(repo.address)) {
|
||||
update = true;
|
||||
apk.compatible = true;
|
||||
apk.incompatible_reasons = null;
|
||||
apps.get(apk.id).compatible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current version - this will be one of the Apks from 'apks'.
|
||||
* Can return null if there are no available versions.
|
||||
* This should be the 'current' version, as in the most recent stable
|
||||
* one, that most users would want by default. It might not be the
|
||||
* most recent, if for example there are betas etc.
|
||||
*/
|
||||
private static void calcCurrentApk(List<Apk> apks, Map<String,App> apps ) {
|
||||
for ( App app : apps.values() ) {
|
||||
List<Apk> apksForApp = new ArrayList<Apk>();
|
||||
for (Apk apk : apks) {
|
||||
if (apk.id.equals(app.id)) {
|
||||
apksForApp.add(apk);
|
||||
}
|
||||
}
|
||||
calcCurrentApkForApp(app, apksForApp);
|
||||
}
|
||||
}
|
||||
|
||||
private static void calcCurrentApkForApp(App app, List<Apk> apksForApp) {
|
||||
Apk latestApk = null;
|
||||
// Try and return the real current version first. It will find the
|
||||
// closest version smaller than the curVercode, being the same
|
||||
// vercode if it exists.
|
||||
if (app.curVercode > 0) {
|
||||
int latestcode = -1;
|
||||
for (Apk apk : apksForApp) {
|
||||
if ((!app.compatible || apk.compatible)
|
||||
&& apk.vercode <= app.curVercode
|
||||
&& apk.vercode > latestcode) {
|
||||
latestApk = apk;
|
||||
latestcode = apk.vercode;
|
||||
}
|
||||
}
|
||||
} else if (app.curVercode == -1) {
|
||||
// If the current version was not set we return the most recent apk.
|
||||
int latestCode = -1;
|
||||
for (Apk apk : apksForApp) {
|
||||
if ((!app.compatible || apk.compatible)
|
||||
&& apk.vercode > latestCode) {
|
||||
latestApk = apk;
|
||||
latestCode = apk.vercode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (latestApk != null) {
|
||||
app.curVercode = latestApk.vercode;
|
||||
app.curVersion = latestApk.version;
|
||||
}
|
||||
}
|
||||
|
||||
private static void calcIconUrls(Context context, List<Apk> apks,
|
||||
Map<String, App> apps, List<Repo> repos) {
|
||||
String iconsDir = Utils.getIconsDir(context);
|
||||
Log.d("FDroid", "Density-specific icons dir is " + iconsDir);
|
||||
for (App app : apps.values()) {
|
||||
if (app.iconUrl == null && app.icon != null) {
|
||||
calcIconUrl(iconsDir, app, apks, repos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void calcIconUrl(String iconsDir, App app,
|
||||
List<Apk> allApks, List<Repo> repos) {
|
||||
List<Apk> apksForApp = new ArrayList<Apk>();
|
||||
for (Apk apk : allApks) {
|
||||
if (apk.id.equals(app.id)) {
|
||||
apksForApp.add(apk);
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(apksForApp);
|
||||
for (int i = apksForApp.size() - 1; i >= 0; i --) {
|
||||
Apk apk = apksForApp.get(i);
|
||||
for (Repo repo : repos) {
|
||||
if (repo.getId() != apk.repo) continue;
|
||||
if (repo.version >= Repo.VERSION_DENSITY_SPECIFIC_ICONS) {
|
||||
app.iconUrl = repo.address + iconsDir + app.icon;
|
||||
} else {
|
||||
keeprepos.add(repo.getId());
|
||||
update = false;
|
||||
app.iconUrl = repo.address + "/icons/" + app.icon;
|
||||
}
|
||||
}
|
||||
if (!update)
|
||||
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()) {
|
||||
updatingApps.addAll(updater.getApps());
|
||||
changes = true;
|
||||
} else {
|
||||
keeprepos.add(repo.getId());
|
||||
}
|
||||
} catch (RepoUpdater.UpdateException e) {
|
||||
errmsg += (errmsg.length() == 0 ? "" : "\n") + e.getMessage();
|
||||
Log.e("FDroid", "Error updating repository " + repo.address + ": " + e.getMessage());
|
||||
Log.e("FDroid", Log.getStackTraceString(e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
boolean success = true;
|
||||
if (!changes) {
|
||||
Log.d("FDroid", "Not checking app details or compatibility, " +
|
||||
"because all repos were up to date.");
|
||||
} else {
|
||||
sendStatus(STATUS_INFO, getString(R.string.status_checking_compatibility));
|
||||
|
||||
DB db = DB.getDB();
|
||||
try {
|
||||
|
||||
// Need to flag things we're keeping despite having received
|
||||
// no data about during the update. (i.e. stuff from a repo
|
||||
// that we know is unchanged due to the etag)
|
||||
for (long keep : keeprepos) {
|
||||
for (DB.App app : apps) {
|
||||
boolean keepapp = false;
|
||||
for (Apk apk : app.apks) {
|
||||
if (apk.repo == keep) {
|
||||
keepapp = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (keepapp) {
|
||||
DB.App app_k = null;
|
||||
for (DB.App app2 : apps) {
|
||||
if (app2.id.equals(app.id)) {
|
||||
app_k = app2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (app_k == null) {
|
||||
updatingApps.add(app);
|
||||
app_k = app;
|
||||
}
|
||||
app_k.updated = true;
|
||||
db.populateDetails(app_k, keep);
|
||||
for (Apk apk : app.apks)
|
||||
if (apk.repo == keep)
|
||||
apk.updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.beginUpdate(apps);
|
||||
for (DB.App app : updatingApps) {
|
||||
db.updateApplication(app);
|
||||
}
|
||||
db.endUpdate();
|
||||
} catch (Exception ex) {
|
||||
db.cancelUpdate();
|
||||
Log.e("FDroid", "Exception during update processing:\n"
|
||||
+ Log.getStackTraceString(ex));
|
||||
errmsg = "Exception during processing - " + ex.getMessage();
|
||||
success = false;
|
||||
} finally {
|
||||
DB.releaseDB();
|
||||
}
|
||||
}
|
||||
|
||||
if (success && changes) {
|
||||
((FDroidApp) getApplication()).invalidateAllApps();
|
||||
if (notify) {
|
||||
apps = ((FDroidApp) getApplication()).getApps();
|
||||
updates = getNumUpdates(apps);
|
||||
}
|
||||
if (notify && updates > 0)
|
||||
showAppUpdatesNotification(updates);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
if (changes) {
|
||||
sendStatus(STATUS_COMPLETE_WITH_CHANGES);
|
||||
} else {
|
||||
sendStatus(STATUS_COMPLETE_AND_SAME);
|
||||
}
|
||||
} else {
|
||||
if (TextUtils.isEmpty(errmsg))
|
||||
errmsg = "Unknown error";
|
||||
sendStatus(STATUS_ERROR, errmsg);
|
||||
}
|
||||
|
||||
return errmsg;
|
||||
}
|
||||
|
||||
private void showAppUpdatesNotification(int updates) throws Exception {
|
||||
@ -447,6 +479,209 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
nm.notify(1, builder.build());
|
||||
}
|
||||
|
||||
private List<String> getKnownAppIds(List<App> apps) {
|
||||
List<String> knownAppIds = new ArrayList<String>();
|
||||
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) {
|
||||
|
||||
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 (knownIdCount > 0) {
|
||||
cursor.moveToFirst();
|
||||
while (!cursor.isAfterLast()) {
|
||||
knownIds.add(cursor.getString(0));
|
||||
cursor.moveToNext();
|
||||
}
|
||||
}
|
||||
|
||||
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 org.fdroid.fdroid.UpdateService#getKnownApks(java.util.List)}
|
||||
* instead, which will only call this with the right number of apks at
|
||||
* a time.
|
||||
* @see org.fdroid.fdroid.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(getContentResolver(), apks, fields);
|
||||
}
|
||||
|
||||
private void updateOrInsertApps(List<App> appsToUpdate, int totalUpdateCount, int currentCount) {
|
||||
|
||||
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
|
||||
List<String> knownAppIds = getKnownAppIds(appsToUpdate);
|
||||
for (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("FDroid", "Updating/inserting " + operations.size() + " apps.");
|
||||
try {
|
||||
executeBatchWithStatus(AppProvider.getAuthority(), operations, currentCount, totalUpdateCount);
|
||||
} catch (RemoteException e) {
|
||||
Log.e("FDroid", e.getMessage());
|
||||
} catch (OperationApplicationException e) {
|
||||
Log.e("FDroid", 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 "fromApks" 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 (Apk apk : apksToUpdate) {
|
||||
boolean known = false;
|
||||
for (Apk knownApk : knownApks) {
|
||||
if (knownApk.id.equals(apk.id) && knownApk.version.equals(knownApk.version)) {
|
||||
known = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (known) {
|
||||
operations.add(updateExistingApk(apk));
|
||||
} else {
|
||||
operations.add(insertNewApk(apk));
|
||||
}
|
||||
}
|
||||
|
||||
Log.d("FDroid", "Updating/inserting " + operations.size() + " apks.");
|
||||
try {
|
||||
executeBatchWithStatus(ApkProvider.getAuthority(), operations, currentCount, totalApksAppsCount);
|
||||
} catch (RemoteException e) {
|
||||
Log.e("FDroid", e.getMessage());
|
||||
} catch (OperationApplicationException e) {
|
||||
Log.e("FDroid", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private ContentProviderOperation updateExistingApk(Apk apk) {
|
||||
Uri uri = ApkProvider.getContentUri(apk);
|
||||
ContentValues values = apk.toContentValues();
|
||||
return ContentProviderOperation.newUpdate(uri).withValues(values).build();
|
||||
}
|
||||
|
||||
private ContentProviderOperation insertNewApk(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();
|
||||
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<App> appsToUpdate,
|
||||
List<Repo> updatedRepos) {
|
||||
for (Repo repo : updatedRepos) {
|
||||
Log.d("FDroid", "Removing apks no longer in repo " + repo.address);
|
||||
// TODO: Implement
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void removeApksFromRepos(List<Repo> repos) {
|
||||
for (Repo repo : repos) {
|
||||
Log.d("FDroid", "Removing apks from repo " + repo.address);
|
||||
Uri uri = ApkProvider.getRepoUri(repo.getId());
|
||||
getContentResolver().delete(uri, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeAppsWithoutApks() {
|
||||
Log.d("FDroid", "Removing aps that don't have any apks");
|
||||
getContentResolver().delete(AppProvider.getNoApksUri(), null, null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Received progress event from the RepoXMLHandler. It could be progress
|
||||
* downloading from the repo, or perhaps processing the info from the repo.
|
||||
|
@ -20,22 +20,60 @@ package org.fdroid.fdroid;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import com.nostra13.universalimageloader.utils.StorageUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.*;
|
||||
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
|
||||
public final class Utils {
|
||||
|
||||
public static final int BUFFER_SIZE = 4096;
|
||||
|
||||
// The date format used for storing dates (e.g. lastupdated, added) in the
|
||||
// database.
|
||||
public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
|
||||
|
||||
private static final String[] FRIENDLY_SIZE_FORMAT = {
|
||||
"%.0f B", "%.0f KiB", "%.1f MiB", "%.2f GiB" };
|
||||
|
||||
public static final SimpleDateFormat LOG_DATE_FORMAT =
|
||||
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
|
||||
|
||||
public static String getIconsDir(Context context) {
|
||||
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||
String iconsDir;
|
||||
if (metrics.densityDpi >= 640) {
|
||||
iconsDir = "/icons-640/";
|
||||
} else if (metrics.densityDpi >= 480) {
|
||||
iconsDir = "/icons-480/";
|
||||
} else if (metrics.densityDpi >= 320) {
|
||||
iconsDir = "/icons-320/";
|
||||
} else if (metrics.densityDpi >= 240) {
|
||||
iconsDir = "/icons-240/";
|
||||
} else if (metrics.densityDpi >= 160) {
|
||||
iconsDir = "/icons-160/";
|
||||
} else {
|
||||
iconsDir = "/icons-120/";
|
||||
}
|
||||
return iconsDir;
|
||||
}
|
||||
|
||||
public static void copy(InputStream input, OutputStream output)
|
||||
throws IOException {
|
||||
copy(input, output, null, null);
|
||||
@ -158,4 +196,145 @@ public final class Utils {
|
||||
return apkCacheDir;
|
||||
}
|
||||
|
||||
public static Map<String, PackageInfo> getInstalledApps(Context context) {
|
||||
return installedApkCache.getApks(context);
|
||||
}
|
||||
|
||||
public static void clearInstalledApksCache() {
|
||||
installedApkCache.emptyCache();
|
||||
}
|
||||
|
||||
public static String calcFingerprint(String keyHexString) {
|
||||
if (TextUtils.isEmpty(keyHexString))
|
||||
return null;
|
||||
else
|
||||
return calcFingerprint(Hasher.unhex(keyHexString));
|
||||
}
|
||||
|
||||
public static String calcFingerprint(Certificate cert) {
|
||||
try {
|
||||
return calcFingerprint(cert.getEncoded());
|
||||
} catch (CertificateEncodingException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String calcFingerprint(byte[] key) {
|
||||
String ret = null;
|
||||
try {
|
||||
// keytool -list -v gives you the SHA-256 fingerprint
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
digest.update(key);
|
||||
byte[] fingerprint = digest.digest();
|
||||
Formatter formatter = new Formatter(new StringBuilder());
|
||||
for (int i = 1; i < fingerprint.length; i++) {
|
||||
formatter.format("%02X", fingerprint[i]);
|
||||
}
|
||||
ret = formatter.toString();
|
||||
formatter.close();
|
||||
} catch (Exception e) {
|
||||
Log.w("FDroid", "Unable to get certificate fingerprint.\n"
|
||||
+ Log.getStackTraceString(e));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static class CommaSeparatedList implements Iterable<String> {
|
||||
private String value;
|
||||
|
||||
private CommaSeparatedList(String list) {
|
||||
value = list;
|
||||
}
|
||||
|
||||
public static CommaSeparatedList make(List<String> list) {
|
||||
if (list == null || list.size() == 0)
|
||||
return null;
|
||||
else {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(int i = 0; i < list.size(); i ++) {
|
||||
if (i > 0) {
|
||||
sb.append(',');
|
||||
}
|
||||
sb.append(list.get(i));
|
||||
}
|
||||
return new CommaSeparatedList(sb.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static CommaSeparatedList make(String list) {
|
||||
if (list == null || list.length() == 0)
|
||||
return null;
|
||||
else
|
||||
return new CommaSeparatedList(list);
|
||||
}
|
||||
|
||||
public static String str(CommaSeparatedList instance) {
|
||||
return (instance == null ? null : instance.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public String toPrettyString() {
|
||||
return value.replaceAll(",", ", ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<String> iterator() {
|
||||
TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(',');
|
||||
splitter.setString(value);
|
||||
return splitter.iterator();
|
||||
}
|
||||
|
||||
public boolean contains(String v) {
|
||||
for (String s : this) {
|
||||
if (s.equals(v))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static InstalledApkCache installedApkCache = null;
|
||||
|
||||
/**
|
||||
* We do a lot of querying of the installed app's. As a result, we like
|
||||
* to cache this information quite heavily (and flush the cache when new
|
||||
* apps are installed). The caching implementation needs to be setup like
|
||||
* this so that it is possible to mock for testing purposes.
|
||||
*/
|
||||
public static void setupInstalledApkCache(InstalledApkCache cache) {
|
||||
installedApkCache = cache;
|
||||
}
|
||||
|
||||
public static class InstalledApkCache {
|
||||
|
||||
protected Map<String, PackageInfo> installedApks = null;
|
||||
|
||||
protected Map<String, PackageInfo> buildAppList(Context context) {
|
||||
Map<String, PackageInfo> info = new HashMap<String, PackageInfo>();
|
||||
Log.d("FDroid", "Reading installed packages");
|
||||
List<PackageInfo> installedPackages = context.getPackageManager().getInstalledPackages(0);
|
||||
for (PackageInfo appInfo : installedPackages) {
|
||||
info.put(appInfo.packageName, appInfo);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
public Map<String, PackageInfo> getApks(Context context) {
|
||||
if (installedApks == null) {
|
||||
installedApks = buildAppList(context);
|
||||
}
|
||||
return installedApks;
|
||||
}
|
||||
|
||||
public void emptyCache() {
|
||||
installedApks = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,37 +1,27 @@
|
||||
package org.fdroid.fdroid.data;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.FeatureInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.DB;
|
||||
import org.fdroid.fdroid.compat.Compatibility;
|
||||
import org.fdroid.fdroid.compat.SupportedArchitectures;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
import java.util.*;
|
||||
|
||||
public class Apk {
|
||||
public class Apk extends ValueObject implements Comparable<Apk> {
|
||||
|
||||
public String id;
|
||||
public String version;
|
||||
public int vercode;
|
||||
public int detail_size; // Size in bytes - 0 means we don't know!
|
||||
public int size; // Size in bytes - 0 means we don't know!
|
||||
public long repo; // ID of the repo it comes from
|
||||
public String detail_hash;
|
||||
public String detail_hashType;
|
||||
public String hash;
|
||||
public String hashType;
|
||||
public int minSdkVersion; // 0 if unknown
|
||||
public Date added;
|
||||
public DB.CommaSeparatedList detail_permissions; // null if empty or
|
||||
public Utils.CommaSeparatedList permissions; // null if empty or
|
||||
// unknown
|
||||
public DB.CommaSeparatedList features; // null if empty or unknown
|
||||
public Utils.CommaSeparatedList features; // null if empty or unknown
|
||||
|
||||
public DB.CommaSeparatedList nativecode; // null if empty or unknown
|
||||
public Utils.CommaSeparatedList nativecode; // null if empty or unknown
|
||||
|
||||
// ID (md5 sum of public key) of signature. Might be null, in the
|
||||
// transition to this field existing.
|
||||
@ -52,30 +42,33 @@ public class Apk {
|
||||
|
||||
public int repoVersion;
|
||||
public String repoAddress;
|
||||
public DB.CommaSeparatedList incompatible_reasons;
|
||||
public Utils.CommaSeparatedList incompatible_reasons;
|
||||
|
||||
public Apk() {
|
||||
updated = false;
|
||||
detail_size = 0;
|
||||
size = 0;
|
||||
added = null;
|
||||
repo = 0;
|
||||
detail_hash = null;
|
||||
detail_hashType = null;
|
||||
detail_permissions = null;
|
||||
hash = null;
|
||||
hashType = null;
|
||||
permissions = null;
|
||||
compatible = false;
|
||||
}
|
||||
|
||||
public Apk(Cursor cursor) {
|
||||
|
||||
checkCursorPosition(cursor);
|
||||
|
||||
for(int i = 0; i < cursor.getColumnCount(); i ++ ) {
|
||||
String column = cursor.getColumnName(i);
|
||||
if (column.equals(ApkProvider.DataColumns.HASH)) {
|
||||
detail_hash = cursor.getString(i);
|
||||
hash = cursor.getString(i);
|
||||
} else if (column.equals(ApkProvider.DataColumns.HASH_TYPE)) {
|
||||
detail_hashType = cursor.getString(i);
|
||||
hashType = cursor.getString(i);
|
||||
} else if (column.equals(ApkProvider.DataColumns.ADDED_DATE)) {
|
||||
added = ValueObject.toDate(cursor.getString(i));
|
||||
} else if (column.equals(ApkProvider.DataColumns.FEATURES)) {
|
||||
features = DB.CommaSeparatedList.make(cursor.getString(i));
|
||||
features = Utils.CommaSeparatedList.make(cursor.getString(i));
|
||||
} else if (column.equals(ApkProvider.DataColumns.APK_ID)) {
|
||||
id = cursor.getString(i);
|
||||
} else if (column.equals(ApkProvider.DataColumns.IS_COMPATIBLE)) {
|
||||
@ -85,15 +78,17 @@ public class Apk {
|
||||
} else if (column.equals(ApkProvider.DataColumns.NAME)) {
|
||||
apkName = cursor.getString(i);
|
||||
} else if (column.equals(ApkProvider.DataColumns.PERMISSIONS)) {
|
||||
detail_permissions = DB.CommaSeparatedList.make(cursor.getString(i));
|
||||
permissions = Utils.CommaSeparatedList.make(cursor.getString(i));
|
||||
} else if (column.equals(ApkProvider.DataColumns.NATIVE_CODE)) {
|
||||
nativecode = DB.CommaSeparatedList.make(cursor.getString(i));
|
||||
nativecode = Utils.CommaSeparatedList.make(cursor.getString(i));
|
||||
} else if (column.equals(ApkProvider.DataColumns.INCOMPATIBLE_REASONS)) {
|
||||
incompatible_reasons = Utils.CommaSeparatedList.make(cursor.getString(i));
|
||||
} else if (column.equals(ApkProvider.DataColumns.REPO_ID)) {
|
||||
repo = cursor.getInt(i);
|
||||
} else if (column.equals(ApkProvider.DataColumns.SIGNATURE)) {
|
||||
sig = cursor.getString(i);
|
||||
} else if (column.equals(ApkProvider.DataColumns.SIZE)) {
|
||||
detail_size = cursor.getInt(i);
|
||||
size = cursor.getInt(i);
|
||||
} else if (column.equals(ApkProvider.DataColumns.SOURCE_NAME)) {
|
||||
srcname = cursor.getString(i);
|
||||
} else if (column.equals(ApkProvider.DataColumns.VERSION)) {
|
||||
@ -108,109 +103,36 @@ public class Apk {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return id + " (version " + vercode + ")";
|
||||
}
|
||||
|
||||
public ContentValues toContentValues() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ApkProvider.DataColumns.APK_ID, id);
|
||||
values.put(ApkProvider.DataColumns.VERSION, version);
|
||||
values.put(ApkProvider.DataColumns.VERSION_CODE, vercode);
|
||||
values.put(ApkProvider.DataColumns.REPO_ID, repo);
|
||||
values.put(ApkProvider.DataColumns.HASH, detail_hash);
|
||||
values.put(ApkProvider.DataColumns.HASH_TYPE, detail_hashType);
|
||||
values.put(ApkProvider.DataColumns.HASH, hash);
|
||||
values.put(ApkProvider.DataColumns.HASH_TYPE, hashType);
|
||||
values.put(ApkProvider.DataColumns.SIGNATURE, sig);
|
||||
values.put(ApkProvider.DataColumns.SOURCE_NAME, srcname);
|
||||
values.put(ApkProvider.DataColumns.SIZE, detail_size);
|
||||
values.put(ApkProvider.DataColumns.SIZE, size);
|
||||
values.put(ApkProvider.DataColumns.NAME, apkName);
|
||||
values.put(ApkProvider.DataColumns.MIN_SDK_VERSION, minSdkVersion);
|
||||
values.put(ApkProvider.DataColumns.ADDED_DATE,
|
||||
added == null ? "" : DB.DATE_FORMAT.format(added));
|
||||
values.put(ApkProvider.DataColumns.PERMISSIONS,
|
||||
DB.CommaSeparatedList.str(detail_permissions));
|
||||
values.put(ApkProvider.DataColumns.FEATURES, DB.CommaSeparatedList.str(features));
|
||||
values.put(ApkProvider.DataColumns.NATIVE_CODE, DB.CommaSeparatedList.str(nativecode));
|
||||
values.put(ApkProvider.DataColumns.ADDED_DATE, added == null ? "" : Utils.DATE_FORMAT.format(added));
|
||||
values.put(ApkProvider.DataColumns.PERMISSIONS, Utils.CommaSeparatedList.str(permissions));
|
||||
values.put(ApkProvider.DataColumns.FEATURES, Utils.CommaSeparatedList.str(features));
|
||||
values.put(ApkProvider.DataColumns.NATIVE_CODE, Utils.CommaSeparatedList.str(nativecode));
|
||||
values.put(ApkProvider.DataColumns.INCOMPATIBLE_REASONS, Utils.CommaSeparatedList.str(incompatible_reasons));
|
||||
values.put(ApkProvider.DataColumns.IS_COMPATIBLE, compatible ? 1 : 0);
|
||||
return values;
|
||||
}
|
||||
|
||||
// Call isCompatible(apk) on an instance of this class to
|
||||
// check if an APK is compatible with the user's device.
|
||||
public static class CompatibilityChecker extends Compatibility {
|
||||
|
||||
private Set<String> features;
|
||||
private Set<String> cpuAbis;
|
||||
private String cpuAbisDesc;
|
||||
private boolean ignoreTouchscreen;
|
||||
|
||||
public CompatibilityChecker(Context ctx) {
|
||||
|
||||
SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(ctx);
|
||||
ignoreTouchscreen = prefs
|
||||
.getBoolean("ignoreTouchscreen", false);
|
||||
|
||||
PackageManager pm = ctx.getPackageManager();
|
||||
StringBuilder logMsg = new StringBuilder();
|
||||
logMsg.append("Available device features:");
|
||||
features = new HashSet<String>();
|
||||
if (pm != null) {
|
||||
for (FeatureInfo fi : pm.getSystemAvailableFeatures()) {
|
||||
features.add(fi.name);
|
||||
logMsg.append('\n');
|
||||
logMsg.append(fi.name);
|
||||
}
|
||||
}
|
||||
|
||||
cpuAbis = SupportedArchitectures.getAbis();
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
boolean first = true;
|
||||
for (String abi : cpuAbis) {
|
||||
if (first) first = false;
|
||||
else builder.append(", ");
|
||||
builder.append(abi);
|
||||
}
|
||||
cpuAbisDesc = builder.toString();
|
||||
builder = null;
|
||||
|
||||
Log.d("FDroid", logMsg.toString());
|
||||
}
|
||||
|
||||
private boolean compatibleApi(DB.CommaSeparatedList nativecode) {
|
||||
if (nativecode == null) return true;
|
||||
for (String abi : nativecode) {
|
||||
if (cpuAbis.contains(abi)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isCompatible(Apk apk) {
|
||||
if (!hasApi(apk.minSdkVersion)) {
|
||||
apk.incompatible_reasons = DB.CommaSeparatedList.make(String.valueOf(apk.minSdkVersion));
|
||||
return false;
|
||||
}
|
||||
if (apk.features != null) {
|
||||
for (String feat : apk.features) {
|
||||
if (ignoreTouchscreen
|
||||
&& feat.equals("android.hardware.touchscreen")) {
|
||||
// Don't check it!
|
||||
} else if (!features.contains(feat)) {
|
||||
apk.incompatible_reasons = DB.CommaSeparatedList.make(feat);
|
||||
Log.d("FDroid", apk.id + " vercode " + apk.vercode
|
||||
+ " is incompatible based on lack of "
|
||||
+ feat);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!compatibleApi(apk.nativecode)) {
|
||||
apk.incompatible_reasons = apk.nativecode;
|
||||
Log.d("FDroid", apk.id + " vercode " + apk.vercode
|
||||
+ " only supports " + DB.CommaSeparatedList.str(apk.nativecode)
|
||||
+ " while your architectures are " + cpuAbisDesc);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@Override
|
||||
public int compareTo(Apk apk) {
|
||||
return Integer.valueOf(vercode).compareTo(apk.vercode);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,12 +8,19 @@ import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.DB;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class ApkProvider extends FDroidProvider {
|
||||
|
||||
/**
|
||||
* SQLite has a maximum of 999 parameters in a query. Each apk we add
|
||||
* requires two (id and vercode) so we can only query half of that. Then,
|
||||
* we may want to add additional constraints, so we give our self some
|
||||
* room by saying only 450 apks can be queried at once.
|
||||
*/
|
||||
public static final int MAX_APKS_TO_QUERY = 450;
|
||||
|
||||
public static final class Helper {
|
||||
|
||||
private Helper() {}
|
||||
@ -75,10 +82,10 @@ public class ApkProvider extends FDroidProvider {
|
||||
Uri uri = getContentUri();
|
||||
String[] args = { Long.toString(repo.getId()) };
|
||||
String selection = DataColumns.REPO_ID + " = ?";
|
||||
resolver.delete(uri, selection + " = ?", args);
|
||||
int count = resolver.delete(uri, selection, args);
|
||||
}
|
||||
|
||||
public static void deleteApksByApp(Context context, DB.App app) {
|
||||
public static void deleteApksByApp(Context context, App app) {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
Uri uri = getContentUri();
|
||||
String[] args = { app.id };
|
||||
@ -95,6 +102,7 @@ public class ApkProvider extends FDroidProvider {
|
||||
Uri uri = getContentUri(id, versionCode);
|
||||
Cursor cursor = resolver.query(uri, projection, null, null, null);
|
||||
if (cursor != null && cursor.getCount() > 0) {
|
||||
cursor.moveToFirst();
|
||||
return new Apk(cursor);
|
||||
} else {
|
||||
return null;
|
||||
@ -106,6 +114,29 @@ public class ApkProvider extends FDroidProvider {
|
||||
Uri uri = getContentUri(id, versionCode);
|
||||
resolver.delete(uri, null, null);
|
||||
}
|
||||
|
||||
public static List<Apk> findByApp(ContentResolver resolver, String appId) {
|
||||
return findByApp(resolver, appId, ApkProvider.DataColumns.ALL);
|
||||
}
|
||||
|
||||
public static List<Apk> findByApp(ContentResolver resolver,
|
||||
String appId, String[] projection) {
|
||||
Uri uri = getAppUri(appId);
|
||||
String sort = ApkProvider.DataColumns.VERSION_CODE + " DESC";
|
||||
Cursor cursor = resolver.query(uri, projection, null, null, sort);
|
||||
return cursorToList(cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns apks in the database, which have the same id and version as
|
||||
* one of the apks in the "apks" argument.
|
||||
*/
|
||||
public static List<Apk> knownApks(ContentResolver resolver,
|
||||
List<Apk> apks, String[] fields) {
|
||||
Uri uri = getContentUri(apks);
|
||||
Cursor cursor = resolver.query(uri, fields, null, null, null);
|
||||
return cursorToList(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
public interface DataColumns extends BaseColumns {
|
||||
@ -126,6 +157,7 @@ public class ApkProvider extends FDroidProvider {
|
||||
public static String HASH_TYPE = "hashType";
|
||||
public static String ADDED_DATE = "added";
|
||||
public static String IS_COMPATIBLE = "compatible";
|
||||
public static String INCOMPATIBLE_REASONS = "incompatibleReasons";
|
||||
public static String REPO_VERSION = "repoVersion";
|
||||
public static String REPO_ADDRESS = "repoAddress";
|
||||
|
||||
@ -133,12 +165,19 @@ public class ApkProvider extends FDroidProvider {
|
||||
_ID, APK_ID, VERSION, REPO_ID, HASH, VERSION_CODE, NAME, SIZE,
|
||||
SIGNATURE, SOURCE_NAME, MIN_SDK_VERSION, PERMISSIONS, FEATURES,
|
||||
NATIVE_CODE, HASH_TYPE, ADDED_DATE, IS_COMPATIBLE,
|
||||
|
||||
REPO_VERSION, REPO_ADDRESS
|
||||
REPO_VERSION, REPO_ADDRESS, INCOMPATIBLE_REASONS
|
||||
};
|
||||
}
|
||||
|
||||
private static final int CODE_APP = CODE_SINGLE + 1;
|
||||
private static final int CODE_REPO = CODE_APP + 1;
|
||||
private static final int CODE_APKS = CODE_REPO + 1;
|
||||
|
||||
private static final String PROVIDER_NAME = "ApkProvider";
|
||||
private static final String PATH_APK = "apk";
|
||||
private static final String PATH_APKS = "apks";
|
||||
private static final String PATH_APP = "app";
|
||||
private static final String PATH_REPO = "repo";
|
||||
|
||||
private static final UriMatcher matcher = new UriMatcher(-1);
|
||||
|
||||
@ -148,22 +187,65 @@ public class ApkProvider extends FDroidProvider {
|
||||
REPO_FIELDS.put(DataColumns.REPO_VERSION, RepoProvider.DataColumns.VERSION);
|
||||
REPO_FIELDS.put(DataColumns.REPO_ADDRESS, RepoProvider.DataColumns.ADDRESS);
|
||||
|
||||
matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, null, CODE_LIST);
|
||||
matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, "/*/#", CODE_SINGLE);
|
||||
matcher.addURI(getAuthority(), PATH_REPO + "/#", CODE_REPO);
|
||||
matcher.addURI(getAuthority(), PATH_APK + "/#/*", CODE_SINGLE);
|
||||
matcher.addURI(getAuthority(), PATH_APKS + "/*", CODE_APKS);
|
||||
matcher.addURI(getAuthority(), PATH_APP + "/*", CODE_APP);
|
||||
matcher.addURI(getAuthority(), null, CODE_LIST);
|
||||
}
|
||||
|
||||
public static String getAuthority() {
|
||||
return AUTHORITY + "." + PROVIDER_NAME;
|
||||
}
|
||||
|
||||
public static Uri getContentUri() {
|
||||
return Uri.parse("content://" + AUTHORITY + "." + PROVIDER_NAME);
|
||||
return Uri.parse("content://" + getAuthority());
|
||||
}
|
||||
|
||||
public static Uri getAppUri(String appId) {
|
||||
return getContentUri()
|
||||
.buildUpon()
|
||||
.appendPath(PATH_APP)
|
||||
.appendPath(appId)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Uri getRepoUri(long repoId) {
|
||||
return getContentUri()
|
||||
.buildUpon()
|
||||
.appendPath(PATH_REPO)
|
||||
.appendPath(Long.toString(repoId))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Uri getContentUri(Apk apk) {
|
||||
return getContentUri(apk.id, apk.vercode);
|
||||
}
|
||||
|
||||
public static Uri getContentUri(String id, int versionCode) {
|
||||
return getContentUri()
|
||||
.buildUpon()
|
||||
.appendPath(PATH_APK)
|
||||
.appendPath(Integer.toString(versionCode))
|
||||
.appendPath(id)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Uri getContentUri(List<Apk> apks) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < apks.size(); i ++) {
|
||||
if (i != 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
Apk a = apks.get(i);
|
||||
builder.append(a.id).append(':').append(a.vercode);
|
||||
}
|
||||
return getContentUri().buildUpon()
|
||||
.appendPath(PATH_APKS)
|
||||
.appendPath(builder.toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTableName() {
|
||||
return DBHelper.TABLE_APK;
|
||||
@ -257,29 +339,70 @@ public class ApkProvider extends FDroidProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private String appendPrimaryKeyToSelection(String selection) {
|
||||
return (selection == null ? "" : selection + " AND ") + " id = ? and vercode = ?";
|
||||
private QuerySelection queryApp(String appId) {
|
||||
String selection = " id = ? ";
|
||||
String[] args = new String[] { appId };
|
||||
return new QuerySelection(selection, args);
|
||||
}
|
||||
|
||||
private String[] appendPrimaryKeyToArgs(Uri uri, String[] selectionArgs) {
|
||||
List<String> args = new ArrayList<String>(selectionArgs.length + 2);
|
||||
for (String arg : args) {
|
||||
args.add(arg);
|
||||
private QuerySelection querySingle(Uri uri) {
|
||||
String selection = " vercode = ? and id = ? ";
|
||||
String[] args = new String[] {
|
||||
// First (0th) path segment is the word "apk",
|
||||
// and we are not interested in it.
|
||||
uri.getPathSegments().get(1),
|
||||
uri.getPathSegments().get(2)
|
||||
};
|
||||
return new QuerySelection(selection, args);
|
||||
}
|
||||
|
||||
private QuerySelection queryRepo(long repoId) {
|
||||
String selection = " repo = ? ";
|
||||
String[] args = new String[]{ Long.toString(repoId) };
|
||||
return new QuerySelection(selection, args);
|
||||
}
|
||||
|
||||
private QuerySelection queryApks(String apkKeys) {
|
||||
String[] apkDetails = apkKeys.split(",");
|
||||
String[] args = new String[apkDetails.length * 2];
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < apkDetails.length; i ++) {
|
||||
String[] parts = apkDetails[i].split(":");
|
||||
String id = parts[0];
|
||||
String verCode = parts[1];
|
||||
args[i * 2] = id;
|
||||
args[i * 2 + 1] = verCode;
|
||||
if (i != 0) {
|
||||
sb.append(" OR ");
|
||||
}
|
||||
sb.append(" ( id = ? AND vercode = ? ) ");
|
||||
}
|
||||
args.addAll(uri.getPathSegments());
|
||||
return (String[])args.toArray();
|
||||
return new QuerySelection(sb.toString(), args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
|
||||
QuerySelection query = new QuerySelection(selection, selectionArgs);
|
||||
|
||||
switch (matcher.match(uri)) {
|
||||
case CODE_LIST:
|
||||
break;
|
||||
|
||||
case CODE_SINGLE:
|
||||
selection = appendPrimaryKeyToSelection(selection);
|
||||
selectionArgs = appendPrimaryKeyToArgs(uri, selectionArgs);
|
||||
query = query.add(querySingle(uri));
|
||||
break;
|
||||
|
||||
case CODE_APP:
|
||||
query = query.add(queryApp(uri.getLastPathSegment()));
|
||||
break;
|
||||
|
||||
case CODE_APKS:
|
||||
query = query.add(queryApks(uri.getLastPathSegment()));
|
||||
break;
|
||||
|
||||
case CODE_REPO:
|
||||
query = query.add(queryRepo(Long.parseLong(uri.getLastPathSegment())));
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -287,14 +410,14 @@ public class ApkProvider extends FDroidProvider {
|
||||
throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri);
|
||||
}
|
||||
|
||||
QueryBuilder query = new QueryBuilder();
|
||||
QueryBuilder queryBuilder = new QueryBuilder();
|
||||
for (String field : projection) {
|
||||
query.addField(field);
|
||||
queryBuilder.addField(field);
|
||||
}
|
||||
query.addSelection(selection);
|
||||
query.addOrderBy(sortOrder);
|
||||
queryBuilder.addSelection(query.getSelection());
|
||||
queryBuilder.addOrderBy(sortOrder);
|
||||
|
||||
Cursor cursor = read().rawQuery(query.toString(), selectionArgs);
|
||||
Cursor cursor = read().rawQuery(queryBuilder.toString(), query.getArgs());
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
return cursor;
|
||||
}
|
||||
@ -313,10 +436,11 @@ public class ApkProvider extends FDroidProvider {
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
|
||||
removeRepoFields(values);
|
||||
long id = write().insertOrThrow(getTableName(), null, values);
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
if (!isApplyingBatch()) {
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
}
|
||||
return getContentUri(
|
||||
values.getAsString(DataColumns.APK_ID),
|
||||
values.getAsInteger(DataColumns.VERSION_CODE));
|
||||
@ -326,14 +450,19 @@ public class ApkProvider extends FDroidProvider {
|
||||
@Override
|
||||
public int delete(Uri uri, String where, String[] whereArgs) {
|
||||
|
||||
QuerySelection query = new QuerySelection(where, whereArgs);
|
||||
|
||||
switch (matcher.match(uri)) {
|
||||
case CODE_LIST:
|
||||
// Don't support deleting of multiple apks yet.
|
||||
return 0;
|
||||
|
||||
case CODE_REPO:
|
||||
query = query.add(queryRepo(Long.parseLong(uri.getLastPathSegment())));
|
||||
break;
|
||||
|
||||
case CODE_SINGLE:
|
||||
where = appendPrimaryKeyToSelection(where);
|
||||
whereArgs = appendPrimaryKeyToArgs(uri, whereArgs);
|
||||
query = query.add(querySingle(uri));
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -341,7 +470,7 @@ public class ApkProvider extends FDroidProvider {
|
||||
throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri);
|
||||
}
|
||||
|
||||
int rowsAffected = write().delete(getTableName(), where, whereArgs);
|
||||
int rowsAffected = write().delete(getTableName(), query.getSelection(), query.getArgs());
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return rowsAffected;
|
||||
|
||||
@ -350,19 +479,22 @@ public class ApkProvider extends FDroidProvider {
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
|
||||
|
||||
QuerySelection query = new QuerySelection(where, whereArgs);
|
||||
|
||||
switch (matcher.match(uri)) {
|
||||
case CODE_LIST:
|
||||
return 0;
|
||||
|
||||
case CODE_SINGLE:
|
||||
where = appendPrimaryKeyToSelection(where);
|
||||
whereArgs = appendPrimaryKeyToArgs(uri, whereArgs);
|
||||
query = query.add(querySingle(uri));
|
||||
break;
|
||||
}
|
||||
|
||||
removeRepoFields(values);
|
||||
int numRows = write().update(getTableName(), values, where, whereArgs);
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
int numRows = write().update(getTableName(), values, query.getSelection(), query.getArgs());
|
||||
if (!isApplyingBatch()) {
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
}
|
||||
return numRows;
|
||||
|
||||
}
|
||||
|
230
src/org/fdroid/fdroid/data/App.java
Normal file
230
src/org/fdroid/fdroid/data/App.java
Normal file
@ -0,0 +1,230 @@
|
||||
package org.fdroid.fdroid.data;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.database.Cursor;
|
||||
import org.fdroid.fdroid.AppFilter;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
public class App extends ValueObject implements Comparable<App> {
|
||||
|
||||
// True if compatible with the device (i.e. if at least one apk is)
|
||||
public boolean compatible;
|
||||
|
||||
public String id = "unknown";
|
||||
public String name = "Unknown";
|
||||
public String summary = "Unknown application";
|
||||
public String icon;
|
||||
|
||||
public String description;
|
||||
|
||||
public String license = "Unknown";
|
||||
|
||||
public String webURL;
|
||||
|
||||
public String trackerURL;
|
||||
|
||||
public String sourceURL;
|
||||
|
||||
public String donateURL;
|
||||
|
||||
public String bitcoinAddr;
|
||||
|
||||
public String litecoinAddr;
|
||||
|
||||
public String dogecoinAddr;
|
||||
|
||||
public String flattrID;
|
||||
|
||||
public String curVersion;
|
||||
public int curVercode;
|
||||
public Date added;
|
||||
public Date lastUpdated;
|
||||
|
||||
// List of categories (as defined in the metadata
|
||||
// documentation) or null if there aren't any.
|
||||
public Utils.CommaSeparatedList categories;
|
||||
|
||||
// List of anti-features (as defined in the metadata
|
||||
// documentation) or null if there aren't any.
|
||||
public Utils.CommaSeparatedList antiFeatures;
|
||||
|
||||
// List of special requirements (such as root privileges) or
|
||||
// null if there aren't any.
|
||||
public Utils.CommaSeparatedList requirements;
|
||||
|
||||
// True if all updates for this app are to be ignored
|
||||
public boolean ignoreAllUpdates;
|
||||
|
||||
// True if the current update for this app is to be ignored
|
||||
public int ignoreThisUpdate;
|
||||
|
||||
// Used internally for tracking during repo updates.
|
||||
public boolean updated;
|
||||
|
||||
public String iconUrl;
|
||||
|
||||
@Override
|
||||
public int compareTo(App app) {
|
||||
return name.compareToIgnoreCase(app.name);
|
||||
}
|
||||
|
||||
public App() {
|
||||
|
||||
}
|
||||
|
||||
public App(Cursor cursor) {
|
||||
|
||||
checkCursorPosition(cursor);
|
||||
|
||||
for(int i = 0; i < cursor.getColumnCount(); i ++ ) {
|
||||
String column = cursor.getColumnName(i);
|
||||
if (column.equals(AppProvider.DataColumns.IS_COMPATIBLE)) {
|
||||
compatible = cursor.getInt(i) == 1;
|
||||
} else if (column.equals(AppProvider.DataColumns.APP_ID)) {
|
||||
id = cursor.getString(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.NAME)) {
|
||||
name = cursor.getString(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.SUMMARY)) {
|
||||
summary = cursor.getString(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.ICON)) {
|
||||
icon = cursor.getString(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.DESCRIPTION)) {
|
||||
description = cursor.getString(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.LICENSE)) {
|
||||
license = cursor.getString(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.WEB_URL)) {
|
||||
webURL = cursor.getString(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.TRACKER_URL)) {
|
||||
trackerURL = cursor.getString(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.SOURCE_URL)) {
|
||||
sourceURL = cursor.getString(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.DONATE_URL)) {
|
||||
donateURL = cursor.getString(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.BITCOIN_ADDR)) {
|
||||
bitcoinAddr = cursor.getString(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.LITECOIN_ADDR)) {
|
||||
litecoinAddr = cursor.getString(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.DOGECOIN_ADDR)) {
|
||||
dogecoinAddr = cursor.getString(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.FLATTR_ID)) {
|
||||
flattrID = cursor.getString(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.CURRENT_VERSION)) {
|
||||
curVersion = cursor.getString(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.CURRENT_VERSION_CODE)) {
|
||||
curVercode = cursor.getInt(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.ADDED)) {
|
||||
added = ValueObject.toDate(cursor.getString(i));
|
||||
} else if (column.equals(AppProvider.DataColumns.LAST_UPDATED)) {
|
||||
lastUpdated = ValueObject.toDate(cursor.getString(i));
|
||||
} else if (column.equals(AppProvider.DataColumns.CATEGORIES)) {
|
||||
categories = Utils.CommaSeparatedList.make(cursor.getString(i));
|
||||
} else if (column.equals(AppProvider.DataColumns.ANTI_FEATURES)) {
|
||||
antiFeatures = Utils.CommaSeparatedList.make(cursor.getString(i));
|
||||
} else if (column.equals(AppProvider.DataColumns.REQUIREMENTS)) {
|
||||
requirements = Utils.CommaSeparatedList.make(cursor.getString(i));
|
||||
} else if (column.equals(AppProvider.DataColumns.IGNORE_ALLUPDATES)) {
|
||||
ignoreAllUpdates = cursor.getInt(i) == 1;
|
||||
} else if (column.equals(AppProvider.DataColumns.IGNORE_THISUPDATE)) {
|
||||
ignoreThisUpdate = cursor.getInt(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.ICON_URL)) {
|
||||
iconUrl = cursor.getString(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ContentValues toContentValues() {
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(AppProvider.DataColumns.APP_ID, id);
|
||||
values.put(AppProvider.DataColumns.NAME, name);
|
||||
values.put(AppProvider.DataColumns.SUMMARY, summary);
|
||||
values.put(AppProvider.DataColumns.ICON, icon);
|
||||
values.put(AppProvider.DataColumns.ICON_URL, iconUrl);
|
||||
values.put(AppProvider.DataColumns.DESCRIPTION, description);
|
||||
values.put(AppProvider.DataColumns.LICENSE, license);
|
||||
values.put(AppProvider.DataColumns.WEB_URL, webURL);
|
||||
values.put(AppProvider.DataColumns.TRACKER_URL, trackerURL);
|
||||
values.put(AppProvider.DataColumns.SOURCE_URL, sourceURL);
|
||||
values.put(AppProvider.DataColumns.DONATE_URL, donateURL);
|
||||
values.put(AppProvider.DataColumns.BITCOIN_ADDR, bitcoinAddr);
|
||||
values.put(AppProvider.DataColumns.LITECOIN_ADDR, litecoinAddr);
|
||||
values.put(AppProvider.DataColumns.DOGECOIN_ADDR, dogecoinAddr);
|
||||
values.put(AppProvider.DataColumns.FLATTR_ID, flattrID);
|
||||
values.put(AppProvider.DataColumns.ADDED, added == null ? "" : Utils.DATE_FORMAT.format(added));
|
||||
values.put(AppProvider.DataColumns.LAST_UPDATED, added == null ? "" : Utils.DATE_FORMAT.format(lastUpdated));
|
||||
values.put(AppProvider.DataColumns.CURRENT_VERSION, curVersion);
|
||||
values.put(AppProvider.DataColumns.CURRENT_VERSION_CODE, curVercode);
|
||||
values.put(AppProvider.DataColumns.CATEGORIES, Utils.CommaSeparatedList.str(categories));
|
||||
values.put(AppProvider.DataColumns.ANTI_FEATURES, Utils.CommaSeparatedList.str(antiFeatures));
|
||||
values.put(AppProvider.DataColumns.REQUIREMENTS, Utils.CommaSeparatedList.str(requirements));
|
||||
values.put(AppProvider.DataColumns.IS_COMPATIBLE, compatible ? 1 : 0);
|
||||
values.put(AppProvider.DataColumns.IGNORE_ALLUPDATES, ignoreAllUpdates ? 1 : 0);
|
||||
values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, ignoreThisUpdate);
|
||||
values.put(AppProvider.DataColumns.ICON_URL, iconUrl);
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version string for the currently installed version of this apk.
|
||||
* If not installed, returns null.
|
||||
*/
|
||||
public String getInstalledVersion(Context context) {
|
||||
PackageInfo info = getInstalledInfo(context);
|
||||
return info == null ? null : info.versionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version code for the currently installed version of this apk.
|
||||
* If not installed, it returns -1.
|
||||
*/
|
||||
public int getInstalledVerCode(Context context) {
|
||||
PackageInfo info = getInstalledInfo(context);
|
||||
return info == null ? -1 : info.versionCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if installed by the user, false if a system apk or not installed.
|
||||
*/
|
||||
public boolean getUserInstalled(Context context) {
|
||||
PackageInfo info = getInstalledInfo(context);
|
||||
return info != null && ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 1);
|
||||
}
|
||||
|
||||
public PackageInfo getInstalledInfo(Context context) {
|
||||
Map<String, PackageInfo> installed = Utils.getInstalledApps(context);
|
||||
return installed.containsKey(id) ? installed.get(id) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if there are new versions (apks) available
|
||||
*/
|
||||
public boolean hasUpdates(Context context) {
|
||||
boolean updates = false;
|
||||
if (curVercode > 0) {
|
||||
int installedVerCode = getInstalledVerCode(context);
|
||||
updates = (installedVerCode > 0 && installedVerCode < curVercode);
|
||||
}
|
||||
return updates;
|
||||
}
|
||||
|
||||
// True if there are new versions (apks) available and the user wants
|
||||
// to be notified about them
|
||||
public boolean canAndWantToUpdate(Context context) {
|
||||
boolean canUpdate = hasUpdates(context);
|
||||
boolean wantsUpdate = !ignoreAllUpdates && ignoreThisUpdate < curVercode;
|
||||
return canUpdate && wantsUpdate && !isFiltered();
|
||||
}
|
||||
|
||||
// Whether the app is filtered or not based on AntiFeatures and root
|
||||
// permission (set in the Settings page)
|
||||
public boolean isFiltered() {
|
||||
return new AppFilter().filter(this);
|
||||
}
|
||||
}
|
481
src/org/fdroid/fdroid/data/AppProvider.java
Normal file
481
src/org/fdroid/fdroid/data/AppProvider.java
Normal file
@ -0,0 +1,481 @@
|
||||
package org.fdroid.fdroid.data;
|
||||
|
||||
import android.content.*;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class AppProvider extends FDroidProvider {
|
||||
|
||||
/**
|
||||
* @see org.fdroid.fdroid.data.ApkProvider.MAX_APKS_TO_QUERY
|
||||
*/
|
||||
public static final int MAX_APPS_TO_QUERY = 900;
|
||||
|
||||
public static final class Helper {
|
||||
|
||||
private Helper() {}
|
||||
|
||||
public static List<App> all(ContentResolver resolver) {
|
||||
return all(resolver, DataColumns.ALL);
|
||||
}
|
||||
|
||||
public static List<App> all(ContentResolver resolver, String[] projection) {
|
||||
Uri uri = AppProvider.getContentUri();
|
||||
Cursor cursor = resolver.query(uri, projection, null, null, null);
|
||||
return cursorToList(cursor);
|
||||
}
|
||||
|
||||
private static List<App> cursorToList(Cursor cursor) {
|
||||
List<App> apps = new ArrayList<App>();
|
||||
if (cursor != null) {
|
||||
cursor.moveToFirst();
|
||||
while (!cursor.isAfterLast()) {
|
||||
apps.add(new App(cursor));
|
||||
cursor.moveToNext();
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
return apps;
|
||||
}
|
||||
|
||||
public static String getCategoryAll(Context context) {
|
||||
return context.getString(R.string.category_all);
|
||||
}
|
||||
|
||||
public static String getCategoryWhatsNew(Context context) {
|
||||
return context.getString(R.string.category_whatsnew);
|
||||
}
|
||||
|
||||
public static String getCategoryRecentlyUpdated(Context context) {
|
||||
return context.getString(R.string.category_recentlyupdated);
|
||||
}
|
||||
|
||||
public static List<String> categories(Context context) {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
Uri uri = getContentUri();
|
||||
String[] projection = { "DISTINCT " + DataColumns.CATEGORIES };
|
||||
Cursor cursor = resolver.query(uri, projection, null, null, null );
|
||||
Set<String> categorySet = new HashSet<String>();
|
||||
if (cursor != null) {
|
||||
cursor.moveToFirst();
|
||||
while (!cursor.isAfterLast()) {
|
||||
for( String s : Utils.CommaSeparatedList.make(cursor.getString(0))) {
|
||||
categorySet.add(s);
|
||||
}
|
||||
cursor.moveToNext();
|
||||
}
|
||||
}
|
||||
List<String> categories = new ArrayList<String>(categorySet);
|
||||
Collections.sort(categories);
|
||||
|
||||
// Populate the category list with the real categories, and the
|
||||
// locally generated meta-categories for "All", "What's New" and
|
||||
// "Recently Updated"...
|
||||
categories.add(0, getCategoryRecentlyUpdated(context));
|
||||
categories.add(0, getCategoryWhatsNew(context));
|
||||
categories.add(0, getCategoryAll(context));
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
public static App findById(ContentResolver resolver, String appId) {
|
||||
return findById(resolver, appId, DataColumns.ALL);
|
||||
}
|
||||
|
||||
public static App findById(ContentResolver resolver, String appId,
|
||||
String[] projection) {
|
||||
Uri uri = getContentUri(appId);
|
||||
Cursor cursor = resolver.query(uri, projection, null, null, null);
|
||||
if (cursor != null && cursor.getCount() > 0) {
|
||||
cursor.moveToFirst();
|
||||
return new App(cursor);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteAppsWithNoApks(ContentResolver resolver) {
|
||||
}
|
||||
}
|
||||
|
||||
public interface DataColumns {
|
||||
|
||||
public static final String _ID = "rowid as _id";
|
||||
public static final String _COUNT = "_count";
|
||||
public static final String IS_COMPATIBLE = "compatible";
|
||||
public static final String APP_ID = "id";
|
||||
public static final String NAME = "name";
|
||||
public static final String SUMMARY = "summary";
|
||||
public static final String ICON = "icon";
|
||||
public static final String DESCRIPTION = "description";
|
||||
public static final String LICENSE = "license";
|
||||
public static final String WEB_URL = "webURL";
|
||||
public static final String TRACKER_URL = "trackerURL";
|
||||
public static final String SOURCE_URL = "sourceURL";
|
||||
public static final String DONATE_URL = "donateURL";
|
||||
public static final String BITCOIN_ADDR = "bitcoinAddr";
|
||||
public static final String LITECOIN_ADDR = "litecoinAddr";
|
||||
public static final String DOGECOIN_ADDR = "dogecoinAddr";
|
||||
public static final String FLATTR_ID = "flattrID";
|
||||
public static final String CURRENT_VERSION = "curVersion";
|
||||
public static final String CURRENT_VERSION_CODE = "curVercode";
|
||||
public static final String CURRENT_APK = null;
|
||||
public static final String ADDED = "added";
|
||||
public static final String LAST_UPDATED = "lastUpdated";
|
||||
public static final String INSTALLED_VERSION = null;
|
||||
public static final String INSTALLED_VERCODE = null;
|
||||
public static final String USER_INSTALLED = null;
|
||||
public static final String CATEGORIES = "categories";
|
||||
public static final String ANTI_FEATURES = "antiFeatures";
|
||||
public static final String REQUIREMENTS = "requirements";
|
||||
public static final String FILTERED = null;
|
||||
public static final String HAS_UPDATES = null;
|
||||
public static final String TO_UPDATE = null;
|
||||
public static final String IGNORE_ALLUPDATES = "ignoreAllUpdates";
|
||||
public static final String IGNORE_THISUPDATE = "ignoreThisUpdate";
|
||||
public static final String ICON_URL = "iconUrl";
|
||||
public static final String UPDATED = null;
|
||||
public static final String APKS = null;
|
||||
|
||||
public static String[] ALL = {
|
||||
IS_COMPATIBLE, APP_ID, NAME, SUMMARY, ICON, DESCRIPTION,
|
||||
LICENSE, WEB_URL, TRACKER_URL, SOURCE_URL, DONATE_URL,
|
||||
BITCOIN_ADDR, LITECOIN_ADDR, DOGECOIN_ADDR, FLATTR_ID,
|
||||
CURRENT_VERSION, CURRENT_VERSION_CODE, ADDED, LAST_UPDATED,
|
||||
CATEGORIES, ANTI_FEATURES, REQUIREMENTS, IGNORE_ALLUPDATES,
|
||||
IGNORE_THISUPDATE, ICON_URL
|
||||
};
|
||||
}
|
||||
|
||||
private static final String PROVIDER_NAME = "AppProvider";
|
||||
|
||||
private static final UriMatcher matcher = new UriMatcher(-1);
|
||||
|
||||
private static final String PATH_INSTALLED = "installed";
|
||||
private static final String PATH_CAN_UPDATE = "canUpdate";
|
||||
private static final String PATH_SEARCH = "search";
|
||||
private static final String PATH_NO_APKS = "noApks";
|
||||
private static final String PATH_APPS = "apps";
|
||||
private static final String PATH_RECENTLY_UPDATED = "recentlyUpdated";
|
||||
private static final String PATH_NEWLY_ADDED = "newlyAdded";
|
||||
private static final String PATH_CATEGORY = "category";
|
||||
|
||||
private static final int CAN_UPDATE = CODE_SINGLE + 1;
|
||||
private static final int INSTALLED = CAN_UPDATE + 1;
|
||||
private static final int SEARCH = INSTALLED + 1;
|
||||
private static final int NO_APKS = SEARCH + 1;
|
||||
private static final int APPS = NO_APKS + 1;
|
||||
private static final int RECENTLY_UPDATED = APPS + 1;
|
||||
private static final int NEWLY_ADDED = RECENTLY_UPDATED + 1;
|
||||
private static final int CATEGORY = NEWLY_ADDED + 1;
|
||||
|
||||
static {
|
||||
matcher.addURI(getAuthority(), null, CODE_LIST);
|
||||
matcher.addURI(getAuthority(), PATH_RECENTLY_UPDATED, RECENTLY_UPDATED);
|
||||
matcher.addURI(getAuthority(), PATH_NEWLY_ADDED, NEWLY_ADDED);
|
||||
matcher.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY);
|
||||
matcher.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH);
|
||||
matcher.addURI(getAuthority(), PATH_CAN_UPDATE, CAN_UPDATE);
|
||||
matcher.addURI(getAuthority(), PATH_INSTALLED, INSTALLED);
|
||||
matcher.addURI(getAuthority(), PATH_NO_APKS, NO_APKS);
|
||||
matcher.addURI(getAuthority(), PATH_APPS + "/*", APPS);
|
||||
matcher.addURI(getAuthority(), "*", CODE_SINGLE);
|
||||
}
|
||||
|
||||
public static Uri getContentUri() {
|
||||
return Uri.parse("content://" + getAuthority());
|
||||
}
|
||||
|
||||
public static Uri getRecentlyUpdatedUri() {
|
||||
return Uri.withAppendedPath(getContentUri(), PATH_RECENTLY_UPDATED);
|
||||
}
|
||||
|
||||
public static Uri getNewlyAddedUri() {
|
||||
return Uri.withAppendedPath(getContentUri(), PATH_NEWLY_ADDED);
|
||||
}
|
||||
|
||||
public static Uri getCategoryUri(String category) {
|
||||
return getContentUri().buildUpon()
|
||||
.appendPath(PATH_CATEGORY)
|
||||
.appendPath(category)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Uri getNoApksUri() {
|
||||
return Uri.withAppendedPath(getContentUri(), PATH_NO_APKS);
|
||||
}
|
||||
|
||||
public static Uri getInstalledUri() {
|
||||
return Uri.withAppendedPath(getContentUri(), PATH_INSTALLED);
|
||||
}
|
||||
|
||||
public static Uri getCanUpdateUri() {
|
||||
return Uri.withAppendedPath(getContentUri(), PATH_CAN_UPDATE);
|
||||
}
|
||||
|
||||
public static Uri getContentUri(List<App> apps) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < apps.size(); i ++) {
|
||||
if (i != 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append(apps.get(i).id);
|
||||
}
|
||||
return getContentUri().buildUpon()
|
||||
.appendPath(PATH_APPS)
|
||||
.appendPath(builder.toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Uri getContentUri(App app) {
|
||||
return getContentUri(app.id);
|
||||
}
|
||||
|
||||
public static Uri getContentUri(String appId) {
|
||||
return Uri.withAppendedPath(getContentUri(), appId);
|
||||
}
|
||||
|
||||
public static Uri getSearchUri(String query) {
|
||||
return getContentUri().buildUpon()
|
||||
.appendPath(PATH_SEARCH)
|
||||
.appendPath(query)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTableName() {
|
||||
return DBHelper.TABLE_APP;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getProviderName() {
|
||||
return "AppProvider";
|
||||
}
|
||||
|
||||
public static String getAuthority() {
|
||||
return AUTHORITY + "." + PROVIDER_NAME;
|
||||
}
|
||||
|
||||
protected UriMatcher getMatcher() {
|
||||
return matcher;
|
||||
}
|
||||
|
||||
private QuerySelection queryCanUpdate() {
|
||||
Map<String, PackageInfo> installedApps = Utils.getInstalledApps(getContext());
|
||||
|
||||
String ignoreCurrent = " ignoreThisUpdate != curVercode ";
|
||||
String ignoreAll = " ignoreAllUpdates != 1 ";
|
||||
String ignore = " ( " + ignoreCurrent + " AND " + ignoreAll + " ) ";
|
||||
|
||||
StringBuilder where = new StringBuilder( ignore + " AND ( 0 ");
|
||||
String[] selectionArgs = new String[installedApps.size() * 2];
|
||||
int i = 0;
|
||||
for (PackageInfo info : installedApps.values() ) {
|
||||
where.append(" OR ( ")
|
||||
.append(AppProvider.DataColumns.APP_ID)
|
||||
.append(" = ? AND ")
|
||||
.append(DataColumns.CURRENT_VERSION_CODE)
|
||||
.append(" > ?) ");
|
||||
selectionArgs[ i * 2 ] = info.packageName;
|
||||
selectionArgs[ i * 2 + 1 ] = Integer.toString(info.versionCode);
|
||||
i ++;
|
||||
}
|
||||
where.append(") ");
|
||||
|
||||
return new QuerySelection(where.toString(), selectionArgs);
|
||||
}
|
||||
|
||||
private QuerySelection queryInstalled() {
|
||||
Map<String, PackageInfo> installedApps = Utils.getInstalledApps(getContext());
|
||||
StringBuilder where = new StringBuilder( " ( 0 ");
|
||||
String[] selectionArgs = new String[installedApps.size()];
|
||||
int i = 0;
|
||||
for (Map.Entry<String, PackageInfo> entry : installedApps.entrySet() ) {
|
||||
where.append(" OR ")
|
||||
.append(AppProvider.DataColumns.APP_ID)
|
||||
.append(" = ? ");
|
||||
selectionArgs[i] = entry.getKey();
|
||||
i ++;
|
||||
}
|
||||
where.append(" ) ");
|
||||
|
||||
return new QuerySelection(where.toString(), selectionArgs);
|
||||
}
|
||||
|
||||
private QuerySelection querySearch(String keywords) {
|
||||
keywords = "%" + keywords + "%";
|
||||
String selection =
|
||||
"id like ? OR " +
|
||||
"name like ? OR " +
|
||||
"summary like ? OR " +
|
||||
"description like ? ";
|
||||
String[] args = new String[] { keywords, keywords, keywords, keywords};
|
||||
return new QuerySelection(selection, args);
|
||||
}
|
||||
|
||||
private QuerySelection queryNewlyAdded() {
|
||||
String selection = "added > ?";
|
||||
String[] args = new String[] {
|
||||
Utils.DATE_FORMAT.format(Preferences.get().calcMaxHistory())
|
||||
};
|
||||
return new QuerySelection(selection, args);
|
||||
}
|
||||
|
||||
private QuerySelection queryRecentlyUpdated() {
|
||||
String selection = "added != lastUpdated AND lastUpdated > ?";
|
||||
String[] args = new String[] {
|
||||
Utils.DATE_FORMAT.format(Preferences.get().calcMaxHistory())
|
||||
};
|
||||
return new QuerySelection(selection, args);
|
||||
}
|
||||
|
||||
private QuerySelection queryCategory(String category) {
|
||||
// TODO: In the future, add a new table for categories,
|
||||
// so we can join onto it.
|
||||
String selection =
|
||||
" categories = ? OR " + // Only category e.g. "internet"
|
||||
" categories LIKE ? OR " + // First category e.g. "internet,%"
|
||||
" categories LIKE ? OR " + // Last category e.g. "%,internet"
|
||||
" categories LIKE ? "; // One of many categories e.g. "%,internet,%"
|
||||
String[] args = new String[] {
|
||||
category,
|
||||
category + ",%",
|
||||
"%," + category,
|
||||
"%," + category + ",%",
|
||||
};
|
||||
return new QuerySelection(selection, args);
|
||||
}
|
||||
|
||||
private QuerySelection queryNoApks() {
|
||||
String selection = "(SELECT COUNT(*) FROM fdroid_apk WHERE fdroid_apk.id = fdroid_app.id) = 0";
|
||||
return new QuerySelection(selection);
|
||||
}
|
||||
|
||||
private QuerySelection queryApps(String appIds) {
|
||||
String[] args = appIds.split(",");
|
||||
String selection = "id IN (" + generateQuestionMarksForInClause(args.length) + ")";
|
||||
return new QuerySelection(selection, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
QuerySelection query = new QuerySelection(selection, selectionArgs);
|
||||
switch (matcher.match(uri)) {
|
||||
case CODE_LIST:
|
||||
break;
|
||||
|
||||
case CODE_SINGLE:
|
||||
query = query.add(
|
||||
DataColumns.APP_ID + " = ?",
|
||||
new String[] { uri.getLastPathSegment() } );
|
||||
break;
|
||||
|
||||
case CAN_UPDATE:
|
||||
query = query.add(queryCanUpdate());
|
||||
break;
|
||||
|
||||
case INSTALLED:
|
||||
query = query.add(queryInstalled());
|
||||
break;
|
||||
|
||||
case SEARCH:
|
||||
query = query.add(querySearch(uri.getLastPathSegment()));
|
||||
break;
|
||||
|
||||
case NO_APKS:
|
||||
query = query.add(queryNoApks());
|
||||
break;
|
||||
|
||||
case APPS:
|
||||
query = query.add(queryApps(uri.getLastPathSegment()));
|
||||
break;
|
||||
|
||||
case CATEGORY:
|
||||
query = query.add(queryCategory(uri.getLastPathSegment()));
|
||||
break;
|
||||
|
||||
case RECENTLY_UPDATED:
|
||||
sortOrder = DataColumns.LAST_UPDATED + " DESC";
|
||||
query = query.add(queryRecentlyUpdated());
|
||||
break;
|
||||
|
||||
case NEWLY_ADDED:
|
||||
sortOrder = DataColumns.ADDED + " DESC";
|
||||
query = query.add(queryNewlyAdded());
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e("FDroid", "Invalid URI for app content provider: " + uri);
|
||||
throw new UnsupportedOperationException("Invalid URI for app content provider: " + uri);
|
||||
}
|
||||
|
||||
for (String field : projection) {
|
||||
if (field.equals(DataColumns._COUNT)) {
|
||||
projection = new String[] { "COUNT(*) AS " + DataColumns._COUNT };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Cursor cursor = read().query(getTableName(), projection, query.getSelection(),
|
||||
query.getArgs(), null, null, sortOrder);
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String where, String[] whereArgs) {
|
||||
|
||||
QuerySelection query = new QuerySelection(where, whereArgs);
|
||||
switch (matcher.match(uri)) {
|
||||
|
||||
case NO_APKS:
|
||||
query = query.add(queryNoApks());
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new UnsupportedOperationException("Can't delete yet");
|
||||
|
||||
}
|
||||
|
||||
int count = write().delete(getTableName(), query.getSelection(), query.getArgs());
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
long id = write().insertOrThrow(getTableName(), null, values);
|
||||
if (!isApplyingBatch()) {
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
}
|
||||
return getContentUri(values.getAsString(DataColumns.APP_ID));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
|
||||
QuerySelection query = new QuerySelection(where, whereArgs);
|
||||
switch (matcher.match(uri)) {
|
||||
|
||||
case CODE_SINGLE:
|
||||
query = query.add(new QuerySelection("id = ?", new String[] { uri.getLastPathSegment()}));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new UnsupportedOperationException("Update not supported for '" + uri + "'.");
|
||||
|
||||
}
|
||||
int count = write().update(getTableName(), values, query.getSelection(), query.getArgs());
|
||||
if (!isApplyingBatch()) {
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
}
|
@ -2,12 +2,13 @@ package org.fdroid.fdroid.data;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.DB;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -23,6 +24,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
// This information is retrieved from the repositories.
|
||||
public static final String TABLE_APK = "fdroid_apk";
|
||||
|
||||
|
||||
private static final String CREATE_TABLE_REPO = "create table "
|
||||
+ TABLE_REPO + " (_id integer primary key, "
|
||||
+ "address text not null, "
|
||||
@ -50,26 +52,41 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
+ "hashType string, "
|
||||
+ "added string, "
|
||||
+ "compatible int not null, "
|
||||
+ "incompatibleReasons text, "
|
||||
+ "primary key(id, vercode)"
|
||||
+ ");";
|
||||
|
||||
private static final String CREATE_TABLE_APP = "create table " + DB.TABLE_APP
|
||||
+ " ( " + "id text not null, " + "name text not null, "
|
||||
+ "summary text not null, " + "icon text, "
|
||||
+ "description text not null, " + "license text not null, "
|
||||
+ "webURL text, " + "trackerURL text, " + "sourceURL text, "
|
||||
+ "curVersion text," + "curVercode integer,"
|
||||
+ "antiFeatures string," + "donateURL string,"
|
||||
+ "bitcoinAddr string," + "litecoinAddr string,"
|
||||
public static final String TABLE_APP = "fdroid_app";
|
||||
private static final String CREATE_TABLE_APP = "CREATE TABLE " + TABLE_APP
|
||||
+ " ( "
|
||||
+ "id text not null, "
|
||||
+ "name text not null, "
|
||||
+ "summary text not null, "
|
||||
+ "icon text, "
|
||||
+ "description text not null, "
|
||||
+ "license text not null, "
|
||||
+ "webURL text, "
|
||||
+ "trackerURL text, "
|
||||
+ "sourceURL text, "
|
||||
+ "curVersion text,"
|
||||
+ "curVercode integer,"
|
||||
+ "antiFeatures string,"
|
||||
+ "donateURL string,"
|
||||
+ "bitcoinAddr string,"
|
||||
+ "litecoinAddr string,"
|
||||
+ "dogecoinAddr string,"
|
||||
+ "flattrID string," + "requirements string,"
|
||||
+ "categories string," + "added string,"
|
||||
+ "lastUpdated string," + "compatible int not null,"
|
||||
+ "flattrID string,"
|
||||
+ "requirements string,"
|
||||
+ "categories string,"
|
||||
+ "added string,"
|
||||
+ "lastUpdated string,"
|
||||
+ "compatible int not null,"
|
||||
+ "ignoreAllUpdates int not null,"
|
||||
+ "ignoreThisUpdate int not null,"
|
||||
+ "iconUrl text, "
|
||||
+ "primary key(id));";
|
||||
|
||||
private static final int DB_VERSION = 37;
|
||||
private static final int DB_VERSION = 39;
|
||||
|
||||
private Context context;
|
||||
|
||||
@ -80,18 +97,20 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
|
||||
private void populateRepoNames(SQLiteDatabase db, int oldVersion) {
|
||||
if (oldVersion < 37) {
|
||||
Log.i("FDroid", "Populating repo names from the url");
|
||||
String[] columns = { "address", "_id" };
|
||||
Cursor cursor = db.query(TABLE_REPO, columns,
|
||||
"name IS NULL OR name = ''", null, null, null, null);
|
||||
cursor.moveToFirst();
|
||||
if (cursor.getCount() > 0) {
|
||||
cursor.moveToFirst();
|
||||
while (!cursor.isAfterLast()) {
|
||||
String address = cursor.getString(0);
|
||||
long id = cursor.getInt(1);
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put("name", Repo.addressToName(address));
|
||||
String name = Repo.addressToName(address);
|
||||
values.put("name", name);
|
||||
String[] args = { Long.toString( id ) };
|
||||
Log.i("FDroid", "Setting repo name to '" + name + "' for repo " + address);
|
||||
db.update(TABLE_REPO, values, "_id = ?", args);
|
||||
cursor.moveToNext();
|
||||
}
|
||||
@ -103,7 +122,6 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
if (oldVersion < 36) {
|
||||
|
||||
Log.d("FDroid", "Renaming " + TABLE_REPO + ".id to _id");
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
@ -166,7 +184,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
context.getString(R.string.default_repo_description));
|
||||
values.put("version", 0);
|
||||
String pubkey = context.getString(R.string.default_repo_pubkey);
|
||||
String fingerprint = DB.calcFingerprint(pubkey);
|
||||
String fingerprint = Utils.calcFingerprint(pubkey);
|
||||
values.put("pubkey", pubkey);
|
||||
values.put("fingerprint", fingerprint);
|
||||
values.put("maxage", 0);
|
||||
@ -296,7 +314,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
c.close();
|
||||
for (Repo repo : oldrepos) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("fingerprint", DB.calcFingerprint(repo.pubkey));
|
||||
values.put("fingerprint", Utils.calcFingerprint(repo.pubkey));
|
||||
db.update(TABLE_REPO, values, "address = ?", new String[] { repo.address });
|
||||
}
|
||||
}
|
||||
@ -316,6 +334,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
|
||||
private void addLastUpdatedToRepo(SQLiteDatabase db, int oldVersion) {
|
||||
if (oldVersion < 35 && !columnExists(db, TABLE_REPO, "lastUpdated")) {
|
||||
Log.i("FDroid", "Adding lastUpdated column to " + TABLE_REPO);
|
||||
db.execSQL("Alter table " + TABLE_REPO + " add column lastUpdated string");
|
||||
}
|
||||
}
|
||||
@ -323,7 +342,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
private void resetTransient(SQLiteDatabase db) {
|
||||
context.getSharedPreferences("FDroid", Context.MODE_PRIVATE).edit()
|
||||
.putBoolean("triedEmptyUpdate", false).commit();
|
||||
db.execSQL("drop table " + DB.TABLE_APP);
|
||||
db.execSQL("drop table " + TABLE_APP);
|
||||
db.execSQL("drop table " + TABLE_APK);
|
||||
db.execSQL("update " + TABLE_REPO + " set lastetag = NULL");
|
||||
createAppApk(db);
|
||||
@ -331,7 +350,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static void createAppApk(SQLiteDatabase db) {
|
||||
db.execSQL(CREATE_TABLE_APP);
|
||||
db.execSQL("create index app_id on " + DB.TABLE_APP + " (id);");
|
||||
db.execSQL("create index app_id on " + TABLE_APP + " (id);");
|
||||
db.execSQL(CREATE_TABLE_APK);
|
||||
db.execSQL("create index apk_vercode on " + TABLE_APK + " (vercode);");
|
||||
db.execSQL("create index apk_id on " + TABLE_APK + " (id);");
|
||||
|
@ -1,11 +1,12 @@
|
||||
package org.fdroid.fdroid.data;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.UriMatcher;
|
||||
import android.content.*;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
|
||||
abstract class FDroidProvider extends ContentProvider {
|
||||
import java.util.ArrayList;
|
||||
|
||||
public abstract class FDroidProvider extends ContentProvider {
|
||||
|
||||
public static final String AUTHORITY = "org.fdroid.fdroid.data";
|
||||
|
||||
@ -14,10 +15,39 @@ abstract class FDroidProvider extends ContentProvider {
|
||||
|
||||
private DBHelper dbHelper;
|
||||
|
||||
private boolean isApplyingBatch = false;
|
||||
|
||||
abstract protected String getTableName();
|
||||
|
||||
abstract protected String getProviderName();
|
||||
|
||||
/**
|
||||
* Tells us if we are in the middle of a batch of operations. Allows us to
|
||||
* decide not to notify the content resolver of changes,
|
||||
* every single time we do something during many operations.
|
||||
* Based on http://stackoverflow.com/a/15886915.
|
||||
* @return
|
||||
*/
|
||||
protected final boolean isApplyingBatch() {
|
||||
return this.isApplyingBatch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
|
||||
throws OperationApplicationException {
|
||||
ContentProviderResult[] result = null;
|
||||
isApplyingBatch = true;
|
||||
write().beginTransaction();
|
||||
try {
|
||||
result = super.applyBatch(operations);
|
||||
write().setTransactionSuccessful();
|
||||
} finally {
|
||||
write().endTransaction();
|
||||
isApplyingBatch = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
dbHelper = new DBHelper(getContext());
|
||||
@ -55,5 +85,15 @@ abstract class FDroidProvider extends ContentProvider {
|
||||
|
||||
abstract protected UriMatcher getMatcher();
|
||||
|
||||
protected String generateQuestionMarksForInClause(int num) {
|
||||
StringBuilder sb = new StringBuilder(num * 2);
|
||||
for (int i = 0; i < num; i ++) {
|
||||
if (i != 0) {
|
||||
sb.append(',');
|
||||
}
|
||||
sb.append('?');
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
79
src/org/fdroid/fdroid/data/QuerySelection.java
Normal file
79
src/org/fdroid/fdroid/data/QuerySelection.java
Normal file
@ -0,0 +1,79 @@
|
||||
package org.fdroid.fdroid.data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper class used by sublasses of ContentProvider to make the constraints
|
||||
* required for a given content URI (e.g. all apps that belong to a repo)
|
||||
* easily appendable to the constraints which are passed into, e.g. the query()
|
||||
* method in the content provider.
|
||||
*/
|
||||
public class QuerySelection {
|
||||
|
||||
private final String[] args;
|
||||
private final String selection;
|
||||
|
||||
public QuerySelection(String selection) {
|
||||
this.selection = selection;
|
||||
this.args = null;
|
||||
}
|
||||
|
||||
public QuerySelection(String selection, String[] args) {
|
||||
this.args = args;
|
||||
this.selection = selection;
|
||||
}
|
||||
|
||||
public QuerySelection(String selection, List<String> args) {
|
||||
this.args = new String[ args.size() ];
|
||||
args.toArray( this.args );
|
||||
this.selection = selection;
|
||||
}
|
||||
|
||||
public String[] getArgs() {
|
||||
return args;
|
||||
}
|
||||
|
||||
public String getSelection() {
|
||||
return selection;
|
||||
}
|
||||
|
||||
public boolean hasSelection() {
|
||||
return selection != null && selection.length() > 0;
|
||||
}
|
||||
|
||||
public boolean hasArgs() {
|
||||
return args != null && args.length > 0;
|
||||
}
|
||||
|
||||
public QuerySelection add(String selection, String[] args) {
|
||||
return add(new QuerySelection(selection, args));
|
||||
}
|
||||
|
||||
public QuerySelection add(QuerySelection query) {
|
||||
String s = null;
|
||||
if (this.hasSelection() && query.hasSelection()) {
|
||||
s = " (" + this.selection + ") AND (" + query.getSelection() + ") ";
|
||||
} else if (this.hasSelection()) {
|
||||
s = this.selection;
|
||||
} else if (query.hasSelection() ) {
|
||||
s = query.selection;
|
||||
}
|
||||
|
||||
int thisNumArgs = this.hasArgs() ? this.args.length : 0;
|
||||
int queryNumArgs = query.hasArgs() ? query.args.length : 0;
|
||||
List<String> a = new ArrayList<String>(thisNumArgs + queryNumArgs);
|
||||
|
||||
if (this.hasArgs()) {
|
||||
Collections.addAll(a, this.args);
|
||||
}
|
||||
|
||||
if (query.hasArgs()) {
|
||||
Collections.addAll(a, query.getArgs());
|
||||
}
|
||||
|
||||
return new QuerySelection(s, a);
|
||||
}
|
||||
|
||||
}
|
@ -3,14 +3,16 @@ package org.fdroid.fdroid.data;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.DB;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.text.ParseException;
|
||||
import java.util.Date;
|
||||
|
||||
public class Repo extends ValueObject{
|
||||
public class Repo extends ValueObject {
|
||||
|
||||
public static final int VERSION_DENSITY_SPECIFIC_ICONS = 11;
|
||||
|
||||
private long id;
|
||||
|
||||
@ -31,6 +33,9 @@ public class Repo extends ValueObject{
|
||||
}
|
||||
|
||||
public Repo(Cursor cursor) {
|
||||
|
||||
checkCursorPosition(cursor);
|
||||
|
||||
for(int i = 0; i < cursor.getColumnCount(); i ++ ) {
|
||||
String column = cursor.getColumnName(i);
|
||||
if (column.equals(RepoProvider.DataColumns._ID)) {
|
||||
@ -132,7 +137,7 @@ public class Repo extends ValueObject{
|
||||
String dateString = values.getAsString(RepoProvider.DataColumns.LAST_UPDATED);
|
||||
if (dateString != null) {
|
||||
try {
|
||||
lastUpdated = DB.DATE_FORMAT.parse(dateString);
|
||||
lastUpdated = Utils.DATE_FORMAT.parse(dateString);
|
||||
} catch (ParseException e) {
|
||||
Log.e("FDroid", "Error parsing date " + dateString);
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.DB;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -15,6 +15,7 @@ import java.util.List;
|
||||
public class RepoProvider extends FDroidProvider {
|
||||
|
||||
public static final class Helper {
|
||||
|
||||
public static final String TAG = "RepoProvider.Helper";
|
||||
|
||||
private Helper() {}
|
||||
@ -105,7 +106,7 @@ public class RepoProvider extends FDroidProvider {
|
||||
*/
|
||||
if (values.containsKey(DataColumns.PUBLIC_KEY)) {
|
||||
String publicKey = values.getAsString(DataColumns.PUBLIC_KEY);
|
||||
String calcedFingerprint = DB.calcFingerprint(publicKey);
|
||||
String calcedFingerprint = Utils.calcFingerprint(publicKey);
|
||||
if (values.containsKey(DataColumns.FINGERPRINT)) {
|
||||
String fingerprint = values.getAsString(DataColumns.FINGERPRINT);
|
||||
if (!TextUtils.isEmpty(publicKey)) {
|
||||
@ -153,15 +154,16 @@ public class RepoProvider extends FDroidProvider {
|
||||
resolver.delete(uri, null, null);
|
||||
}
|
||||
|
||||
public static void purgeApps(Repo repo, FDroidApp app) {
|
||||
// TODO: Once we have content providers for apps and apks, use them
|
||||
// to do this...
|
||||
DB db = DB.getDB();
|
||||
try {
|
||||
db.purgeApps(repo, app);
|
||||
} finally {
|
||||
DB.releaseDB();
|
||||
}
|
||||
public static void purgeApps(Context context, Repo repo, FDroidApp app) {
|
||||
Uri apkUri = ApkProvider.getRepoUri(repo.getId());
|
||||
int apkCount = context.getContentResolver().delete(apkUri, null, null);
|
||||
Log.d("FDroid", "Removed " + apkCount + " apks from repo " + repo.name);
|
||||
|
||||
Uri appUri = AppProvider.getNoApksUri();
|
||||
int appCount = context.getContentResolver().delete(appUri, null, null);
|
||||
Log.d("Log", "Removed " + appCount + " apps with no apks.");
|
||||
|
||||
app.invalidateAllApps();
|
||||
}
|
||||
|
||||
public static int countAppsForRepo(ContentResolver resolver,
|
||||
|
@ -1,18 +1,27 @@
|
||||
package org.fdroid.fdroid.data;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.DB;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.Date;
|
||||
|
||||
abstract class ValueObject {
|
||||
|
||||
protected void checkCursorPosition(Cursor cursor) throws IllegalArgumentException {
|
||||
if (cursor.getPosition() == -1) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cursor position is -1. " +
|
||||
"Did you forget to moveToFirst() or move() before passing to the value object?");
|
||||
}
|
||||
}
|
||||
|
||||
static Date toDate(String string) {
|
||||
Date date = null;
|
||||
if (string != null) {
|
||||
try {
|
||||
date = DB.DATE_FORMAT.parse(string);
|
||||
date = Utils.DATE_FORMAT.parse(string);
|
||||
} catch (ParseException e) {
|
||||
Log.e("FDroid", "Error parsing date " + string);
|
||||
}
|
||||
|
@ -4,10 +4,11 @@ import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.DB;
|
||||
import org.fdroid.fdroid.ProgressListener;
|
||||
import org.fdroid.fdroid.RepoXMLHandler;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.data.RepoProvider;
|
||||
import org.fdroid.fdroid.net.Downloader;
|
||||
@ -40,7 +41,8 @@ abstract public class RepoUpdater {
|
||||
|
||||
protected final Context context;
|
||||
protected final Repo repo;
|
||||
protected final List<DB.App> apps = new ArrayList<DB.App>();
|
||||
private List<App> apps = new ArrayList<App>();
|
||||
private List<Apk> apks = new ArrayList<Apk>();
|
||||
protected boolean hasChanged = false;
|
||||
protected ProgressListener progressListener;
|
||||
|
||||
@ -57,10 +59,14 @@ abstract public class RepoUpdater {
|
||||
return hasChanged;
|
||||
}
|
||||
|
||||
public List<DB.App> getApps() {
|
||||
public List<App> getApps() {
|
||||
return apps;
|
||||
}
|
||||
|
||||
public List<Apk> getApks() {
|
||||
return apks;
|
||||
}
|
||||
|
||||
public boolean isInteractive() {
|
||||
return progressListener != null;
|
||||
}
|
||||
@ -173,7 +179,7 @@ abstract public class RepoUpdater {
|
||||
// Process the index...
|
||||
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
|
||||
XMLReader reader = parser.getXMLReader();
|
||||
RepoXMLHandler handler = new RepoXMLHandler(repo, apps, progressListener);
|
||||
RepoXMLHandler handler = new RepoXMLHandler(repo, progressListener);
|
||||
|
||||
if (isInteractive()) {
|
||||
// Only bother spending the time to count the expected apps
|
||||
@ -186,6 +192,8 @@ abstract public class RepoUpdater {
|
||||
new BufferedReader(new FileReader(indexFile)));
|
||||
|
||||
reader.parse(is);
|
||||
apps = handler.getApps();
|
||||
apks = handler.getApks();
|
||||
updateRepo(handler, downloader.getETag());
|
||||
}
|
||||
} catch (SAXException e) {
|
||||
@ -216,7 +224,7 @@ abstract public class RepoUpdater {
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
values.put(RepoProvider.DataColumns.LAST_UPDATED, DB.DATE_FORMAT.format(new Date()));
|
||||
values.put(RepoProvider.DataColumns.LAST_UPDATED, Utils.DATE_FORMAT.format(new Date()));
|
||||
|
||||
if (repo.lastetag == null || !repo.lastetag.equals(etag)) {
|
||||
values.put(RepoProvider.DataColumns.LAST_ETAG, etag);
|
||||
|
@ -2,7 +2,6 @@ package org.fdroid.fdroid.updater;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.DB;
|
||||
import org.fdroid.fdroid.Hasher;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
@ -30,7 +29,7 @@ public class SignedRepoUpdater extends RepoUpdater {
|
||||
boolean match = false;
|
||||
for (Certificate cert : certs) {
|
||||
String certdata = Hasher.hex(cert);
|
||||
if (repo.pubkey == null && repo.fingerprint.equals(DB.calcFingerprint(cert))) {
|
||||
if (repo.pubkey == null && repo.fingerprint.equals(Utils.calcFingerprint(cert))) {
|
||||
repo.pubkey = certdata;
|
||||
}
|
||||
if (repo.pubkey != null && repo.pubkey.equals(certdata)) {
|
||||
|
@ -2,9 +2,7 @@ package org.fdroid.fdroid.updater;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.DB;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.net.Downloader;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
|
@ -1,50 +1,60 @@
|
||||
package org.fdroid.fdroid.views;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Typeface;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.support.v4.widget.CursorAdapter;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.*;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import org.fdroid.fdroid.DB;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.compat.LayoutCompat;
|
||||
|
||||
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
|
||||
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
|
||||
abstract public class AppListAdapter extends BaseAdapter {
|
||||
abstract public class AppListAdapter extends CursorAdapter {
|
||||
|
||||
private List<DB.App> items = new ArrayList<DB.App>();
|
||||
private Context mContext;
|
||||
private LayoutInflater mInflater;
|
||||
private DisplayImageOptions displayImageOptions;
|
||||
|
||||
public AppListAdapter(Context context) {
|
||||
public AppListAdapter(Context context, Cursor c) {
|
||||
super(context, c);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public AppListAdapter(Context context, Cursor c, boolean autoRequery) {
|
||||
super(context, c, autoRequery);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public AppListAdapter(Context context, Cursor c, int flags) {
|
||||
super(context, c, flags);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
mContext = context;
|
||||
mInflater = (LayoutInflater) mContext.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE);
|
||||
|
||||
displayImageOptions = new DisplayImageOptions.Builder()
|
||||
.cacheInMemory(true)
|
||||
.cacheOnDisc(true)
|
||||
.imageScaleType(ImageScaleType.NONE)
|
||||
.resetViewBeforeLoading(true)
|
||||
.showImageOnLoading(R.drawable.ic_repo_app_default)
|
||||
.showImageForEmptyUri(R.drawable.ic_repo_app_default)
|
||||
.displayer(new FadeInBitmapDisplayer(200, true, true, false))
|
||||
.bitmapConfig(Bitmap.Config.RGB_565)
|
||||
.build();
|
||||
.cacheInMemory(true)
|
||||
.cacheOnDisc(true)
|
||||
.imageScaleType(ImageScaleType.NONE)
|
||||
.resetViewBeforeLoading(true)
|
||||
.showImageOnLoading(R.drawable.ic_repo_app_default)
|
||||
.showImageForEmptyUri(R.drawable.ic_repo_app_default)
|
||||
.displayer(new FadeInBitmapDisplayer(200, true, true, false))
|
||||
.bitmapConfig(Bitmap.Config.RGB_565)
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
@ -52,33 +62,6 @@ abstract public class AppListAdapter extends BaseAdapter {
|
||||
|
||||
abstract protected boolean showStatusInstalled();
|
||||
|
||||
public void addItem(DB.App app) {
|
||||
items.add(app);
|
||||
}
|
||||
|
||||
public void addItems(List<DB.App> apps) {
|
||||
items.addAll(apps);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
items.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return items.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return items.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
private static class ViewHolder {
|
||||
TextView name;
|
||||
TextView summary;
|
||||
@ -88,26 +71,32 @@ abstract public class AppListAdapter extends BaseAdapter {
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
View view = mInflater.inflate(R.layout.applistitem, null);
|
||||
|
||||
ViewHolder holder = new ViewHolder();
|
||||
holder.name = (TextView) view.findViewById(R.id.name);
|
||||
holder.summary = (TextView) view.findViewById(R.id.summary);
|
||||
holder.status = (TextView) view.findViewById(R.id.status);
|
||||
holder.license = (TextView) view.findViewById(R.id.license);
|
||||
holder.icon = (ImageView) view.findViewById(R.id.icon);
|
||||
view.setTag(holder);
|
||||
|
||||
setupView(context, view, cursor, holder);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
ViewHolder holder = (ViewHolder)view.getTag();
|
||||
setupView(context, view, cursor, holder);
|
||||
}
|
||||
|
||||
private void setupView(Context context, View view, Cursor cursor, ViewHolder holder) {
|
||||
final App app = new App(cursor);
|
||||
|
||||
boolean compact = Preferences.get().hasCompactLayout();
|
||||
DB.App app = items.get(position);
|
||||
ViewHolder holder;
|
||||
|
||||
if (convertView == null) {
|
||||
convertView = mInflater.inflate(R.layout.applistitem, null);
|
||||
|
||||
holder = new ViewHolder();
|
||||
holder.name = (TextView) convertView.findViewById(R.id.name);
|
||||
holder.summary = (TextView) convertView.findViewById(R.id.summary);
|
||||
holder.status = (TextView) convertView.findViewById(R.id.status);
|
||||
holder.license = (TextView) convertView.findViewById(R.id.license);
|
||||
holder.icon = (ImageView) convertView.findViewById(R.id.icon);
|
||||
|
||||
convertView.setTag(holder);
|
||||
} else {
|
||||
holder = (ViewHolder) convertView.getTag();
|
||||
}
|
||||
|
||||
holder.name.setText(app.name);
|
||||
holder.summary.setText(app.summary);
|
||||
@ -121,47 +110,50 @@ abstract public class AppListAdapter extends BaseAdapter {
|
||||
|
||||
// Disable it all if it isn't compatible...
|
||||
View[] views = {
|
||||
convertView,
|
||||
view,
|
||||
holder.status,
|
||||
holder.summary,
|
||||
holder.license,
|
||||
holder.name
|
||||
};
|
||||
|
||||
for (View view : views) {
|
||||
view.setEnabled(app.compatible && !app.filtered);
|
||||
for (View v : views) {
|
||||
v.setEnabled(app.compatible && !app.isFiltered());
|
||||
}
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
private String ellipsize(String input, int maxLength) {
|
||||
private String ellipsize(String input, int maxLength) {
|
||||
if (input == null || input.length() < maxLength+1) {
|
||||
return input;
|
||||
}
|
||||
return input.substring(0, maxLength) + "…";
|
||||
}
|
||||
|
||||
private String getVersionInfo(DB.App app) {
|
||||
private String getVersionInfo(App app) {
|
||||
|
||||
if (app.curApk == null) {
|
||||
if (app.curVercode <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (app.installedVersion == null) {
|
||||
return ellipsize(app.curApk.version, 12);
|
||||
PackageInfo installedInfo = app.getInstalledInfo(mContext);
|
||||
|
||||
if (installedInfo == null) {
|
||||
return ellipsize(app.curVersion, 12);
|
||||
}
|
||||
|
||||
if (app.toUpdate && showStatusUpdate()) {
|
||||
return ellipsize(app.installedVersion, 8) +
|
||||
" → " + ellipsize(app.curApk.version, 8);
|
||||
String installedVersionString = installedInfo.versionName;
|
||||
int installedVersionCode = installedInfo.versionCode;
|
||||
|
||||
if (app.canAndWantToUpdate(mContext) && showStatusUpdate()) {
|
||||
return ellipsize(installedVersionString, 8) +
|
||||
" → " + ellipsize(app.curVersion, 8);
|
||||
}
|
||||
|
||||
if (app.installedVerCode > 0 && showStatusInstalled()) {
|
||||
return ellipsize(app.installedVersion, 12) + " ✔";
|
||||
if (installedVersionCode > 0 && showStatusInstalled()) {
|
||||
return ellipsize(installedVersionString, 12) + " ✔";
|
||||
}
|
||||
|
||||
return app.installedVersion;
|
||||
return installedVersionString;
|
||||
}
|
||||
|
||||
private void layoutIcon(ImageView icon, boolean compact) {
|
||||
|
@ -1,10 +1,13 @@
|
||||
package org.fdroid.fdroid.views;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
|
||||
import org.fdroid.fdroid.FDroid;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.views.fragments.AvailableAppsFragment;
|
||||
import org.fdroid.fdroid.views.fragments.CanUpdateAppsFragment;
|
||||
import org.fdroid.fdroid.views.fragments.InstalledAppsFragment;
|
||||
@ -19,7 +22,20 @@ public class AppListFragmentPageAdapter extends FragmentPagerAdapter {
|
||||
|
||||
public AppListFragmentPageAdapter(FDroid parent) {
|
||||
super(parent.getSupportFragmentManager());
|
||||
this.parent = parent;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
private String getUpdateTabTitle() {
|
||||
Uri uri = AppProvider.getCanUpdateUri();
|
||||
String[] projection = new String[] { AppProvider.DataColumns._COUNT };
|
||||
Cursor cursor = parent.getContentResolver().query(uri, projection, null, null, null);
|
||||
String suffix = "";
|
||||
if (cursor != null && cursor.getCount() == 1) {
|
||||
cursor.moveToFirst();
|
||||
int count = cursor.getInt(0);
|
||||
suffix = " (" + count + ")";
|
||||
}
|
||||
return parent.getString(R.string.tab_updates) + suffix;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -46,8 +62,7 @@ public class AppListFragmentPageAdapter extends FragmentPagerAdapter {
|
||||
case 1:
|
||||
return parent.getString(R.string.inst);
|
||||
case 2:
|
||||
return parent.getString(R.string.tab_updates) + " ("
|
||||
+ parent.getManager().getCanUpdateAdapter().getCount() + ")";
|
||||
return getUpdateTabTitle();
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
@ -1,10 +1,20 @@
|
||||
package org.fdroid.fdroid.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
public class AvailableAppListAdapter extends AppListAdapter {
|
||||
public AvailableAppListAdapter(Context context) {
|
||||
super(context);
|
||||
|
||||
public AvailableAppListAdapter(Context context, Cursor c) {
|
||||
super(context, c);
|
||||
}
|
||||
|
||||
public AvailableAppListAdapter(Context context, Cursor c, boolean autoRequery) {
|
||||
super(context, c, autoRequery);
|
||||
}
|
||||
|
||||
public AvailableAppListAdapter(Context context, Cursor c, int flags) {
|
||||
super(context, c, flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,10 +1,20 @@
|
||||
package org.fdroid.fdroid.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
public class CanUpdateAppListAdapter extends AppListAdapter {
|
||||
public CanUpdateAppListAdapter(Context context) {
|
||||
super(context);
|
||||
|
||||
public CanUpdateAppListAdapter(Context context, Cursor c) {
|
||||
super(context, c);
|
||||
}
|
||||
|
||||
public CanUpdateAppListAdapter(Context context, Cursor c, boolean autoRequery) {
|
||||
super(context, c, autoRequery);
|
||||
}
|
||||
|
||||
public CanUpdateAppListAdapter(Context context, Cursor c, int flags) {
|
||||
super(context, c, flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,10 +1,20 @@
|
||||
package org.fdroid.fdroid.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
public class InstalledAppListAdapter extends AppListAdapter {
|
||||
public InstalledAppListAdapter(Context context) {
|
||||
super(context);
|
||||
|
||||
public InstalledAppListAdapter(Context context, Cursor c) {
|
||||
super(context, c);
|
||||
}
|
||||
|
||||
public InstalledAppListAdapter(Context context, Cursor c, boolean autoRequery) {
|
||||
super(context, c, autoRequery);
|
||||
}
|
||||
|
||||
public InstalledAppListAdapter(Context context, Cursor c, int flags) {
|
||||
super(context, c, flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -8,6 +8,7 @@ import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.compat.ActionBarCompat;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.data.RepoProvider;
|
||||
@ -20,6 +21,9 @@ public class RepoDetailsActivity extends FragmentActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
||||
((FDroidApp) getApplication()).applyTheme(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
long repoId = getIntent().getLongExtra(RepoDetailsFragment.ARG_REPO_ID, 0);
|
||||
|
@ -1,31 +1,114 @@
|
||||
package org.fdroid.fdroid.views.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.ListFragment;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
|
||||
import android.support.v4.app.Fragment;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.listener.PauseOnScrollListener;
|
||||
import org.fdroid.fdroid.*;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.views.AppListAdapter;
|
||||
import org.fdroid.fdroid.views.AppListView;
|
||||
|
||||
abstract class AppListFragment extends Fragment implements AdapterView.OnItemClickListener, Preferences.ChangeListener {
|
||||
abstract public class AppListFragment extends ListFragment implements
|
||||
AdapterView.OnItemClickListener,
|
||||
Preferences.ChangeListener,
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
protected FDroid parent;
|
||||
public static final String[] APP_PROJECTION = {
|
||||
AppProvider.DataColumns._ID,
|
||||
AppProvider.DataColumns.APP_ID,
|
||||
AppProvider.DataColumns.NAME,
|
||||
AppProvider.DataColumns.SUMMARY,
|
||||
AppProvider.DataColumns.IS_COMPATIBLE,
|
||||
AppProvider.DataColumns.LICENSE,
|
||||
AppProvider.DataColumns.ICON,
|
||||
AppProvider.DataColumns.ICON_URL,
|
||||
AppProvider.DataColumns.CURRENT_VERSION,
|
||||
AppProvider.DataColumns.CURRENT_VERSION_CODE,
|
||||
AppProvider.DataColumns.REQUIREMENTS, // Needed for filtering apps that require root.
|
||||
};
|
||||
|
||||
public static final String APP_SORT = AppProvider.DataColumns.NAME;
|
||||
|
||||
protected AppListAdapter appAdapter;
|
||||
|
||||
protected abstract AppListAdapter getAppListAdapter();
|
||||
|
||||
protected abstract String getFromTitle();
|
||||
|
||||
protected abstract Uri getDataUri();
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
// Can't do this in the onCreate view, because "onCreateView" which
|
||||
// returns the list view is "called between onCreate and
|
||||
// onActivityCreated" according to the docs.
|
||||
getListView().setFastScrollEnabled(true);
|
||||
getListView().setOnItemClickListener(this);
|
||||
getListView().setOnScrollListener(new PauseOnScrollListener(ImageLoader.getInstance(), false, true));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
//Starts a new or restarts an existing Loader in this manager
|
||||
getLoaderManager().restartLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Preferences.get().registerCompactLayoutChangeListener(this);
|
||||
|
||||
appAdapter = getAppListAdapter();
|
||||
|
||||
if (appAdapter.getCount() == 0) {
|
||||
updateEmptyRepos();
|
||||
}
|
||||
|
||||
setListAdapter(appAdapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* The first time the app is run, we will have an empty app list.
|
||||
* If this is the case, we will attempt to update with the default repo.
|
||||
* However, if we have tried this at least once, then don't try to do
|
||||
* it automatically again, because the repos or internet connection may
|
||||
* be bad.
|
||||
*/
|
||||
public boolean updateEmptyRepos() {
|
||||
final String TRIED_EMPTY_UPDATE = "triedEmptyUpdate";
|
||||
SharedPreferences prefs = getActivity().getPreferences(Context.MODE_PRIVATE);
|
||||
boolean hasTriedEmptyUpdate = prefs.getBoolean(TRIED_EMPTY_UPDATE, false);
|
||||
if (!hasTriedEmptyUpdate) {
|
||||
Log.d("FDroid", "Empty app list, and we haven't done an update yet. Forcing repo update.");
|
||||
prefs.edit().putBoolean(TRIED_EMPTY_UPDATE, true).commit();
|
||||
UpdateService.updateNow(getActivity());
|
||||
return true;
|
||||
} else {
|
||||
Log.d("FDroid", "Empty app list, but it looks like we've had an update previously. Will not force repo update.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -34,48 +117,9 @@ abstract class AppListFragment extends Fragment implements AdapterView.OnItemCli
|
||||
Preferences.get().unregisterCompactLayoutChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
try {
|
||||
parent = (FDroid)activity;
|
||||
} catch (ClassCastException e) {
|
||||
// I know fragments are meant to be activity agnostic, but I can't
|
||||
// think of a better way to share the one application list between
|
||||
// all three app list fragments.
|
||||
throw new RuntimeException(
|
||||
"AppListFragment can only be attached to FDroid activity. " +
|
||||
"Here it was attached to a " + activity.getClass() );
|
||||
}
|
||||
}
|
||||
|
||||
public AppListManager getAppListManager() {
|
||||
return parent.getManager();
|
||||
}
|
||||
|
||||
protected AppListView createPlainAppList() {
|
||||
AppListView view = new AppListView(getActivity());
|
||||
ListView list = createAppListView();
|
||||
view.addView(
|
||||
list,
|
||||
new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
view.setAppList(list);
|
||||
return view;
|
||||
}
|
||||
|
||||
protected ListView createAppListView() {
|
||||
ListView list = new ListView(getActivity());
|
||||
list.setFastScrollEnabled(true);
|
||||
list.setOnItemClickListener(this);
|
||||
list.setAdapter(getAppListAdapter());
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
final DB.App app = (DB.App)getAppListAdapter().getItem(position);
|
||||
final App app = new App((Cursor)getListView().getItemAtPosition(position));
|
||||
Intent intent = new Intent(getActivity(), AppDetails.class);
|
||||
intent.putExtra("appid", app.id);
|
||||
intent.putExtra("from", getFromTitle());
|
||||
@ -86,4 +130,22 @@ abstract class AppListFragment extends Fragment implements AdapterView.OnItemCli
|
||||
public void onPreferenceChange() {
|
||||
getAppListAdapter().notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
||||
appAdapter.swapCursor(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
appAdapter.swapCursor(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
Uri uri = getDataUri();
|
||||
return new CursorLoader(
|
||||
getActivity(), uri, APP_PROJECTION, null, null, APP_SORT);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,28 +1,74 @@
|
||||
package org.fdroid.fdroid.views.fragments;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.*;
|
||||
|
||||
import org.fdroid.fdroid.views.AppListAdapter;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.views.AppListView;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.views.AppListAdapter;
|
||||
import org.fdroid.fdroid.views.AvailableAppListAdapter;
|
||||
|
||||
public class AvailableAppsFragment extends AppListFragment implements AdapterView.OnItemSelectedListener {
|
||||
import java.util.List;
|
||||
|
||||
public class AvailableAppsFragment extends AppListFragment implements
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
private String currentCategory = null;
|
||||
private AppListAdapter adapter = null;
|
||||
|
||||
@Override
|
||||
protected String getFromTitle() {
|
||||
return "Available";
|
||||
}
|
||||
|
||||
protected AppListAdapter getAppListAdapter() {
|
||||
if (adapter == null) {
|
||||
final AppListAdapter a = new AvailableAppListAdapter(getActivity(), null);
|
||||
Preferences.get().registerUpdateHistoryListener(new Preferences.ChangeListener() {
|
||||
@Override
|
||||
public void onPreferenceChange() {
|
||||
a.notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
adapter = a;
|
||||
}
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
AppListView view = new AppListView(getActivity());
|
||||
LinearLayout view = new LinearLayout(getActivity());
|
||||
view.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
final List<String> categories = AppProvider.Helper.categories(getActivity());
|
||||
|
||||
Spinner spinner = new Spinner(getActivity());
|
||||
// Giving it an ID lets the default save/restore state
|
||||
// functionality do its stuff.
|
||||
spinner.setId(R.id.categorySpinner);
|
||||
spinner.setAdapter(getAppListManager().getCategoriesAdapter());
|
||||
spinner.setOnItemSelectedListener(this);
|
||||
spinner.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, categories));
|
||||
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
|
||||
currentCategory = categories.get(pos);
|
||||
Log.d("FDroid", "Category '" + currentCategory + "' selected.");
|
||||
getLoaderManager().restartLoader(0, null, AvailableAppsFragment.this);
|
||||
}
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
currentCategory = null;
|
||||
Log.d("FDroid", "Select empty category.");
|
||||
getLoaderManager().restartLoader(0, null, AvailableAppsFragment.this);
|
||||
}
|
||||
});
|
||||
spinner.setPadding( 0, 0, 0, 0 );
|
||||
|
||||
view.addView(
|
||||
spinner,
|
||||
@ -30,8 +76,10 @@ public class AvailableAppsFragment extends AppListFragment implements AdapterVie
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
ListView list = createAppListView();
|
||||
view.setAppList(list);
|
||||
ListView list = new ListView(getActivity());
|
||||
list.setId(android.R.id.list);
|
||||
list.setFastScrollEnabled(true);
|
||||
list.setOnItemClickListener(this);
|
||||
view.addView(
|
||||
list,
|
||||
new ViewGroup.LayoutParams(
|
||||
@ -42,24 +90,14 @@ public class AvailableAppsFragment extends AppListFragment implements AdapterVie
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int pos,
|
||||
long id) {
|
||||
String category = parent.getItemAtPosition(pos).toString();
|
||||
getAppListManager().setCurrentCategory(category);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AppListAdapter getAppListAdapter() {
|
||||
return getAppListManager().getAvailableAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getFromTitle() {
|
||||
return getAppListManager().getCurrentCategory();
|
||||
protected Uri getDataUri() {
|
||||
if (currentCategory == null || currentCategory.equals(AppProvider.Helper.getCategoryAll(getActivity())))
|
||||
return AppProvider.getContentUri();
|
||||
else if (currentCategory.equals(AppProvider.Helper.getCategoryRecentlyUpdated(getActivity())))
|
||||
return AppProvider.getRecentlyUpdatedUri();
|
||||
else if (currentCategory.equals(AppProvider.Helper.getCategoryWhatsNew(getActivity())))
|
||||
return AppProvider.getNewlyAddedUri();
|
||||
else
|
||||
return AppProvider.getCategoryUri(currentCategory);
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,30 @@
|
||||
package org.fdroid.fdroid.views.fragments;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.views.AppListAdapter;
|
||||
import org.fdroid.fdroid.views.CanUpdateAppListAdapter;
|
||||
|
||||
public class CanUpdateAppsFragment extends AppListFragment {
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return createPlainAppList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AppListAdapter getAppListAdapter() {
|
||||
return getAppListManager().getCanUpdateAdapter();
|
||||
return new CanUpdateAppListAdapter(getActivity(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getFromTitle() {
|
||||
return parent.getString(R.string.tab_updates);
|
||||
return getString(R.string.tab_updates);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Uri getDataUri() {
|
||||
return AppProvider.getCanUpdateUri();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,27 +1,30 @@
|
||||
package org.fdroid.fdroid.views.fragments;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.views.AppListAdapter;
|
||||
import org.fdroid.fdroid.views.InstalledAppListAdapter;
|
||||
|
||||
public class InstalledAppsFragment extends AppListFragment {
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return createPlainAppList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AppListAdapter getAppListAdapter() {
|
||||
return getAppListManager().getInstalledAdapter();
|
||||
return new InstalledAppListAdapter(getActivity(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getFromTitle() {
|
||||
return parent.getString(R.string.inst);
|
||||
return getString(R.string.inst);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Uri getDataUri() {
|
||||
return AppProvider.getInstalledUri();
|
||||
}
|
||||
|
||||
}
|
||||
|
10
test/.gitignore
vendored
Normal file
10
test/.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/local.properties
|
||||
/.classpath
|
||||
/bin/
|
||||
/gen/
|
||||
/build/
|
||||
/.gradle/
|
||||
*~
|
||||
/.idea/
|
||||
/*.iml
|
||||
out
|
21
test/AndroidManifest.xml
Normal file
21
test/AndroidManifest.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- package name must be unique so suffix with "tests" so package loader doesn't ignore us -->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.fdroid.fdroid.tests"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
<!-- We add an application tag here just so that we can indicate that
|
||||
this package needs to link against the android.test library,
|
||||
which is needed when building test cases. -->
|
||||
<application>
|
||||
<uses-library android:name="android.test.runner"/>
|
||||
</application>
|
||||
<!--
|
||||
This declares that this application uses the instrumentation test runner targeting
|
||||
the package of org.fdroid.fdroid. To run the tests use the command:
|
||||
"adb shell am instrument -w org.fdroid.fdroid.tests/android.test.InstrumentationTestRunner"
|
||||
-->
|
||||
<instrumentation android:name="android.test.InstrumentationTestRunner"
|
||||
android:targetPackage="org.fdroid.fdroid"
|
||||
android:label="Tests for org.fdroid.fdroid" />
|
||||
</manifest>
|
18
test/ant.properties
Normal file
18
test/ant.properties
Normal file
@ -0,0 +1,18 @@
|
||||
# This file is used to override default values used by the Ant build system.
|
||||
#
|
||||
# This file must be checked into Version Control Systems, as it is
|
||||
# integral to the build system of your project.
|
||||
|
||||
# This file is only used by the Ant script.
|
||||
|
||||
# You can use this to override default values such as
|
||||
# 'source.dir' for the location of your java source folder and
|
||||
# 'out.dir' for the location of your output folder.
|
||||
|
||||
# You can also use it define how the release builds are signed by declaring
|
||||
# the following properties:
|
||||
# 'key.store' for the location of your keystore and
|
||||
# 'key.alias' for the name of the key to use.
|
||||
# The password will be asked during the build when you use the 'release' target.
|
||||
|
||||
tested.project.dir=/home/pete/code/fdroid/client
|
92
test/build.xml
Normal file
92
test/build.xml
Normal file
@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project name="test" default="help">
|
||||
|
||||
<!-- The local.properties file is created and updated by the 'android' tool.
|
||||
It contains the path to the SDK. It should *NOT* be checked into
|
||||
Version Control Systems. -->
|
||||
<property file="local.properties"/>
|
||||
|
||||
<!-- The ant.properties file can be created by you. It is only edited by the
|
||||
'android' tool to add properties to it.
|
||||
This is the place to change some Ant specific build properties.
|
||||
Here are some properties you may want to change/update:
|
||||
|
||||
source.dir
|
||||
The name of the source directory. Default is 'src'.
|
||||
out.dir
|
||||
The name of the output directory. Default is 'bin'.
|
||||
|
||||
For other overridable properties, look at the beginning of the rules
|
||||
files in the SDK, at tools/ant/build.xml
|
||||
|
||||
Properties related to the SDK location or the project target should
|
||||
be updated using the 'android' tool with the 'update' action.
|
||||
|
||||
This file is an integral part of the build system for your
|
||||
application and should be checked into Version Control Systems.
|
||||
|
||||
-->
|
||||
<property file="ant.properties"/>
|
||||
|
||||
<!-- if sdk.dir was not set from one of the property file, then
|
||||
get it from the ANDROID_HOME env var.
|
||||
This must be done before we load project.properties since
|
||||
the proguard config can use sdk.dir -->
|
||||
<property environment="env"/>
|
||||
<condition property="sdk.dir" value="${env.ANDROID_HOME}">
|
||||
<isset property="env.ANDROID_HOME"/>
|
||||
</condition>
|
||||
|
||||
<!-- The project.properties file is created and updated by the 'android'
|
||||
tool, as well as ADT.
|
||||
|
||||
This contains project specific properties such as project target, and library
|
||||
dependencies. Lower level build properties are stored in ant.properties
|
||||
(or in .classpath for Eclipse projects).
|
||||
|
||||
This file is an integral part of the build system for your
|
||||
application and should be checked into Version Control Systems. -->
|
||||
<loadproperties srcFile="project.properties"/>
|
||||
|
||||
<!-- quick check on sdk.dir -->
|
||||
<fail
|
||||
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
|
||||
unless="sdk.dir"
|
||||
/>
|
||||
|
||||
<!--
|
||||
Import per project custom build rules if present at the root of the project.
|
||||
This is the place to put custom intermediary targets such as:
|
||||
-pre-build
|
||||
-pre-compile
|
||||
-post-compile (This is typically used for code obfuscation.
|
||||
Compiled code location: ${out.classes.absolute.dir}
|
||||
If this is not done in place, override ${out.dex.input.absolute.dir})
|
||||
-post-package
|
||||
-post-build
|
||||
-pre-clean
|
||||
-->
|
||||
<import file="custom_rules.xml" optional="true"/>
|
||||
|
||||
<!-- Import the actual build file.
|
||||
|
||||
To customize existing targets, there are two options:
|
||||
- Customize only one target:
|
||||
- copy/paste the target into this file, *before* the
|
||||
<import> task.
|
||||
- customize it to your needs.
|
||||
- Customize the whole content of build.xml
|
||||
- copy/paste the content of the rules files (minus the top node)
|
||||
into this file, replacing the <import> task.
|
||||
- customize to your needs.
|
||||
|
||||
***********************
|
||||
****** IMPORTANT ******
|
||||
***********************
|
||||
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
|
||||
in order to avoid having your file be overridden by tools such as "android update project"
|
||||
-->
|
||||
<!-- version-tag: 1 -->
|
||||
<import file="${sdk.dir}/tools/ant/build.xml"/>
|
||||
|
||||
</project>
|
20
test/proguard-project.txt
Normal file
20
test/proguard-project.txt
Normal file
@ -0,0 +1,20 @@
|
||||
# To enable ProGuard in your project, edit project.properties
|
||||
# to define the proguard.config property as described in that file.
|
||||
#
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in ${sdk.dir}/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the ProGuard
|
||||
# include property in project.properties.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
@ -11,4 +11,4 @@
|
||||
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
|
||||
|
||||
# Project target.
|
||||
target=android-4
|
||||
target=android-19
|
228
test/src/android/test/ProviderTestCase2MockContext.java
Normal file
228
test/src/android/test/ProviderTestCase2MockContext.java
Normal file
@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright (C) 2007 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.test;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.test.mock.MockContext;
|
||||
import android.test.mock.MockContentResolver;
|
||||
import android.database.DatabaseUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* This test case class provides a framework for testing a single
|
||||
* {@link ContentProvider} and for testing your app code with an
|
||||
* isolated content provider. Instead of using the system map of
|
||||
* providers that is based on the manifests of other applications, the test
|
||||
* case creates its own internal map. It then uses this map to resolve providers
|
||||
* given an authority. This allows you to inject test providers and to null out
|
||||
* providers that you do not want to use.
|
||||
* <p>
|
||||
* This test case also sets up the following mock objects:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>
|
||||
* An {@link android.test.IsolatedContext} that stubs out Context methods that might
|
||||
* affect the rest of the running system, while allowing tests to do real file and
|
||||
* database work.
|
||||
* </li>
|
||||
* <li>
|
||||
* A {@link android.test.mock.MockContentResolver} that provides the functionality of a
|
||||
* regular content resolver, but uses {@link IsolatedContext}. It stubs out
|
||||
* {@link ContentResolver#notifyChange(Uri, ContentObserver, boolean)} to
|
||||
* prevent the test from affecting the running system.
|
||||
* </li>
|
||||
* <li>
|
||||
* An instance of the provider under test, running in an {@link IsolatedContext}.
|
||||
* </li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* This framework is set up automatically by the base class' {@link #setUp()} method. If you
|
||||
* override this method, you must call the super method as the first statement in
|
||||
* your override.
|
||||
* </p>
|
||||
* <p>
|
||||
* In order for their tests to be run, concrete subclasses must provide their own
|
||||
* constructor with no arguments. This constructor must call
|
||||
* {@link #ProviderTestCase2MockContext(Class, String)} as its first operation.
|
||||
* </p>
|
||||
* For more information on content provider testing, please see
|
||||
* <a href="{@docRoot}tools/testing/contentprovider_testing.html">Content Provider Testing</a>.
|
||||
*/
|
||||
public abstract class ProviderTestCase2MockContext<T extends ContentProvider> extends AndroidTestCase {
|
||||
|
||||
Class<T> mProviderClass;
|
||||
String mProviderAuthority;
|
||||
|
||||
private IsolatedContext mProviderContext;
|
||||
private MockContentResolver mResolver;
|
||||
|
||||
private class MockContext2 extends MockContext {
|
||||
|
||||
@Override
|
||||
public Resources getResources() {
|
||||
return getContext().getResources();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getDir(String name, int mode) {
|
||||
// name the directory so the directory will be separated from
|
||||
// one created through the regular Context
|
||||
return getContext().getDir("mockcontext2_" + name, mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context getApplicationContext() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param providerClass The class name of the provider under test
|
||||
* @param providerAuthority The provider's authority string
|
||||
*/
|
||||
public ProviderTestCase2MockContext(Class<T> providerClass, String providerAuthority) {
|
||||
mProviderClass = providerClass;
|
||||
mProviderAuthority = providerAuthority;
|
||||
}
|
||||
|
||||
private T mProvider;
|
||||
|
||||
/**
|
||||
* Returns the content provider created by this class in the {@link #setUp()} method.
|
||||
* @return T An instance of the provider class given as a parameter to the test case class.
|
||||
*/
|
||||
public T getProvider() {
|
||||
return mProvider;
|
||||
}
|
||||
|
||||
abstract protected Context createMockContext(Context delegate);
|
||||
|
||||
/**
|
||||
* Sets up the environment for the test fixture.
|
||||
* <p>
|
||||
* Creates a new
|
||||
* {@link android.test.mock.MockContentResolver}, a new IsolatedContext
|
||||
* that isolates the provider's file operations, and a new instance of
|
||||
* the provider under test within the isolated environment.
|
||||
* </p>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
mResolver = new MockContentResolver();
|
||||
final String filenamePrefix = "test.";
|
||||
RenamingDelegatingContext targetContextWrapper = new
|
||||
RenamingDelegatingContext(
|
||||
createMockContext(new MockContext2()), // The context that most methods are delegated to
|
||||
getContext(), // The context that file methods are delegated to
|
||||
filenamePrefix);
|
||||
mProviderContext = new IsolatedContext(mResolver, targetContextWrapper);
|
||||
|
||||
mProvider = mProviderClass.newInstance();
|
||||
mProvider.attachInfo(mProviderContext, null);
|
||||
assertNotNull(mProvider);
|
||||
mResolver.addProvider(mProviderAuthority, getProvider());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tears down the environment for the test fixture.
|
||||
* <p>
|
||||
* Calls {@link android.content.ContentProvider#shutdown()} on the
|
||||
* {@link android.content.ContentProvider} represented by mProvider.
|
||||
*/
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
mProvider.shutdown();
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link MockContentResolver} created by this class during initialization. You
|
||||
* must use the methods of this resolver to access the provider under test.
|
||||
*
|
||||
* @return A {@link MockContentResolver} instance.
|
||||
*/
|
||||
public MockContentResolver getMockContentResolver() {
|
||||
return mResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link IsolatedContext} created by this class during initialization.
|
||||
* @return The {@link IsolatedContext} instance
|
||||
*/
|
||||
public IsolatedContext getMockContext() {
|
||||
return mProviderContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Creates a new content provider of the same type as that passed to the test case class,
|
||||
* with an authority name set to the authority parameter, and using an SQLite database as
|
||||
* the underlying data source. The SQL statement parameter is used to create the database.
|
||||
* This method also creates a new {@link MockContentResolver} and adds the provider to it.
|
||||
* </p>
|
||||
* <p>
|
||||
* Both the new provider and the new resolver are put into an {@link IsolatedContext}
|
||||
* that uses the targetContext parameter for file operations and a {@link MockContext}
|
||||
* for everything else. The IsolatedContext prepends the filenamePrefix parameter to
|
||||
* file, database, and directory names.
|
||||
* </p>
|
||||
* <p>
|
||||
* This is a convenience method for creating a "mock" provider that can contain test data.
|
||||
* </p>
|
||||
*
|
||||
* @param targetContext The context to use as the basis of the IsolatedContext
|
||||
* @param filenamePrefix A string that is prepended to file, database, and directory names
|
||||
* @param providerClass The type of the provider being tested
|
||||
* @param authority The authority string to associated with the test provider
|
||||
* @param databaseName The name assigned to the database
|
||||
* @param databaseVersion The version assigned to the database
|
||||
* @param sql A string containing the SQL statements that are needed to create the desired
|
||||
* database and its tables. The format is the same as that generated by the
|
||||
* <a href="http://www.sqlite.org/sqlite.html">sqlite3</a> tool's <code>.dump</code> command.
|
||||
* @return ContentResolver A new {@link MockContentResolver} linked to the provider
|
||||
*
|
||||
* @throws IllegalAccessException
|
||||
* @throws InstantiationException
|
||||
*/
|
||||
public static <T extends ContentProvider> ContentResolver newResolverWithContentProviderFromSql(
|
||||
Context targetContext, String filenamePrefix, Class<T> providerClass, String authority,
|
||||
String databaseName, int databaseVersion, String sql)
|
||||
throws IllegalAccessException, InstantiationException {
|
||||
MockContentResolver resolver = new MockContentResolver();
|
||||
RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext(
|
||||
new MockContext(), // The context that most methods are delegated to
|
||||
targetContext, // The context that file methods are delegated to
|
||||
filenamePrefix);
|
||||
Context context = new IsolatedContext(resolver, targetContextWrapper);
|
||||
DatabaseUtils.createDbFromSqlStatements(context, databaseName, databaseVersion, sql);
|
||||
|
||||
T provider = providerClass.newInstance();
|
||||
provider.attachInfo(context, null);
|
||||
resolver.addProvider(authority, provider);
|
||||
|
||||
return resolver;
|
||||
}
|
||||
}
|
14
test/src/mock/MockContextEmptyComponents.java
Normal file
14
test/src/mock/MockContextEmptyComponents.java
Normal file
@ -0,0 +1,14 @@
|
||||
package mock;
|
||||
|
||||
/**
|
||||
* As more components are required to test different parts of F-Droid, we can
|
||||
* create them and add them here (and accessors to the parent class).
|
||||
*/
|
||||
public class MockContextEmptyComponents extends MockContextSwappableComponents {
|
||||
|
||||
public MockContextEmptyComponents() {
|
||||
setPackageManager(new MockEmptyPackageManager());
|
||||
setResources(new MockEmptyResources());
|
||||
}
|
||||
|
||||
}
|
32
test/src/mock/MockContextSwappableComponents.java
Normal file
32
test/src/mock/MockContextSwappableComponents.java
Normal file
@ -0,0 +1,32 @@
|
||||
package mock;
|
||||
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Resources;
|
||||
import android.test.mock.MockContext;
|
||||
|
||||
public class MockContextSwappableComponents extends MockContext {
|
||||
|
||||
private PackageManager packageManager;
|
||||
|
||||
private Resources resources;
|
||||
|
||||
public MockContextSwappableComponents setPackageManager(PackageManager pm) {
|
||||
packageManager = pm;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MockContextSwappableComponents setResources(Resources resources) {
|
||||
this.resources = resources;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PackageManager getPackageManager() {
|
||||
return packageManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resources getResources() {
|
||||
return resources;
|
||||
}
|
||||
}
|
16
test/src/mock/MockEmptyPackageManager.java
Normal file
16
test/src/mock/MockEmptyPackageManager.java
Normal file
@ -0,0 +1,16 @@
|
||||
package mock;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.test.mock.MockPackageManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MockEmptyPackageManager extends MockPackageManager {
|
||||
|
||||
@Override
|
||||
public List<PackageInfo> getInstalledPackages(int flags) {
|
||||
return new ArrayList<PackageInfo>();
|
||||
}
|
||||
|
||||
}
|
12
test/src/mock/MockEmptyResources.java
Normal file
12
test/src/mock/MockEmptyResources.java
Normal file
@ -0,0 +1,12 @@
|
||||
package mock;
|
||||
|
||||
import android.test.mock.MockResources;
|
||||
|
||||
public class MockEmptyResources extends MockResources {
|
||||
|
||||
@Override
|
||||
public String getString(int id) {
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
26
test/src/mock/MockInstallablePackageManager.java
Normal file
26
test/src/mock/MockInstallablePackageManager.java
Normal file
@ -0,0 +1,26 @@
|
||||
package mock;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.test.mock.MockPackageManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MockInstallablePackageManager extends MockPackageManager {
|
||||
|
||||
private List<PackageInfo> info = new ArrayList<PackageInfo>();
|
||||
|
||||
@Override
|
||||
public List<PackageInfo> getInstalledPackages(int flags) {
|
||||
return info;
|
||||
}
|
||||
|
||||
public void install(String id, int version, String versionName) {
|
||||
PackageInfo p = new PackageInfo();
|
||||
p.packageName = id;
|
||||
p.versionCode = version;
|
||||
p.versionName = versionName;
|
||||
info.add(p);
|
||||
}
|
||||
|
||||
}
|
138
test/src/org/fdroid/fdroid/AppProviderTest.java
Normal file
138
test/src/org/fdroid/fdroid/AppProviderTest.java
Normal file
@ -0,0 +1,138 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract;
|
||||
import mock.MockInstallablePackageManager;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class AppProviderTest extends FDroidProviderTest<AppProvider> {
|
||||
|
||||
public AppProviderTest() {
|
||||
super(AppProvider.class, AppProvider.getAuthority());
|
||||
}
|
||||
|
||||
protected String[] getMinimalProjection() {
|
||||
return new String[] {
|
||||
AppProvider.DataColumns.APP_ID,
|
||||
AppProvider.DataColumns.NAME
|
||||
};
|
||||
}
|
||||
|
||||
public void testUris() {
|
||||
assertInvalidUri(AppProvider.getAuthority());
|
||||
assertInvalidUri(ApkProvider.getContentUri());
|
||||
|
||||
assertValidUri(AppProvider.getContentUri());
|
||||
assertValidUri(AppProvider.getSearchUri("'searching!'"));
|
||||
assertValidUri(AppProvider.getNoApksUri());
|
||||
assertValidUri(AppProvider.getInstalledUri());
|
||||
assertValidUri(AppProvider.getCanUpdateUri());
|
||||
|
||||
App app = new App();
|
||||
app.id = "org.fdroid.fdroid";
|
||||
|
||||
List<App> apps = new ArrayList<App>(1);
|
||||
apps.add(app);
|
||||
|
||||
assertValidUri(AppProvider.getContentUri(app));
|
||||
assertValidUri(AppProvider.getContentUri(apps));
|
||||
assertValidUri(AppProvider.getContentUri("org.fdroid.fdroid"));
|
||||
}
|
||||
|
||||
public void testQuery() {
|
||||
Cursor cursor = queryAllApps();
|
||||
assertNotNull(cursor);
|
||||
}
|
||||
|
||||
public void testInstalled() {
|
||||
|
||||
Utils.clearInstalledApksCache();
|
||||
|
||||
MockInstallablePackageManager pm = new MockInstallablePackageManager();
|
||||
getSwappableContext().setPackageManager(pm);
|
||||
|
||||
for (int i = 0; i < 100; i ++) {
|
||||
insertApp("com.example.test." + i, "Test app " + i);
|
||||
}
|
||||
|
||||
assertAppCount(100, AppProvider.getContentUri());
|
||||
assertAppCount(0, AppProvider.getInstalledUri());
|
||||
|
||||
for (int i = 10; i < 20; i ++) {
|
||||
pm.install("com.example.test." + i, i, "v1");
|
||||
}
|
||||
|
||||
assertAppCount(10, AppProvider.getInstalledUri());
|
||||
}
|
||||
|
||||
private void assertAppCount(int expectedCount, Uri uri) {
|
||||
Cursor cursor = getProvider().query(uri, getMinimalProjection(), null, null, null);
|
||||
assertNotNull(cursor);
|
||||
assertEquals(expectedCount, cursor.getCount());
|
||||
}
|
||||
|
||||
public void testInsert() {
|
||||
|
||||
// Start with an empty database...
|
||||
Cursor cursor = queryAllApps();
|
||||
assertNotNull(cursor);
|
||||
assertEquals(0, cursor.getCount());
|
||||
|
||||
// Insert a new record...
|
||||
insertApp("org.fdroid.fdroid", "F-Droid");
|
||||
cursor = queryAllApps();
|
||||
assertNotNull(cursor);
|
||||
assertEquals(1, cursor.getCount());
|
||||
|
||||
// We intentionally throw an IllegalArgumentException if you haven't
|
||||
// yet called cursor.move*()...
|
||||
try {
|
||||
new App(cursor);
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Success!
|
||||
} catch (Exception e) {
|
||||
fail();
|
||||
}
|
||||
|
||||
// And now we should be able to recover these values from the app
|
||||
// value object (because the queryAllApps() helper asks for NAME and
|
||||
// APP_ID.
|
||||
cursor.moveToFirst();
|
||||
App app = new App(cursor);
|
||||
assertEquals("org.fdroid.fdroid", app.id);
|
||||
assertEquals("F-Droid", app.name);
|
||||
}
|
||||
|
||||
private Cursor queryAllApps() {
|
||||
return getProvider().query(AppProvider.getContentUri(), getMinimalProjection(), null, null, null);
|
||||
}
|
||||
|
||||
private void insertApp(String id, String name) {
|
||||
ContentValues values = new ContentValues(2);
|
||||
values.put(AppProvider.DataColumns.APP_ID, id);
|
||||
values.put(AppProvider.DataColumns.NAME, name);
|
||||
|
||||
// Required fields (NOT NULL in the database).
|
||||
values.put(AppProvider.DataColumns.SUMMARY, "test summary");
|
||||
values.put(AppProvider.DataColumns.DESCRIPTION, "test description");
|
||||
values.put(AppProvider.DataColumns.LICENSE, "GPL?");
|
||||
values.put(AppProvider.DataColumns.IS_COMPATIBLE, 1);
|
||||
values.put(AppProvider.DataColumns.IGNORE_ALLUPDATES, 0);
|
||||
values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, 0);
|
||||
|
||||
Uri uri = AppProvider.getContentUri();
|
||||
|
||||
getProvider().insert(uri, values);
|
||||
}
|
||||
|
||||
}
|
73
test/src/org/fdroid/fdroid/FDroidProviderTest.java
Normal file
73
test/src/org/fdroid/fdroid/FDroidProviderTest.java
Normal file
@ -0,0 +1,73 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.ContactsContract;
|
||||
import android.test.ProviderTestCase2MockContext;
|
||||
import mock.MockContextEmptyComponents;
|
||||
import mock.MockContextSwappableComponents;
|
||||
import org.fdroid.fdroid.data.FDroidProvider;
|
||||
import org.fdroid.fdroid.mock.MockInstalledApkCache;
|
||||
|
||||
public abstract class FDroidProviderTest<T extends FDroidProvider> extends ProviderTestCase2MockContext<T> {
|
||||
|
||||
private MockContextSwappableComponents swappableContext;
|
||||
|
||||
public FDroidProviderTest(Class<T> providerClass, String providerAuthority) {
|
||||
super(providerClass, providerAuthority);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
Utils.setupInstalledApkCache(new MockInstalledApkCache());
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.ECLAIR)
|
||||
public void testObviouslyInvalidUris() {
|
||||
assertInvalidUri("http://www.google.com");
|
||||
assertInvalidUri(ContactsContract.AUTHORITY_URI);
|
||||
assertInvalidUri("junk");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Context createMockContext(Context delegate) {
|
||||
swappableContext = new MockContextEmptyComponents();
|
||||
return swappableContext;
|
||||
}
|
||||
|
||||
public MockContextSwappableComponents getSwappableContext() {
|
||||
return swappableContext;
|
||||
}
|
||||
|
||||
protected void assertInvalidUri(String uri) {
|
||||
assertInvalidUri(Uri.parse(uri));
|
||||
}
|
||||
|
||||
protected void assertValidUri(String uri) {
|
||||
assertValidUri(Uri.parse(uri));
|
||||
}
|
||||
|
||||
protected void assertInvalidUri(Uri uri) {
|
||||
try {
|
||||
getProvider().query(uri, getMinimalProjection(), null, null, null);
|
||||
fail();
|
||||
} catch (UnsupportedOperationException e) {}
|
||||
}
|
||||
|
||||
protected void assertValidUri(Uri uri) {
|
||||
Cursor cursor = getProvider().query(uri, getMinimalProjection(), null, null, null);
|
||||
assertNotNull(cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Many queries need at least some sort of projection in order to produce
|
||||
* valid SQL. As such, we also need to know about that, so we can provide
|
||||
* helper functions that revolve around the contnet provider under test.
|
||||
*/
|
||||
protected abstract String[] getMinimalProjection();
|
||||
|
||||
}
|
14
test/src/org/fdroid/fdroid/FDroidTest.java
Normal file
14
test/src/org/fdroid/fdroid/FDroidTest.java
Normal file
@ -0,0 +1,14 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.test.ActivityInstrumentationTestCase2;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.CUPCAKE)
|
||||
public class FDroidTest extends ActivityInstrumentationTestCase2<FDroid> {
|
||||
|
||||
public FDroidTest() {
|
||||
super("org.fdroid.fdroid", FDroid.class);
|
||||
}
|
||||
|
||||
}
|
16
test/src/org/fdroid/fdroid/mock/MockInstalledApkCache.java
Normal file
16
test/src/org/fdroid/fdroid/mock/MockInstalledApkCache.java
Normal file
@ -0,0 +1,16 @@
|
||||
package org.fdroid.fdroid.mock;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class MockInstalledApkCache extends Utils.InstalledApkCache {
|
||||
|
||||
@Override
|
||||
public Map<String, PackageInfo> getApks(Context context) {
|
||||
return buildAppList(context);
|
||||
}
|
||||
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
/*___Generated_by_IDEA___*/
|
||||
|
||||
/** Automatically generated file. DO NOT MODIFY */
|
||||
package org.fdroid.fdroid.tests;
|
||||
|
||||
public final class BuildConfig {
|
||||
public final static boolean DEBUG = true;
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
/*___Generated_by_IDEA___*/
|
||||
|
||||
package org.fdroid.fdroid.tests;
|
||||
|
||||
/* This stub is for using by IDE only. It is NOT the Manifest class actually packed into APK */
|
||||
public final class Manifest {
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
/*___Generated_by_IDEA___*/
|
||||
|
||||
package org.fdroid.fdroid.tests;
|
||||
|
||||
/* This stub is for using by IDE only. It is NOT the R class actually packed into APK */
|
||||
public final class R {
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
# This file is automatically generated by Android Tools.
|
||||
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
|
||||
#
|
||||
# This file must *NOT* be checked into Version Control Systems,
|
||||
# as it contains information specific to your local configuration.
|
||||
|
||||
# location of the SDK. This is only used by Ant
|
||||
# For customization when using a Version Control System, please read the
|
||||
# header note.
|
||||
sdk.dir=/opt/android-sdk
|
Loading…
x
Reference in New Issue
Block a user