Merge commit 'refs/merge-requests/60' of gitorious.org:f-droid/fdroidclient
This commit is contained in:
commit
aaae885161
@ -36,10 +36,20 @@
|
||||
android:supportsRtl="false" >
|
||||
|
||||
<provider
|
||||
android:authorities="org.fdroid.fdroid.data"
|
||||
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"
|
||||
android:exported="false"/>
|
||||
|
||||
<provider
|
||||
android:authorities="org.fdroid.fdroid.data.ApkProvider"
|
||||
android:name="org.fdroid.fdroid.data.ApkProvider"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".FDroid"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize" >
|
||||
|
@ -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,12 +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.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;
|
||||
@ -37,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;
|
||||
@ -63,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;
|
||||
@ -72,77 +62,50 @@ 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.display.FadeInBitmapDisplayer;
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
|
||||
import com.nostra13.universalimageloader.utils.StorageUtils;
|
||||
|
||||
import android.os.Environment;
|
||||
|
||||
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<DB.Apk> items;
|
||||
private LayoutInflater mInflater;
|
||||
|
||||
public ApkListAdapter(Context context, List<DB.Apk> items) {
|
||||
this.items = new ArrayList<DB.Apk>();
|
||||
if (items != null) {
|
||||
for (DB.Apk apk : items) {
|
||||
this.addItem(apk);
|
||||
}
|
||||
}
|
||||
mInflater = (LayoutInflater) mctx.getSystemService(
|
||||
private LayoutInflater mInflater = (LayoutInflater) mctx.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE);
|
||||
}
|
||||
|
||||
public void addItem(DB.Apk 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) {
|
||||
items.add(apk);
|
||||
add(apk);
|
||||
}
|
||||
}
|
||||
|
||||
public List<DB.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);
|
||||
DB.Apk apk = items.get(position);
|
||||
Apk apk = getItem(position);
|
||||
ViewHolder holder;
|
||||
|
||||
if (convertView == null) {
|
||||
@ -153,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);
|
||||
@ -164,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));
|
||||
@ -174,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);
|
||||
@ -211,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...
|
||||
@ -222,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;
|
||||
@ -251,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;
|
||||
@ -339,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();
|
||||
|
||||
@ -381,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;
|
||||
@ -426,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);
|
||||
@ -443,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,
|
||||
@ -548,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);
|
||||
@ -560,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 {
|
||||
@ -589,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);
|
||||
@ -634,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) {
|
||||
@ -656,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),
|
||||
@ -667,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),
|
||||
@ -680,7 +651,7 @@ public class AppDetails extends ListActivity {
|
||||
AlertDialog alert = ask_alrt.create();
|
||||
alert.show();
|
||||
} else
|
||||
install();
|
||||
install(apk);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -690,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),
|
||||
@ -730,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);
|
||||
}
|
||||
|
||||
@ -801,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:
|
||||
@ -815,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;
|
||||
|
||||
}
|
||||
@ -859,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),
|
||||
@ -877,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()));
|
||||
}
|
||||
@ -893,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),
|
||||
@ -908,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()));
|
||||
}
|
||||
|
||||
@ -940,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");
|
||||
|
||||
@ -983,7 +958,7 @@ public class AppDetails extends ListActivity {
|
||||
private boolean updating;
|
||||
private String id;
|
||||
|
||||
public DownloadHandler(DB.Apk apk, String repoaddress, File destdir) {
|
||||
public DownloadHandler(Apk apk, String repoaddress, File destdir) {
|
||||
id = apk.id;
|
||||
download = new Downloader(apk, repoaddress, destdir);
|
||||
download.start();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -27,10 +27,11 @@ import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
|
||||
public class Downloader extends Thread {
|
||||
|
||||
private DB.Apk curapk;
|
||||
private Apk curapk;
|
||||
private String repoaddress;
|
||||
private String filename;
|
||||
private File destdir;
|
||||
@ -38,11 +39,11 @@ public class Downloader extends Thread {
|
||||
|
||||
public static enum Status {
|
||||
STARTING, RUNNING, ERROR, DONE, CANCELLED
|
||||
};
|
||||
}
|
||||
|
||||
public static enum Error {
|
||||
CORRUPT, UNKNOWN
|
||||
};
|
||||
}
|
||||
|
||||
private Status status = Status.STARTING;
|
||||
private Error error;
|
||||
@ -52,7 +53,7 @@ public class Downloader extends Thread {
|
||||
|
||||
// Constructor - creates a Downloader to download the given Apk,
|
||||
// which must have its detail populated.
|
||||
Downloader(DB.Apk apk, String repoaddress, File destdir) {
|
||||
Downloader(Apk apk, String repoaddress, File destdir) {
|
||||
curapk = apk;
|
||||
this.repoaddress = repoaddress;
|
||||
this.destdir = destdir;
|
||||
@ -91,7 +92,7 @@ public class Downloader extends Thread {
|
||||
}
|
||||
|
||||
// The APK being downloaded
|
||||
public synchronized DB.Apk getApk() {
|
||||
public synchronized Apk getApk() {
|
||||
return curapk;
|
||||
}
|
||||
|
||||
@ -107,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;
|
||||
@ -129,7 +130,7 @@ public class Downloader extends Thread {
|
||||
synchronized (this) {
|
||||
filename = remotefile;
|
||||
progress = 0;
|
||||
max = curapk.detail_size;
|
||||
max = curapk.size;
|
||||
status = Status.RUNNING;
|
||||
}
|
||||
|
||||
@ -158,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,7 +127,27 @@ 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;
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,8 @@
|
||||
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;
|
||||
@ -27,20 +29,18 @@ 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 DB.Apk curapk = null;
|
||||
private App curapp = null;
|
||||
private Apk curapk = null;
|
||||
private StringBuilder curchars = new StringBuilder();
|
||||
|
||||
// After processing the XML, these will be -1 if the index didn't specify
|
||||
@ -63,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; }
|
||||
@ -103,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")) {
|
||||
@ -130,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;
|
||||
@ -158,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")) {
|
||||
@ -179,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;
|
||||
@ -213,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;
|
||||
}
|
||||
@ -226,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;
|
||||
@ -269,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 ++;
|
||||
@ -280,7 +281,7 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
totalAppCount, progressData));
|
||||
|
||||
} else if (localName.equals("package") && curapp != null && curapk == null) {
|
||||
curapk = new DB.Apk();
|
||||
curapk = new Apk();
|
||||
curapk.id = curapp.id;
|
||||
curapk.repo = repo.getId();
|
||||
hashType = null;
|
||||
|
@ -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,41 +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.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";
|
||||
@ -202,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) {
|
||||
|
||||
@ -255,17 +230,110 @@ 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 {
|
||||
@ -276,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());
|
||||
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 {
|
||||
apk.compatible = true;
|
||||
apk.incompatible_reasons = null;
|
||||
apps.get(apk.id).compatible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process each repo...
|
||||
List<DB.App> updatingApps = new ArrayList<DB.App>();
|
||||
Set<Long> keeprepos = new TreeSet<Long>();
|
||||
boolean changes = false;
|
||||
boolean update;
|
||||
/**
|
||||
* 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.inuse)
|
||||
continue;
|
||||
// are we updating all repos, or just one?
|
||||
if (TextUtils.isEmpty(address)) {
|
||||
update = true;
|
||||
if (repo.getId() != apk.repo) continue;
|
||||
if (repo.version >= Repo.VERSION_DENSITY_SPECIFIC_ICONS) {
|
||||
app.iconUrl = repo.address + iconsDir + app.icon;
|
||||
} else {
|
||||
// if only updating one repo, mark the rest as keepers
|
||||
if (address.equals(repo.address)) {
|
||||
update = true;
|
||||
} else {
|
||||
keeprepos.add(repo.getId());
|
||||
update = false;
|
||||
app.iconUrl = repo.address + "/icons/" + app.icon;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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 (DB.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 (DB.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 {
|
||||
@ -446,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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
138
src/org/fdroid/fdroid/data/Apk.java
Normal file
138
src/org/fdroid/fdroid/data/Apk.java
Normal file
@ -0,0 +1,138 @@
|
||||
package org.fdroid.fdroid.data;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class Apk extends ValueObject implements Comparable<Apk> {
|
||||
|
||||
public String id;
|
||||
public String version;
|
||||
public int vercode;
|
||||
public int size; // Size in bytes - 0 means we don't know!
|
||||
public long repo; // ID of the repo it comes from
|
||||
public String hash;
|
||||
public String hashType;
|
||||
public int minSdkVersion; // 0 if unknown
|
||||
public Date added;
|
||||
public Utils.CommaSeparatedList permissions; // null if empty or
|
||||
// unknown
|
||||
public Utils.CommaSeparatedList features; // 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.
|
||||
public String sig;
|
||||
|
||||
// True if compatible with the device.
|
||||
public boolean compatible;
|
||||
|
||||
public String apkName;
|
||||
|
||||
// If not null, this is the name of the source tarball for the
|
||||
// application. Null indicates that it's a developer's binary
|
||||
// build - otherwise it's built from source.
|
||||
public String srcname;
|
||||
|
||||
// Used internally for tracking during repo updates.
|
||||
public boolean updated;
|
||||
|
||||
public int repoVersion;
|
||||
public String repoAddress;
|
||||
public Utils.CommaSeparatedList incompatible_reasons;
|
||||
|
||||
public Apk() {
|
||||
updated = false;
|
||||
size = 0;
|
||||
added = null;
|
||||
repo = 0;
|
||||
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)) {
|
||||
hash = cursor.getString(i);
|
||||
} else if (column.equals(ApkProvider.DataColumns.HASH_TYPE)) {
|
||||
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 = 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)) {
|
||||
compatible = cursor.getInt(i) == 1;
|
||||
} else if (column.equals(ApkProvider.DataColumns.MIN_SDK_VERSION)) {
|
||||
minSdkVersion = cursor.getInt(i);
|
||||
} else if (column.equals(ApkProvider.DataColumns.NAME)) {
|
||||
apkName = cursor.getString(i);
|
||||
} else if (column.equals(ApkProvider.DataColumns.PERMISSIONS)) {
|
||||
permissions = Utils.CommaSeparatedList.make(cursor.getString(i));
|
||||
} else if (column.equals(ApkProvider.DataColumns.NATIVE_CODE)) {
|
||||
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)) {
|
||||
size = cursor.getInt(i);
|
||||
} else if (column.equals(ApkProvider.DataColumns.SOURCE_NAME)) {
|
||||
srcname = cursor.getString(i);
|
||||
} else if (column.equals(ApkProvider.DataColumns.VERSION)) {
|
||||
version = cursor.getString(i);
|
||||
} else if (column.equals(ApkProvider.DataColumns.VERSION_CODE)) {
|
||||
vercode = cursor.getInt(i);
|
||||
} else if (column.equals(ApkProvider.DataColumns.REPO_VERSION)) {
|
||||
repoVersion = cursor.getInt(i);
|
||||
} else if (column.equals(ApkProvider.DataColumns.REPO_ADDRESS)) {
|
||||
repoAddress = cursor.getString(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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, 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, size);
|
||||
values.put(ApkProvider.DataColumns.NAME, apkName);
|
||||
values.put(ApkProvider.DataColumns.MIN_SDK_VERSION, minSdkVersion);
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Apk apk) {
|
||||
return Integer.valueOf(vercode).compareTo(apk.vercode);
|
||||
}
|
||||
|
||||
}
|
502
src/org/fdroid/fdroid/data/ApkProvider.java
Normal file
502
src/org/fdroid/fdroid/data/ApkProvider.java
Normal file
@ -0,0 +1,502 @@
|
||||
package org.fdroid.fdroid.data;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
import android.util.Log;
|
||||
|
||||
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() {}
|
||||
|
||||
public static void update(Context context, Apk apk,
|
||||
String id, int versionCode) {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
Uri uri = getContentUri(id, versionCode);
|
||||
resolver.update(uri, apk.toContentValues(), null, null);
|
||||
}
|
||||
|
||||
public static void update(Context context, Apk apk) {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
Uri uri = getContentUri(apk.id, apk.vercode);
|
||||
resolver.update(uri, apk.toContentValues(), null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This doesn't do anything other than call "insert" on the content
|
||||
* resolver, but I thought I'd put it here in the interests of having
|
||||
* each of the CRUD methods available in the helper class.
|
||||
*/
|
||||
public static void insert(Context context, ContentValues values) {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
resolver.insert(getContentUri(), values);
|
||||
}
|
||||
|
||||
public static void insert(Context context, Apk apk) {
|
||||
insert(context, apk.toContentValues());
|
||||
}
|
||||
|
||||
public static List<Apk> all(Context context) {
|
||||
return all(context, DataColumns.ALL);
|
||||
}
|
||||
|
||||
public static List<Apk> all(Context context, String[] projection) {
|
||||
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
Uri uri = ApkProvider.getContentUri();
|
||||
Cursor cursor = resolver.query(uri, projection, null, null, null);
|
||||
return cursorToList(cursor);
|
||||
}
|
||||
|
||||
private static List<Apk> cursorToList(Cursor cursor) {
|
||||
List<Apk> apks = new ArrayList<Apk>();
|
||||
if (cursor != null) {
|
||||
cursor.moveToFirst();
|
||||
while (!cursor.isAfterLast()) {
|
||||
apks.add(new Apk(cursor));
|
||||
cursor.moveToNext();
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
return apks;
|
||||
}
|
||||
|
||||
public static void deleteApksByRepo(Context context, Repo repo) {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
Uri uri = getContentUri();
|
||||
String[] args = { Long.toString(repo.getId()) };
|
||||
String selection = DataColumns.REPO_ID + " = ?";
|
||||
int count = resolver.delete(uri, selection, args);
|
||||
}
|
||||
|
||||
public static void deleteApksByApp(Context context, App app) {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
Uri uri = getContentUri();
|
||||
String[] args = { app.id };
|
||||
String selection = DataColumns.APK_ID + " = ?";
|
||||
resolver.delete(uri, selection, args);
|
||||
}
|
||||
|
||||
public static Apk find(Context context, String id, int versionCode) {
|
||||
return find(context, id, versionCode, DataColumns.ALL);
|
||||
}
|
||||
|
||||
public static Apk find(Context context, String id, int versionCode, String[] projection) {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public static void delete(Context context, String id, int versionCode) {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
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 {
|
||||
|
||||
public static String APK_ID = "id";
|
||||
public static String VERSION = "version";
|
||||
public static String REPO_ID = "repo";
|
||||
public static String HASH = "hash";
|
||||
public static String VERSION_CODE = "vercode";
|
||||
public static String NAME = "apkName";
|
||||
public static String SIZE = "size";
|
||||
public static String SIGNATURE = "sig";
|
||||
public static String SOURCE_NAME = "srcname";
|
||||
public static String MIN_SDK_VERSION = "minSdkVersion";
|
||||
public static String PERMISSIONS = "permissions";
|
||||
public static String FEATURES = "features";
|
||||
public static String NATIVE_CODE = "nativecode";
|
||||
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";
|
||||
|
||||
public static String[] ALL = {
|
||||
_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, 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);
|
||||
|
||||
public static Map<String,String> REPO_FIELDS = new HashMap<String,String>();
|
||||
|
||||
static {
|
||||
REPO_FIELDS.put(DataColumns.REPO_VERSION, RepoProvider.DataColumns.VERSION);
|
||||
REPO_FIELDS.put(DataColumns.REPO_ADDRESS, RepoProvider.DataColumns.ADDRESS);
|
||||
|
||||
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://" + 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getProviderName() {
|
||||
return PROVIDER_NAME;
|
||||
}
|
||||
|
||||
protected UriMatcher getMatcher() {
|
||||
return matcher;
|
||||
}
|
||||
|
||||
private static class QueryBuilder {
|
||||
|
||||
private StringBuilder fields = new StringBuilder();
|
||||
private StringBuilder tables = new StringBuilder(DBHelper.TABLE_APK + " AS apk");
|
||||
private String selection = null;
|
||||
private String orderBy = null;
|
||||
|
||||
private boolean repoTableRequired = false;
|
||||
|
||||
public void addField(String field) {
|
||||
if (REPO_FIELDS.containsKey(field)) {
|
||||
addRepoField(REPO_FIELDS.get(field), field);
|
||||
} else if (field.equals(DataColumns._ID)) {
|
||||
appendField("rowid", "apk", "_id");
|
||||
} else if (field.startsWith("COUNT")) {
|
||||
appendField(field);
|
||||
} else {
|
||||
appendField(field, "apk");
|
||||
}
|
||||
}
|
||||
|
||||
public void addRepoField(String field, String alias) {
|
||||
if (!repoTableRequired) {
|
||||
repoTableRequired = true;
|
||||
tables.append(" LEFT JOIN ");
|
||||
tables.append(DBHelper.TABLE_REPO);
|
||||
tables.append(" AS repo ON (apk.repo = repo._id) ");
|
||||
}
|
||||
appendField(field, "repo", alias);
|
||||
}
|
||||
|
||||
private void appendField(String field) {
|
||||
appendField(field, null, null);
|
||||
}
|
||||
|
||||
private void appendField(String field, String tableAlias) {
|
||||
appendField(field, tableAlias, null);
|
||||
}
|
||||
|
||||
private void appendField(String field, String tableAlias,
|
||||
String fieldAlias) {
|
||||
if (fields.length() != 0) {
|
||||
fields.append(',');
|
||||
}
|
||||
|
||||
if (tableAlias != null) {
|
||||
fields.append(tableAlias).append('.');
|
||||
}
|
||||
|
||||
fields.append(field);
|
||||
|
||||
if (fieldAlias != null) {
|
||||
fields.append(" AS ").append(fieldAlias);
|
||||
}
|
||||
}
|
||||
|
||||
public void addSelection(String selection) {
|
||||
this.selection = selection;
|
||||
}
|
||||
|
||||
public void addOrderBy(String orderBy) {
|
||||
this.orderBy = orderBy;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
|
||||
StringBuilder suffix = new StringBuilder();
|
||||
if (selection != null) {
|
||||
suffix.append(" WHERE ").append(selection);
|
||||
}
|
||||
|
||||
if (orderBy != null) {
|
||||
suffix.append(" ORDER BY ").append(orderBy);
|
||||
}
|
||||
|
||||
return "SELECT " + fields + " FROM " + tables + suffix;
|
||||
}
|
||||
}
|
||||
|
||||
private QuerySelection queryApp(String appId) {
|
||||
String selection = " id = ? ";
|
||||
String[] args = new String[] { appId };
|
||||
return new QuerySelection(selection, args);
|
||||
}
|
||||
|
||||
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 = ? ) ");
|
||||
}
|
||||
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:
|
||||
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:
|
||||
Log.e("FDroid", "Invalid URI for apk content provider: " + uri);
|
||||
throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri);
|
||||
}
|
||||
|
||||
QueryBuilder queryBuilder = new QueryBuilder();
|
||||
for (String field : projection) {
|
||||
queryBuilder.addField(field);
|
||||
}
|
||||
queryBuilder.addSelection(query.getSelection());
|
||||
queryBuilder.addOrderBy(sortOrder);
|
||||
|
||||
Cursor cursor = read().rawQuery(queryBuilder.toString(), query.getArgs());
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
private static void removeRepoFields(ContentValues values) {
|
||||
for (Map.Entry<String,String> repoField : REPO_FIELDS.entrySet()) {
|
||||
String field = repoField.getKey();
|
||||
if (values.containsKey(field)) {
|
||||
Log.i("FDroid", "Cannot insert/update '" + field + "' field " +
|
||||
"on apk table, as it belongs to the repo table. " +
|
||||
"This field will be ignored.");
|
||||
values.remove(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
removeRepoFields(values);
|
||||
long id = write().insertOrThrow(getTableName(), null, values);
|
||||
if (!isApplyingBatch()) {
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
}
|
||||
return getContentUri(
|
||||
values.getAsString(DataColumns.APK_ID),
|
||||
values.getAsInteger(DataColumns.VERSION_CODE));
|
||||
|
||||
}
|
||||
|
||||
@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:
|
||||
query = query.add(querySingle(uri));
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e("FDroid", "Invalid URI for apk content provider: " + uri);
|
||||
throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri);
|
||||
}
|
||||
|
||||
int rowsAffected = write().delete(getTableName(), query.getSelection(), query.getArgs());
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return rowsAffected;
|
||||
|
||||
}
|
||||
|
||||
@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:
|
||||
query = query.add(querySingle(uri));
|
||||
break;
|
||||
}
|
||||
|
||||
removeRepoFields(values);
|
||||
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;
|
||||
@ -18,6 +19,12 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
|
||||
public static final String TABLE_REPO = "fdroid_repo";
|
||||
|
||||
// The TABLE_APK table stores details of all the application versions we
|
||||
// know about. Each relates directly back to an entry in TABLE_APP.
|
||||
// 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, "
|
||||
@ -27,33 +34,59 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
+ "version integer not null default 0, "
|
||||
+ "lastetag text, lastUpdated string);";
|
||||
|
||||
private static final String CREATE_TABLE_APK = "create table " + DB.TABLE_APK
|
||||
+ " ( " + "id text not null, " + "version text not null, "
|
||||
+ "repo integer not null, " + "hash text not null, "
|
||||
+ "vercode int not null," + "apkName text not null, "
|
||||
+ "size int not null," + "sig string," + "srcname string,"
|
||||
+ "minSdkVersion integer," + "permissions string,"
|
||||
+ "features string," + "nativecode string,"
|
||||
+ "hashType string," + "added string,"
|
||||
+ "compatible int not null," + "primary key(id,vercode));";
|
||||
private static final String CREATE_TABLE_APK =
|
||||
"CREATE TABLE " + TABLE_APK + " ( "
|
||||
+ "id text not null, "
|
||||
+ "version text not null, "
|
||||
+ "repo integer not null, "
|
||||
+ "hash text not null, "
|
||||
+ "vercode int not null,"
|
||||
+ "apkName text not null, "
|
||||
+ "size int not null, "
|
||||
+ "sig string, "
|
||||
+ "srcname string, "
|
||||
+ "minSdkVersion integer, "
|
||||
+ "permissions string, "
|
||||
+ "features string, "
|
||||
+ "nativecode string, "
|
||||
+ "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;
|
||||
|
||||
@ -64,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();
|
||||
}
|
||||
@ -87,7 +122,6 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
if (oldVersion < 36) {
|
||||
|
||||
Log.d("FDroid", "Renaming " + TABLE_REPO + ".id to _id");
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
@ -150,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);
|
||||
@ -280,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 });
|
||||
}
|
||||
}
|
||||
@ -300,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");
|
||||
}
|
||||
}
|
||||
@ -307,18 +342,18 @@ 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 " + DB.TABLE_APK);
|
||||
db.execSQL("drop table " + TABLE_APP);
|
||||
db.execSQL("drop table " + TABLE_APK);
|
||||
db.execSQL("update " + TABLE_REPO + " set lastetag = NULL");
|
||||
createAppApk(db);
|
||||
}
|
||||
|
||||
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 " + DB.TABLE_APK + " (vercode);");
|
||||
db.execSQL("create index apk_id on " + DB.TABLE_APK + " (id);");
|
||||
db.execSQL("create index apk_vercode on " + TABLE_APK + " (vercode);");
|
||||
db.execSQL("create index apk_id on " + TABLE_APK + " (id);");
|
||||
}
|
||||
|
||||
private static boolean columnExists(SQLiteDatabase db,
|
||||
|
@ -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 {
|
||||
public class Repo extends ValueObject {
|
||||
|
||||
public static final int VERSION_DENSITY_SPECIFIC_ICONS = 11;
|
||||
|
||||
private long id;
|
||||
|
||||
@ -31,6 +33,9 @@ public class Repo {
|
||||
}
|
||||
|
||||
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)) {
|
||||
@ -46,14 +51,7 @@ public class Repo {
|
||||
} else if (column.equals(RepoProvider.DataColumns.IN_USE)) {
|
||||
inuse = cursor.getInt(i) == 1;
|
||||
} else if (column.equals(RepoProvider.DataColumns.LAST_UPDATED)) {
|
||||
String dateString = cursor.getString(i);
|
||||
if (dateString != null) {
|
||||
try {
|
||||
lastUpdated = DB.DATE_FORMAT.parse(dateString);
|
||||
} catch (ParseException e) {
|
||||
Log.e("FDroid", "Error parsing date " + dateString);
|
||||
}
|
||||
}
|
||||
lastUpdated = toDate(cursor.getString(i));
|
||||
} else if (column.equals(RepoProvider.DataColumns.MAX_AGE)) {
|
||||
maxage = cursor.getInt(i);
|
||||
} else if (column.equals(RepoProvider.DataColumns.VERSION)) {
|
||||
@ -78,13 +76,6 @@ public class Repo {
|
||||
return address;
|
||||
}
|
||||
|
||||
public int getNumberOfApps() {
|
||||
DB db = DB.getDB();
|
||||
int count = db.countAppsForRepo(id);
|
||||
DB.releaseDB();
|
||||
return count;
|
||||
}
|
||||
|
||||
public boolean isSigned() {
|
||||
return this.pubkey != null && this.pubkey.length() > 0;
|
||||
}
|
||||
@ -146,7 +137,7 @@ public class Repo {
|
||||
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() {}
|
||||
@ -76,6 +77,7 @@ public class RepoProvider extends FDroidProvider {
|
||||
repos.add(new Repo(cursor));
|
||||
cursor.moveToNext();
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
return repos;
|
||||
}
|
||||
@ -104,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)) {
|
||||
@ -152,17 +154,32 @@ 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,
|
||||
long repoId) {
|
||||
String[] projection = { "COUNT(distinct id)" };
|
||||
String selection = "repo = ?";
|
||||
String[] args = { Long.toString(repoId) };
|
||||
Uri apkUri = ApkProvider.getContentUri();
|
||||
Cursor result = resolver.query(apkUri, projection, selection, args, null);
|
||||
if (result != null && result.getCount() > 0) {
|
||||
result.moveToFirst();
|
||||
return result.getInt(0);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface DataColumns extends BaseColumns {
|
||||
@ -189,12 +206,12 @@ public class RepoProvider extends FDroidProvider {
|
||||
private static final UriMatcher matcher = new UriMatcher(-1);
|
||||
|
||||
static {
|
||||
matcher.addURI(AUTHORITY, PROVIDER_NAME, CODE_LIST);
|
||||
matcher.addURI(AUTHORITY, PROVIDER_NAME + "/#", CODE_SINGLE);
|
||||
matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, null, CODE_LIST);
|
||||
matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, "#", CODE_SINGLE);
|
||||
}
|
||||
|
||||
public static Uri getContentUri() {
|
||||
return Uri.parse("content://" + AUTHORITY + "/" + PROVIDER_NAME);
|
||||
return Uri.parse("content://" + AUTHORITY + "." + PROVIDER_NAME);
|
||||
}
|
||||
|
||||
public static Uri getContentUri(long repoId) {
|
||||
@ -226,8 +243,8 @@ public class RepoProvider extends FDroidProvider {
|
||||
break;
|
||||
|
||||
case CODE_SINGLE:
|
||||
selection = ( selection == null ? "" : selection ) +
|
||||
"_ID = " + uri.getLastPathSegment();
|
||||
selection = ( selection == null ? "" : selection + " AND " ) +
|
||||
DataColumns._ID + " = " + uri.getLastPathSegment();
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -287,7 +304,7 @@ public class RepoProvider extends FDroidProvider {
|
||||
return 0;
|
||||
|
||||
case CODE_SINGLE:
|
||||
where = ( where == null ? "" : where ) +
|
||||
where = ( where == null ? "" : where + " AND " ) +
|
||||
"_ID = " + uri.getLastPathSegment();
|
||||
break;
|
||||
|
||||
|
32
src/org/fdroid/fdroid/data/ValueObject.java
Normal file
32
src/org/fdroid/fdroid/data/ValueObject.java
Normal file
@ -0,0 +1,32 @@
|
||||
package org.fdroid.fdroid.data;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.util.Log;
|
||||
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 = Utils.DATE_FORMAT.parse(string);
|
||||
} catch (ParseException e) {
|
||||
Log.e("FDroid", "Error parsing date " + string);
|
||||
}
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
}
|
@ -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,40 +1,50 @@
|
||||
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)
|
||||
@ -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,18 +110,16 @@ 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) {
|
||||
@ -142,26 +129,31 @@ abstract public class AppListAdapter extends BaseAdapter {
|
||||
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;
|
||||
@ -22,6 +25,19 @@ public class AppListFragmentPageAdapter extends FragmentPagerAdapter {
|
||||
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
|
||||
public Fragment getItem(int i) {
|
||||
if ( i == 0 ) {
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -148,7 +148,10 @@ public class RepoDetailsFragment extends Fragment {
|
||||
TextView lastUpdated = (TextView)repoView.findViewById(R.id.text_last_update);
|
||||
|
||||
name.setText(repo.getName());
|
||||
numApps.setText(Integer.toString(repo.getNumberOfApps()));
|
||||
|
||||
int appCount = RepoProvider.Helper.countAppsForRepo(
|
||||
getActivity().getContentResolver(), repo.getId());
|
||||
numApps.setText(Integer.toString(appCount));
|
||||
|
||||
setupDescription(repoView, repo);
|
||||
setupRepoFingerprint(repoView, repo);
|
||||
|
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