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" >
|
android:supportsRtl="false" >
|
||||||
|
|
||||||
<provider
|
<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:name="org.fdroid.fdroid.data.RepoProvider"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:authorities="org.fdroid.fdroid.data.ApkProvider"
|
||||||
|
android:name="org.fdroid.fdroid.data.ApkProvider"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".FDroid"
|
android:name=".FDroid"
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize" >
|
android:configChanges="keyboardHidden|orientation|screenSize" >
|
||||||
|
@ -58,4 +58,11 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_width="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>
|
</RelativeLayout>
|
||||||
|
@ -12,10 +12,4 @@
|
|||||||
<item>Dark</item>
|
<item>Dark</item>
|
||||||
<item>Light</item>
|
<item>Light</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="dbSyncModeNames">
|
|
||||||
<item>Off (unsafe)</item>
|
|
||||||
<item>Normal</item>
|
|
||||||
<item>Full</item>
|
|
||||||
</string-array>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -127,8 +127,6 @@
|
|||||||
|
|
||||||
<string name="search_hint">Search applications</string>
|
<string name="search_hint">Search applications</string>
|
||||||
|
|
||||||
<string name="db_sync_mode">Database sync mode</string>
|
|
||||||
|
|
||||||
<string name="appcompatibility">Application compatibility</string>
|
<string name="appcompatibility">Application compatibility</string>
|
||||||
<string name="show_incompat_versions">Incompatible versions</string>
|
<string name="show_incompat_versions">Incompatible versions</string>
|
||||||
<string name="show_incompat_versions_on">Show app versions incompatible with the device</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_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_connecting_to_repo">Connecting to\n%1$s</string>
|
||||||
<string name="status_checking_compatibility">Checking apps compatibility with your device…</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="no_permissions">No permissions are used.</string>
|
||||||
<string name="permissions_for_long">Permissions for version %s</string>
|
<string name="permissions_for_long">Permissions for version %s</string>
|
||||||
<string name="showPermissions">Show permissions</string>
|
<string name="showPermissions">Show permissions</string>
|
||||||
@ -195,7 +194,8 @@
|
|||||||
<string name="repo_disabled_notification">Disabled "%1$s".\n\nYou will
|
<string name="repo_disabled_notification">Disabled "%1$s".\n\nYou will
|
||||||
need to re-enable this repository to install apps from it.
|
need to re-enable this repository to install apps from it.
|
||||||
</string>
|
</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="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>
|
</resources>
|
||||||
|
@ -49,10 +49,5 @@
|
|||||||
<CheckBoxPreference android:title="@string/expert"
|
<CheckBoxPreference android:title="@string/expert"
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
android:key="expert" />
|
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>
|
</PreferenceCategory>
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
@ -21,12 +21,12 @@ package org.fdroid.fdroid;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.fdroid.fdroid.data.Repo;
|
import android.content.*;
|
||||||
import org.fdroid.fdroid.data.RepoProvider;
|
import android.widget.*;
|
||||||
|
import org.fdroid.fdroid.data.*;
|
||||||
import org.xml.sax.XMLReader;
|
import org.xml.sax.XMLReader;
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
@ -37,19 +37,10 @@ import android.os.Bundle;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.preference.PreferenceManager;
|
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.PackageManager;
|
||||||
import android.content.pm.PackageInfo;
|
import android.content.pm.PackageInfo;
|
||||||
import android.content.pm.Signature;
|
import android.content.pm.Signature;
|
||||||
import android.content.pm.PackageManager.NameNotFoundException;
|
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.Editable;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.Html.TagHandler;
|
import android.text.Html.TagHandler;
|
||||||
@ -63,7 +54,6 @@ import android.view.MenuItem;
|
|||||||
import android.view.SubMenu;
|
import android.view.SubMenu;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.BaseAdapter;
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
|
||||||
import android.support.v4.app.NavUtils;
|
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.PackageManagerCompat;
|
||||||
import org.fdroid.fdroid.compat.ActionBarCompat;
|
import org.fdroid.fdroid.compat.ActionBarCompat;
|
||||||
import org.fdroid.fdroid.compat.MenuManager;
|
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.DisplayImageOptions;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
|
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
|
||||||
import com.nostra13.universalimageloader.utils.StorageUtils;
|
|
||||||
|
|
||||||
import android.os.Environment;
|
|
||||||
|
|
||||||
public class AppDetails extends ListActivity {
|
public class AppDetails extends ListActivity {
|
||||||
|
|
||||||
private static final int REQUEST_INSTALL = 0;
|
private static final int REQUEST_INSTALL = 0;
|
||||||
private static final int REQUEST_UNINSTALL = 1;
|
private static final int REQUEST_UNINSTALL = 1;
|
||||||
|
private ApkListAdapter adapter;
|
||||||
|
|
||||||
private static class ViewHolder {
|
private static class ViewHolder {
|
||||||
TextView version;
|
TextView version;
|
||||||
TextView status;
|
TextView status;
|
||||||
TextView size;
|
TextView size;
|
||||||
TextView api;
|
TextView api;
|
||||||
|
TextView incompatibleReasons;
|
||||||
TextView buildtype;
|
TextView buildtype;
|
||||||
TextView added;
|
TextView added;
|
||||||
TextView nativecode;
|
TextView nativecode;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ApkListAdapter extends BaseAdapter {
|
private class ApkListAdapter extends ArrayAdapter<Apk> {
|
||||||
|
|
||||||
private List<DB.Apk> items;
|
private LayoutInflater mInflater = (LayoutInflater) mctx.getSystemService(
|
||||||
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(
|
|
||||||
Context.LAYOUT_INFLATER_SERVICE);
|
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) {
|
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
|
@Override
|
||||||
public View getView(int position, View convertView, ViewGroup parent) {
|
public View getView(int position, View convertView, ViewGroup parent) {
|
||||||
|
|
||||||
java.text.DateFormat df = DateFormat.getDateFormat(mctx);
|
java.text.DateFormat df = DateFormat.getDateFormat(mctx);
|
||||||
DB.Apk apk = items.get(position);
|
Apk apk = getItem(position);
|
||||||
ViewHolder holder;
|
ViewHolder holder;
|
||||||
|
|
||||||
if (convertView == null) {
|
if (convertView == null) {
|
||||||
@ -153,6 +116,7 @@ public class AppDetails extends ListActivity {
|
|||||||
holder.status = (TextView) convertView.findViewById(R.id.status);
|
holder.status = (TextView) convertView.findViewById(R.id.status);
|
||||||
holder.size = (TextView) convertView.findViewById(R.id.size);
|
holder.size = (TextView) convertView.findViewById(R.id.size);
|
||||||
holder.api = (TextView) convertView.findViewById(R.id.api);
|
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.buildtype = (TextView) convertView.findViewById(R.id.buildtype);
|
||||||
holder.added = (TextView) convertView.findViewById(R.id.added);
|
holder.added = (TextView) convertView.findViewById(R.id.added);
|
||||||
holder.nativecode = (TextView) convertView.findViewById(R.id.nativecode);
|
holder.nativecode = (TextView) convertView.findViewById(R.id.nativecode);
|
||||||
@ -164,9 +128,9 @@ public class AppDetails extends ListActivity {
|
|||||||
|
|
||||||
holder.version.setText(getString(R.string.version)
|
holder.version.setText(getString(R.string.version)
|
||||||
+ " " + apk.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
|
&& mInstalledSigID != null && apk.sig != null
|
||||||
&& apk.sig.equals(mInstalledSigID)) {
|
&& apk.sig.equals(mInstalledSigID)) {
|
||||||
holder.status.setText(getString(R.string.inst));
|
holder.status.setText(getString(R.string.inst));
|
||||||
@ -174,14 +138,14 @@ public class AppDetails extends ListActivity {
|
|||||||
holder.status.setText(getString(R.string.not_inst));
|
holder.status.setText(getString(R.string.not_inst));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apk.detail_size > 0) {
|
if (apk.size > 0) {
|
||||||
holder.size.setText(Utils.getFriendlySize(apk.detail_size));
|
holder.size.setText(Utils.getFriendlySize(apk.size));
|
||||||
holder.size.setVisibility(View.VISIBLE);
|
holder.size.setVisibility(View.VISIBLE);
|
||||||
} else {
|
} else {
|
||||||
holder.size.setVisibility(View.GONE);
|
holder.size.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apk.minSdkVersion > 0) {
|
if (pref_expert && apk.minSdkVersion > 0) {
|
||||||
holder.api.setText(getString(R.string.minsdk_or_later,
|
holder.api.setText(getString(R.string.minsdk_or_later,
|
||||||
Utils.getAndroidVersionName(apk.minSdkVersion)));
|
Utils.getAndroidVersionName(apk.minSdkVersion)));
|
||||||
holder.api.setVisibility(View.VISIBLE);
|
holder.api.setVisibility(View.VISIBLE);
|
||||||
@ -211,8 +175,13 @@ public class AppDetails extends ListActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (apk.incompatible_reasons != null) {
|
if (apk.incompatible_reasons != null) {
|
||||||
holder.api.setText(apk.incompatible_reasons.toString());
|
holder.incompatibleReasons.setText(
|
||||||
holder.api.setVisibility(View.VISIBLE);
|
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...
|
// Disable it all if it isn't compatible...
|
||||||
@ -222,13 +191,14 @@ public class AppDetails extends ListActivity {
|
|||||||
holder.status,
|
holder.status,
|
||||||
holder.size,
|
holder.size,
|
||||||
holder.api,
|
holder.api,
|
||||||
|
holder.incompatibleReasons,
|
||||||
holder.buildtype,
|
holder.buildtype,
|
||||||
holder.added,
|
holder.added,
|
||||||
holder.nativecode
|
holder.nativecode
|
||||||
};
|
};
|
||||||
|
|
||||||
for (View view : views) {
|
for (View v : views) {
|
||||||
view.setEnabled(apk.compatible);
|
v.setEnabled(apk.compatible);
|
||||||
}
|
}
|
||||||
|
|
||||||
return convertView;
|
return convertView;
|
||||||
@ -251,7 +221,7 @@ public class AppDetails extends ListActivity {
|
|||||||
private static final int FLATTR = Menu.FIRST + 13;
|
private static final int FLATTR = Menu.FIRST + 13;
|
||||||
private static final int DONATE_URL = Menu.FIRST + 14;
|
private static final int DONATE_URL = Menu.FIRST + 14;
|
||||||
|
|
||||||
private DB.App app;
|
private App app;
|
||||||
private String appid;
|
private String appid;
|
||||||
private PackageManager mPm;
|
private PackageManager mPm;
|
||||||
private DownloadHandler downloadHandler;
|
private DownloadHandler downloadHandler;
|
||||||
@ -339,8 +309,8 @@ public class AppDetails extends ListActivity {
|
|||||||
headerView = new LinearLayout(this);
|
headerView = new LinearLayout(this);
|
||||||
ListView lv = (ListView) findViewById(android.R.id.list);
|
ListView lv = (ListView) findViewById(android.R.id.list);
|
||||||
lv.addHeaderView(headerView);
|
lv.addHeaderView(headerView);
|
||||||
ApkListAdapter la = new ApkListAdapter(this, app.apks);
|
adapter = new ApkListAdapter(this, app);
|
||||||
setListAdapter(la);
|
setListAdapter(adapter);
|
||||||
|
|
||||||
startViews();
|
startViews();
|
||||||
|
|
||||||
@ -381,17 +351,24 @@ public class AppDetails extends ListActivity {
|
|||||||
}
|
}
|
||||||
if (app != null && (app.ignoreAllUpdates != startingIgnoreAll
|
if (app != null && (app.ignoreAllUpdates != startingIgnoreAll
|
||||||
|| app.ignoreThisUpdate != startingIgnoreThis)) {
|
|| app.ignoreThisUpdate != startingIgnoreThis)) {
|
||||||
try {
|
setIgnoreUpdates(app.id, app.ignoreAllUpdates, app.ignoreThisUpdate);
|
||||||
DB db = DB.getDB();
|
|
||||||
db.setIgnoreUpdates(app.id,
|
|
||||||
app.ignoreAllUpdates, app.ignoreThisUpdate);
|
|
||||||
} finally {
|
|
||||||
DB.releaseDB();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
super.onPause();
|
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
|
@Override
|
||||||
public Object onRetainNonConfigurationInstance() {
|
public Object onRetainNonConfigurationInstance() {
|
||||||
stateRetained = true;
|
stateRetained = true;
|
||||||
@ -426,15 +403,11 @@ public class AppDetails extends ListActivity {
|
|||||||
|
|
||||||
Log.d("FDroid", "Getting application details for " + appid);
|
Log.d("FDroid", "Getting application details for " + appid);
|
||||||
app = null;
|
app = null;
|
||||||
|
|
||||||
if (appid != null && appid.length() > 0) {
|
if (appid != null && appid.length() > 0) {
|
||||||
List<DB.App> apps = ((FDroidApp) getApplication()).getApps();
|
app = AppProvider.Helper.findById(getContentResolver(), appid);
|
||||||
for (DB.App tapp : apps) {
|
|
||||||
if (tapp.id.equals(appid)) {
|
|
||||||
app = tapp;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app == null) {
|
if (app == null) {
|
||||||
Toast toast = Toast.makeText(this,
|
Toast toast = Toast.makeText(this,
|
||||||
getString(R.string.no_such_app), Toast.LENGTH_LONG);
|
getString(R.string.no_such_app), Toast.LENGTH_LONG);
|
||||||
@ -443,23 +416,13 @@ public class AppDetails extends ListActivity {
|
|||||||
return false;
|
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;
|
startingIgnoreAll = app.ignoreAllUpdates;
|
||||||
startingIgnoreThis = app.ignoreThisUpdate;
|
startingIgnoreThis = app.ignoreThisUpdate;
|
||||||
|
|
||||||
// Get the signature of the installed package...
|
// Get the signature of the installed package...
|
||||||
mInstalledSignature = null;
|
mInstalledSignature = null;
|
||||||
mInstalledSigID = null;
|
mInstalledSigID = null;
|
||||||
if (app.installedVersion != null) {
|
if (app.getInstalledVersion(this) != null) {
|
||||||
PackageManager pm = getBaseContext().getPackageManager();
|
PackageManager pm = getBaseContext().getPackageManager();
|
||||||
try {
|
try {
|
||||||
PackageInfo pi = pm.getPackageInfo(appid,
|
PackageInfo pi = pm.getPackageInfo(appid,
|
||||||
@ -548,7 +511,7 @@ public class AppDetails extends ListActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spanned desc = Html.fromHtml(
|
Spanned desc = Html.fromHtml(
|
||||||
app.detail_description, null, new HtmlTagHandler());
|
app.description, null, new HtmlTagHandler());
|
||||||
tv.setText(desc.subSequence(0, desc.length() - 2));
|
tv.setText(desc.subSequence(0, desc.length() - 2));
|
||||||
|
|
||||||
tv = (TextView) infoView.findViewById(R.id.appid);
|
tv = (TextView) infoView.findViewById(R.id.appid);
|
||||||
@ -560,11 +523,20 @@ public class AppDetails extends ListActivity {
|
|||||||
tv = (TextView) infoView.findViewById(R.id.summary);
|
tv = (TextView) infoView.findViewById(R.id.summary);
|
||||||
tv.setText(app.summary);
|
tv.setText(app.summary);
|
||||||
|
|
||||||
if (pref_permissions && app.curApk != null &&
|
Apk curApk = null;
|
||||||
(app.curApk.compatible || pref_incompatibleVersions)) {
|
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);
|
tv = (TextView) infoView.findViewById(R.id.permissions_list);
|
||||||
|
|
||||||
CommaSeparatedList permsList = app.curApk.detail_permissions;
|
CommaSeparatedList permsList = adapter.getItem(0).permissions;
|
||||||
if (permsList == null) {
|
if (permsList == null) {
|
||||||
tv.setText(getString(R.string.no_permissions));
|
tv.setText(getString(R.string.no_permissions));
|
||||||
} else {
|
} else {
|
||||||
@ -589,7 +561,7 @@ public class AppDetails extends ListActivity {
|
|||||||
}
|
}
|
||||||
tv = (TextView) infoView.findViewById(R.id.permissions);
|
tv = (TextView) infoView.findViewById(R.id.permissions);
|
||||||
tv.setText(getString(
|
tv.setText(getString(
|
||||||
R.string.permissions_for_long, app.apks.get(0).version));
|
R.string.permissions_for_long, adapter.getItem(0).version));
|
||||||
} else {
|
} else {
|
||||||
infoView.findViewById(R.id.permissions).setVisibility(View.GONE);
|
infoView.findViewById(R.id.permissions).setVisibility(View.GONE);
|
||||||
infoView.findViewById(R.id.permissions_list).setVisibility(View.GONE);
|
infoView.findViewById(R.id.permissions_list).setVisibility(View.GONE);
|
||||||
@ -634,15 +606,14 @@ public class AppDetails extends ListActivity {
|
|||||||
private void updateViews() {
|
private void updateViews() {
|
||||||
|
|
||||||
// Refresh the list...
|
// Refresh the list...
|
||||||
ApkListAdapter la = (ApkListAdapter) getListAdapter();
|
adapter.notifyDataSetChanged();
|
||||||
la.notifyDataSetChanged();
|
|
||||||
|
|
||||||
TextView tv = (TextView) findViewById(R.id.status);
|
TextView tv = (TextView) findViewById(R.id.status);
|
||||||
if (app.installedVersion == null)
|
if (app.getInstalledVersion(this) == null)
|
||||||
tv.setText(getString(R.string.details_notinstalled));
|
tv.setText(getString(R.string.details_notinstalled));
|
||||||
else
|
else
|
||||||
tv.setText(getString(R.string.details_installed,
|
tv.setText(getString(R.string.details_installed,
|
||||||
app.installedVersion));
|
app.getInstalledVersion(this)));
|
||||||
|
|
||||||
tv = (TextView) infoView.findViewById(R.id.signature);
|
tv = (TextView) infoView.findViewById(R.id.signature);
|
||||||
if (pref_expert && mInstalledSignature != null) {
|
if (pref_expert && mInstalledSignature != null) {
|
||||||
@ -656,10 +627,10 @@ public class AppDetails extends ListActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onListItemClick(ListView l, View v, int position, long id) {
|
protected void onListItemClick(ListView l, View v, int position, long id) {
|
||||||
app.curApk = app.apks.get(position - l.getHeaderViewsCount());
|
final Apk apk = adapter.getItem(position - l.getHeaderViewsCount());
|
||||||
if (app.installedVerCode == app.curApk.vercode)
|
if (app.getInstalledVerCode(this) == apk.vercode)
|
||||||
removeApk(app.id);
|
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);
|
AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this);
|
||||||
ask_alrt.setMessage(getString(R.string.installDowngrade));
|
ask_alrt.setMessage(getString(R.string.installDowngrade));
|
||||||
ask_alrt.setPositiveButton(getString(R.string.yes),
|
ask_alrt.setPositiveButton(getString(R.string.yes),
|
||||||
@ -667,7 +638,7 @@ public class AppDetails extends ListActivity {
|
|||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog,
|
public void onClick(DialogInterface dialog,
|
||||||
int whichButton) {
|
int whichButton) {
|
||||||
install();
|
install(apk);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ask_alrt.setNegativeButton(getString(R.string.no),
|
ask_alrt.setNegativeButton(getString(R.string.no),
|
||||||
@ -680,7 +651,7 @@ public class AppDetails extends ListActivity {
|
|||||||
AlertDialog alert = ask_alrt.create();
|
AlertDialog alert = ask_alrt.create();
|
||||||
alert.show();
|
alert.show();
|
||||||
} else
|
} else
|
||||||
install();
|
install(apk);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -690,20 +661,23 @@ public class AppDetails extends ListActivity {
|
|||||||
menu.clear();
|
menu.clear();
|
||||||
if (app == null)
|
if (app == null)
|
||||||
return true;
|
return true;
|
||||||
if (app.toUpdate) {
|
if (app.canAndWantToUpdate(this)) {
|
||||||
MenuItemCompat.setShowAsAction(menu.add(
|
MenuItemCompat.setShowAsAction(menu.add(
|
||||||
Menu.NONE, INSTALL, 0, R.string.menu_upgrade)
|
Menu.NONE, INSTALL, 0, R.string.menu_upgrade)
|
||||||
.setIcon(R.drawable.ic_menu_refresh),
|
.setIcon(R.drawable.ic_menu_refresh),
|
||||||
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
|
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
|
||||||
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
|
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(
|
MenuItemCompat.setShowAsAction(menu.add(
|
||||||
Menu.NONE, INSTALL, 1, R.string.menu_install)
|
Menu.NONE, INSTALL, 1, R.string.menu_install)
|
||||||
.setIcon(android.R.drawable.ic_menu_add),
|
.setIcon(android.R.drawable.ic_menu_add),
|
||||||
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
|
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
|
||||||
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
|
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
|
||||||
} else if (app.installedVersion != null) {
|
} else if (app.getInstalledVersion(this) != null) {
|
||||||
MenuItemCompat.setShowAsAction(menu.add(
|
MenuItemCompat.setShowAsAction(menu.add(
|
||||||
Menu.NONE, UNINSTALL, 1, R.string.menu_uninstall)
|
Menu.NONE, UNINSTALL, 1, R.string.menu_uninstall)
|
||||||
.setIcon(android.R.drawable.ic_menu_delete),
|
.setIcon(android.R.drawable.ic_menu_delete),
|
||||||
@ -730,40 +704,40 @@ public class AppDetails extends ListActivity {
|
|||||||
.setCheckable(true)
|
.setCheckable(true)
|
||||||
.setChecked(app.ignoreAllUpdates);
|
.setChecked(app.ignoreAllUpdates);
|
||||||
|
|
||||||
if (app.hasUpdates) {
|
if (app.hasUpdates(this)) {
|
||||||
menu.add(Menu.NONE, IGNORETHIS, 2, R.string.menu_ignore_this)
|
menu.add(Menu.NONE, IGNORETHIS, 2, R.string.menu_ignore_this)
|
||||||
.setIcon(android.R.drawable.ic_menu_close_clear_cancel)
|
.setIcon(android.R.drawable.ic_menu_close_clear_cancel)
|
||||||
.setCheckable(true)
|
.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(
|
menu.add(Menu.NONE, WEBSITE, 3, R.string.menu_website).setIcon(
|
||||||
android.R.drawable.ic_menu_view);
|
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(
|
menu.add(Menu.NONE, ISSUES, 4, R.string.menu_issues).setIcon(
|
||||||
android.R.drawable.ic_menu_view);
|
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(
|
menu.add(Menu.NONE, SOURCE, 5, R.string.menu_source).setIcon(
|
||||||
android.R.drawable.ic_menu_view);
|
android.R.drawable.ic_menu_view);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app.detail_bitcoinAddr != null || app.detail_litecoinAddr != null ||
|
if (app.bitcoinAddr != null || app.litecoinAddr != null ||
|
||||||
app.detail_dogecoinAddr != null ||
|
app.dogecoinAddr != null ||
|
||||||
app.detail_flattrID != null || app.detail_donateURL != null) {
|
app.flattrID != null || app.donateURL != null) {
|
||||||
SubMenu donate = menu.addSubMenu(Menu.NONE, DONATE, 7,
|
SubMenu donate = menu.addSubMenu(Menu.NONE, DONATE, 7,
|
||||||
R.string.menu_donate).setIcon(
|
R.string.menu_donate).setIcon(
|
||||||
android.R.drawable.ic_menu_send);
|
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);
|
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);
|
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);
|
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);
|
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);
|
donate.add(Menu.NONE, DONATE_URL, 10, R.string.menu_website);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -801,8 +775,10 @@ public class AppDetails extends ListActivity {
|
|||||||
|
|
||||||
case INSTALL:
|
case INSTALL:
|
||||||
// Note that this handles updating as well as installing.
|
// Note that this handles updating as well as installing.
|
||||||
if (app.curApk != null)
|
if (app.curVercode > 0) {
|
||||||
install();
|
final Apk apkToInstall = ApkProvider.Helper.find(this, app.id, app.curVercode);
|
||||||
|
install(apkToInstall);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case UNINSTALL:
|
case UNINSTALL:
|
||||||
@ -815,43 +791,43 @@ public class AppDetails extends ListActivity {
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
case IGNORETHIS:
|
case IGNORETHIS:
|
||||||
if (app.ignoreThisUpdate >= app.curApk.vercode)
|
if (app.ignoreThisUpdate >= app.curVercode)
|
||||||
app.ignoreThisUpdate = 0;
|
app.ignoreThisUpdate = 0;
|
||||||
else
|
else
|
||||||
app.ignoreThisUpdate = app.curApk.vercode;
|
app.ignoreThisUpdate = app.curVercode;
|
||||||
item.setChecked(app.ignoreThisUpdate > 0);
|
item.setChecked(app.ignoreThisUpdate > 0);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case WEBSITE:
|
case WEBSITE:
|
||||||
tryOpenUri(app.detail_webURL);
|
tryOpenUri(app.webURL);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case ISSUES:
|
case ISSUES:
|
||||||
tryOpenUri(app.detail_trackerURL);
|
tryOpenUri(app.trackerURL);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case SOURCE:
|
case SOURCE:
|
||||||
tryOpenUri(app.detail_sourceURL);
|
tryOpenUri(app.sourceURL);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case BITCOIN:
|
case BITCOIN:
|
||||||
tryOpenUri("bitcoin:" + app.detail_bitcoinAddr);
|
tryOpenUri("bitcoin:" + app.bitcoinAddr);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case LITECOIN:
|
case LITECOIN:
|
||||||
tryOpenUri("litecoin:" + app.detail_litecoinAddr);
|
tryOpenUri("litecoin:" + app.litecoinAddr);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case DOGECOIN:
|
case DOGECOIN:
|
||||||
tryOpenUri("dogecoin:" + app.detail_dogecoinAddr);
|
tryOpenUri("dogecoin:" + app.dogecoinAddr);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case FLATTR:
|
case FLATTR:
|
||||||
tryOpenUri("https://flattr.com/thing/" + app.detail_flattrID);
|
tryOpenUri("https://flattr.com/thing/" + app.flattrID);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case DONATE_URL:
|
case DONATE_URL:
|
||||||
tryOpenUri(app.detail_donateURL);
|
tryOpenUri(app.donateURL);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -859,17 +835,16 @@ public class AppDetails extends ListActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Install the version of this app denoted by 'app.curApk'.
|
// Install the version of this app denoted by 'app.curApk'.
|
||||||
private void install() {
|
private void install(final Apk apk) {
|
||||||
|
|
||||||
String [] projection = { RepoProvider.DataColumns.ADDRESS };
|
String [] projection = { RepoProvider.DataColumns.ADDRESS };
|
||||||
Repo repo = RepoProvider.Helper.findById(
|
Repo repo = RepoProvider.Helper.findById(
|
||||||
getContentResolver(), app.curApk.repo, projection);
|
getContentResolver(), apk.repo, projection);
|
||||||
if (repo == null || repo.address == null) {
|
if (repo == null || repo.address == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final String repoaddress = repo.address;
|
final String repoaddress = repo.address;
|
||||||
|
|
||||||
if (!app.curApk.compatible) {
|
if (!apk.compatible) {
|
||||||
AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this);
|
AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this);
|
||||||
ask_alrt.setMessage(getString(R.string.installIncompatible));
|
ask_alrt.setMessage(getString(R.string.installIncompatible));
|
||||||
ask_alrt.setPositiveButton(getString(R.string.yes),
|
ask_alrt.setPositiveButton(getString(R.string.yes),
|
||||||
@ -877,7 +852,7 @@ public class AppDetails extends ListActivity {
|
|||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog,
|
public void onClick(DialogInterface dialog,
|
||||||
int whichButton) {
|
int whichButton) {
|
||||||
downloadHandler = new DownloadHandler(app.curApk,
|
downloadHandler = new DownloadHandler(apk,
|
||||||
repoaddress, Utils
|
repoaddress, Utils
|
||||||
.getApkCacheDir(getBaseContext()));
|
.getApkCacheDir(getBaseContext()));
|
||||||
}
|
}
|
||||||
@ -893,8 +868,8 @@ public class AppDetails extends ListActivity {
|
|||||||
alert.show();
|
alert.show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mInstalledSigID != null && app.curApk.sig != null
|
if (mInstalledSigID != null && apk.sig != null
|
||||||
&& !app.curApk.sig.equals(mInstalledSigID)) {
|
&& !apk.sig.equals(mInstalledSigID)) {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
builder.setMessage(R.string.SignatureMismatch).setPositiveButton(
|
builder.setMessage(R.string.SignatureMismatch).setPositiveButton(
|
||||||
getString(R.string.ok),
|
getString(R.string.ok),
|
||||||
@ -908,7 +883,7 @@ public class AppDetails extends ListActivity {
|
|||||||
alert.show();
|
alert.show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
downloadHandler = new DownloadHandler(app.curApk, repoaddress,
|
downloadHandler = new DownloadHandler(apk, repoaddress,
|
||||||
Utils.getApkCacheDir(getBaseContext()));
|
Utils.getApkCacheDir(getBaseContext()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -940,7 +915,7 @@ public class AppDetails extends ListActivity {
|
|||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void shareApp(DB.App app) {
|
private void shareApp(App app) {
|
||||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||||
shareIntent.setType("text/plain");
|
shareIntent.setType("text/plain");
|
||||||
|
|
||||||
@ -983,7 +958,7 @@ public class AppDetails extends ListActivity {
|
|||||||
private boolean updating;
|
private boolean updating;
|
||||||
private String id;
|
private String id;
|
||||||
|
|
||||||
public DownloadHandler(DB.Apk apk, String repoaddress, File destdir) {
|
public DownloadHandler(Apk apk, String repoaddress, File destdir) {
|
||||||
id = apk.id;
|
id = apk.id;
|
||||||
download = new Downloader(apk, repoaddress, destdir);
|
download = new Downloader(apk, repoaddress, destdir);
|
||||||
download.start();
|
download.start();
|
||||||
|
@ -21,28 +21,25 @@ package org.fdroid.fdroid;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
import org.fdroid.fdroid.data.App;
|
||||||
|
|
||||||
public class AppFilter {
|
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
|
// Return true if the given app should be filtered out based on user
|
||||||
// preferences, and false otherwise.
|
// preferences, and false otherwise.
|
||||||
public boolean filter(DB.App app) {
|
public boolean filter(App app) {
|
||||||
if (app.requirements == null) return false;
|
|
||||||
|
boolean filterRequiringRoot = Preferences.get().filterAppsRequiringRoot();
|
||||||
|
|
||||||
|
if (app.requirements == null || !filterRequiringRoot) return false;
|
||||||
|
|
||||||
for (String r : app.requirements) {
|
for (String r : app.requirements) {
|
||||||
if (r.equals("root") && !pref_rooted)
|
if (r.equals("root"))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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 java.net.URL;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import org.fdroid.fdroid.data.Apk;
|
||||||
|
|
||||||
public class Downloader extends Thread {
|
public class Downloader extends Thread {
|
||||||
|
|
||||||
private DB.Apk curapk;
|
private Apk curapk;
|
||||||
private String repoaddress;
|
private String repoaddress;
|
||||||
private String filename;
|
private String filename;
|
||||||
private File destdir;
|
private File destdir;
|
||||||
@ -38,11 +39,11 @@ public class Downloader extends Thread {
|
|||||||
|
|
||||||
public static enum Status {
|
public static enum Status {
|
||||||
STARTING, RUNNING, ERROR, DONE, CANCELLED
|
STARTING, RUNNING, ERROR, DONE, CANCELLED
|
||||||
};
|
}
|
||||||
|
|
||||||
public static enum Error {
|
public static enum Error {
|
||||||
CORRUPT, UNKNOWN
|
CORRUPT, UNKNOWN
|
||||||
};
|
}
|
||||||
|
|
||||||
private Status status = Status.STARTING;
|
private Status status = Status.STARTING;
|
||||||
private Error error;
|
private Error error;
|
||||||
@ -52,7 +53,7 @@ public class Downloader extends Thread {
|
|||||||
|
|
||||||
// Constructor - creates a Downloader to download the given Apk,
|
// Constructor - creates a Downloader to download the given Apk,
|
||||||
// which must have its detail populated.
|
// which must have its detail populated.
|
||||||
Downloader(DB.Apk apk, String repoaddress, File destdir) {
|
Downloader(Apk apk, String repoaddress, File destdir) {
|
||||||
curapk = apk;
|
curapk = apk;
|
||||||
this.repoaddress = repoaddress;
|
this.repoaddress = repoaddress;
|
||||||
this.destdir = destdir;
|
this.destdir = destdir;
|
||||||
@ -91,7 +92,7 @@ public class Downloader extends Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The APK being downloaded
|
// The APK being downloaded
|
||||||
public synchronized DB.Apk getApk() {
|
public synchronized Apk getApk() {
|
||||||
return curapk;
|
return curapk;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,8 +108,8 @@ public class Downloader extends Thread {
|
|||||||
// See if we already have this apk cached...
|
// See if we already have this apk cached...
|
||||||
if (localfile.exists()) {
|
if (localfile.exists()) {
|
||||||
// We do - if its hash matches, we'll use it...
|
// We do - if its hash matches, we'll use it...
|
||||||
Hasher hash = new Hasher(curapk.detail_hashType, localfile);
|
Hasher hash = new Hasher(curapk.hashType, localfile);
|
||||||
if (hash.match(curapk.detail_hash)) {
|
if (hash.match(curapk.hash)) {
|
||||||
Log.d("FDroid", "Using cached apk at " + localfile);
|
Log.d("FDroid", "Using cached apk at " + localfile);
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
progress = 1;
|
progress = 1;
|
||||||
@ -129,7 +130,7 @@ public class Downloader extends Thread {
|
|||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
filename = remotefile;
|
filename = remotefile;
|
||||||
progress = 0;
|
progress = 0;
|
||||||
max = curapk.detail_size;
|
max = curapk.size;
|
||||||
status = Status.RUNNING;
|
status = Status.RUNNING;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,11 +159,11 @@ public class Downloader extends Thread {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Hasher hash = new Hasher(curapk.detail_hashType, localfile);
|
Hasher hash = new Hasher(curapk.hashType, localfile);
|
||||||
if (!hash.match(curapk.detail_hash)) {
|
if (!hash.match(curapk.hash)) {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
Log.d("FDroid", "Downloaded file hash of " + hash.getHash()
|
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
|
// No point keeping a bad file, whether we're
|
||||||
// caching or not.
|
// caching or not.
|
||||||
localfile.delete();
|
localfile.delete();
|
||||||
|
@ -25,9 +25,11 @@ import android.app.NotificationManager;
|
|||||||
import android.content.*;
|
import android.content.*;
|
||||||
import android.content.pm.PackageInfo;
|
import android.content.pm.PackageInfo;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
|
import android.database.ContentObserver;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.ContextThemeWrapper;
|
import android.view.ContextThemeWrapper;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@ -41,6 +43,7 @@ import android.support.v4.view.MenuItemCompat;
|
|||||||
import android.support.v4.view.ViewPager;
|
import android.support.v4.view.ViewPager;
|
||||||
|
|
||||||
import org.fdroid.fdroid.compat.TabManager;
|
import org.fdroid.fdroid.compat.TabManager;
|
||||||
|
import org.fdroid.fdroid.data.AppProvider;
|
||||||
import org.fdroid.fdroid.views.AppListFragmentPageAdapter;
|
import org.fdroid.fdroid.views.AppListFragmentPageAdapter;
|
||||||
|
|
||||||
public class FDroid extends FragmentActivity {
|
public class FDroid extends FragmentActivity {
|
||||||
@ -58,21 +61,14 @@ public class FDroid extends FragmentActivity {
|
|||||||
|
|
||||||
private ViewPager viewPager;
|
private ViewPager viewPager;
|
||||||
|
|
||||||
private AppListManager manager = null;
|
|
||||||
|
|
||||||
private TabManager tabManager = null;
|
private TabManager tabManager = null;
|
||||||
|
|
||||||
public AppListManager getManager() {
|
|
||||||
return manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
||||||
((FDroidApp) getApplication()).applyTheme(this);
|
((FDroidApp) getApplication()).applyTheme(this);
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
manager = new AppListManager(this);
|
|
||||||
setContentView(R.layout.fdroid);
|
setContentView(R.layout.fdroid);
|
||||||
createViews();
|
createViews();
|
||||||
getTabManager().createTabs();
|
getTabManager().createTabs();
|
||||||
@ -99,21 +95,9 @@ public class FDroid extends FragmentActivity {
|
|||||||
call.putExtra("appid", appid);
|
call.putExtra("appid", appid);
|
||||||
startActivityForResult(call, REQUEST_APPDETAILS);
|
startActivityForResult(call, REQUEST_APPDETAILS);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
Uri uri = AppProvider.getContentUri();
|
||||||
protected void onResume() {
|
getContentResolver().registerContentObserver(uri, true, new AppObserver());
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -253,8 +237,6 @@ public class FDroid extends FragmentActivity {
|
|||||||
|
|
||||||
if ((resultCode & PreferencesActivity.RESULT_RELOAD) != 0) {
|
if ((resultCode & PreferencesActivity.RESULT_RELOAD) != 0) {
|
||||||
((FDroidApp) getApplication()).invalidateAllApps();
|
((FDroidApp) getApplication()).invalidateAllApps();
|
||||||
} else if ((resultCode & PreferencesActivity.RESULT_REFILTER) != 0) {
|
|
||||||
((FDroidApp) getApplication()).filterApps();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((resultCode & PreferencesActivity.RESULT_RESTART) != 0) {
|
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
|
// is told to do the update, which will result in the database changing. The
|
||||||
// UpdateReceiver class should get told when this is finished.
|
// UpdateReceiver class should get told when this is finished.
|
||||||
public void updateRepos() {
|
public void updateRepos() {
|
||||||
UpdateService.updateNow(this).setListener(new ProgressListener() {
|
UpdateService.updateNow(this);
|
||||||
@Override
|
|
||||||
public void onProgress(Event event) {
|
|
||||||
if (event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES){
|
|
||||||
repopulateViews();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private TabManager getTabManager() {
|
private TabManager getTabManager() {
|
||||||
@ -335,4 +310,27 @@ public class FDroid extends FragmentActivity {
|
|||||||
nMgr.cancel(id);
|
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.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
import org.fdroid.fdroid.Utils;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.cache.disc.impl.LimitedAgeDiscCache;
|
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.cache.disc.naming.FileNameGenerator;
|
||||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
||||||
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
|
||||||
import com.nostra13.universalimageloader.utils.StorageUtils;
|
import com.nostra13.universalimageloader.utils.StorageUtils;
|
||||||
|
|
||||||
import de.duenndns.ssl.MemorizingTrustManager;
|
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.PinningTrustManager;
|
||||||
import org.thoughtcrime.ssl.pinning.SystemKeyStore;
|
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...
|
// it is more deterministic as to when this gets called...
|
||||||
Preferences.setup(this);
|
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
|
// Clear cached apk files. We used to just remove them after they'd
|
||||||
// been installed, but this causes problems for proprietary gapps
|
// been installed, but this causes problems for proprietary gapps
|
||||||
// users since the introduction of verification (on pre-4.2 Android),
|
// users since the introduction of verification (on pre-4.2 Android),
|
||||||
@ -117,7 +127,6 @@ public class FDroidApp extends Application {
|
|||||||
apps = null;
|
apps = null;
|
||||||
invalidApps = new ArrayList<String>();
|
invalidApps = new ArrayList<String>();
|
||||||
ctx = getApplicationContext();
|
ctx = getApplicationContext();
|
||||||
DB.initDB(ctx);
|
|
||||||
UpdateService.schedule(ctx);
|
UpdateService.schedule(ctx);
|
||||||
|
|
||||||
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(ctx)
|
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(ctx)
|
||||||
@ -179,7 +188,7 @@ public class FDroidApp extends Application {
|
|||||||
private Context ctx;
|
private Context ctx;
|
||||||
|
|
||||||
// Global list of all known applications.
|
// 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
|
// Set when something has changed (database or installed apps) so we know
|
||||||
// we should invalidate the apps.
|
// we should invalidate the apps.
|
||||||
@ -206,59 +215,4 @@ public class FDroidApp extends Application {
|
|||||||
invalidApps.add(id);
|
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.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.ContentValues;
|
import android.content.*;
|
||||||
import android.content.Context;
|
import android.preference.PreferenceManager;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.support.v4.app.FragmentActivity;
|
import android.support.v4.app.FragmentActivity;
|
||||||
import android.support.v4.app.ListFragment;
|
import android.support.v4.app.ListFragment;
|
||||||
import android.support.v4.content.CursorLoader;
|
import android.support.v4.content.CursorLoader;
|
||||||
import android.content.Intent;
|
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.net.wifi.WifiInfo;
|
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.content.Loader;
|
||||||
import android.support.v4.view.MenuItemCompat;
|
import android.support.v4.view.MenuItemCompat;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.text.format.DateFormat;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
@ -55,6 +54,7 @@ import org.fdroid.fdroid.views.fragments.RepoDetailsFragment;
|
|||||||
|
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
public class ManageRepo extends FragmentActivity {
|
public class ManageRepo extends FragmentActivity {
|
||||||
@ -178,7 +178,7 @@ class RepoListFragment extends ListFragment
|
|||||||
changed = true;
|
changed = true;
|
||||||
} else {
|
} else {
|
||||||
FDroidApp app = (FDroidApp)getActivity().getApplication();
|
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);
|
String notification = getString(R.string.repo_disabled_notification, repo.name);
|
||||||
Toast.makeText(getActivity(), notification, Toast.LENGTH_LONG).show();
|
Toast.makeText(getActivity(), notification, Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
@ -200,6 +200,43 @@ class RepoListFragment extends ListFragment
|
|||||||
*/
|
*/
|
||||||
private boolean isImportingRepo = false;
|
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
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
||||||
@ -207,29 +244,6 @@ class RepoListFragment extends ListFragment
|
|||||||
|
|
||||||
setHasOptionsMenu(true);
|
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 */
|
/* let's see if someone is trying to send us a new repo */
|
||||||
Intent intent = getActivity().getIntent();
|
Intent intent = getActivity().getIntent();
|
||||||
/* an URL from a click, NFC, QRCode scan, etc */
|
/* an URL from a click, NFC, QRCode scan, etc */
|
||||||
@ -342,9 +356,12 @@ class RepoListFragment extends ListFragment
|
|||||||
UpdateService.updateNow(getActivity()).setListener(new ProgressListener() {
|
UpdateService.updateNow(getActivity()).setListener(new ProgressListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onProgress(Event event) {
|
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!
|
// No need to prompt to update any more, we just did it!
|
||||||
changed = false;
|
changed = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ public class PackageReceiver extends BroadcastReceiver {
|
|||||||
String appid = intent.getData().getSchemeSpecificPart();
|
String appid = intent.getData().getSchemeSpecificPart();
|
||||||
Log.d("FDroid", "PackageReceiver received "+appid);
|
Log.d("FDroid", "PackageReceiver received "+appid);
|
||||||
((FDroidApp) ctx.getApplicationContext()).invalidateApp(appid);
|
((FDroidApp) ctx.getApplicationContext()).invalidateApp(appid);
|
||||||
|
Utils.clearInstalledApksCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
package org.fdroid.fdroid;
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
|
import android.app.LoaderManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.preference.PreferenceManager;
|
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_IGN_TOUCH = "ignoreTouchscreen";
|
||||||
public static final String PREF_CACHE_APK = "cacheDownloaded";
|
public static final String PREF_CACHE_APK = "cacheDownloaded";
|
||||||
public static final String PREF_EXPERT = "expert";
|
public static final String PREF_EXPERT = "expert";
|
||||||
public static final String PREF_DB_SYNC = "dbSyncMode";
|
|
||||||
public static final String PREF_UPD_LAST = "lastUpdateCheck";
|
public static final String PREF_UPD_LAST = "lastUpdateCheck";
|
||||||
|
|
||||||
private static final boolean DEFAULT_COMPACT_LAYOUT = false;
|
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 compactLayout = DEFAULT_COMPACT_LAYOUT;
|
||||||
|
private boolean filterAppsRequiringRoot = DEFAULT_ROOTED;
|
||||||
|
|
||||||
private Map<String,Boolean> initialized = new HashMap<String,Boolean>();
|
private Map<String,Boolean> initialized = new HashMap<String,Boolean>();
|
||||||
|
|
||||||
private List<ChangeListener> compactLayoutListeners = new ArrayList<ChangeListener>();
|
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) {
|
private boolean isInitialized(String key) {
|
||||||
return initialized.containsKey(key) && initialized.get(key);
|
return initialized.containsKey(key) && initialized.get(key);
|
||||||
@ -76,6 +79,45 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
|
|||||||
compactLayoutListeners.remove(listener);
|
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
|
@Override
|
||||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||||
Log.d("FDroid", "Invalidating preference '" + key + "'.");
|
Log.d("FDroid", "Invalidating preference '" + key + "'.");
|
||||||
@ -85,7 +127,27 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
|
|||||||
for ( ChangeListener listener : compactLayoutListeners ) {
|
for ( ChangeListener listener : compactLayoutListeners ) {
|
||||||
listener.onPreferenceChange();
|
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;
|
private static Preferences instance;
|
||||||
@ -110,8 +172,4 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static interface ChangeListener {
|
|
||||||
public void onPreferenceChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,6 @@ public class PreferencesActivity extends PreferenceActivity implements
|
|||||||
OnSharedPreferenceChangeListener {
|
OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
public static final int RESULT_RELOAD = 1;
|
public static final int RESULT_RELOAD = 1;
|
||||||
public static final int RESULT_REFILTER = 2;
|
|
||||||
public static final int RESULT_RESTART = 4;
|
public static final int RESULT_RESTART = 4;
|
||||||
private int result = 0;
|
private int result = 0;
|
||||||
|
|
||||||
@ -53,8 +52,7 @@ public class PreferencesActivity extends PreferenceActivity implements
|
|||||||
Preferences.PREF_COMPACT_LAYOUT,
|
Preferences.PREF_COMPACT_LAYOUT,
|
||||||
Preferences.PREF_IGN_TOUCH,
|
Preferences.PREF_IGN_TOUCH,
|
||||||
Preferences.PREF_CACHE_APK,
|
Preferences.PREF_CACHE_APK,
|
||||||
Preferences.PREF_EXPERT,
|
Preferences.PREF_EXPERT
|
||||||
Preferences.PREF_DB_SYNC
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -133,10 +131,6 @@ public class PreferencesActivity extends PreferenceActivity implements
|
|||||||
} else if (key.equals(Preferences.PREF_ROOTED)) {
|
} else if (key.equals(Preferences.PREF_ROOTED)) {
|
||||||
onoffSummary(key, R.string.rooted_on,
|
onoffSummary(key, R.string.rooted_on,
|
||||||
R.string.rooted_off);
|
R.string.rooted_off);
|
||||||
if (changing) {
|
|
||||||
result ^= RESULT_REFILTER;
|
|
||||||
setResult(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (key.equals(Preferences.PREF_IGN_TOUCH)) {
|
} else if (key.equals(Preferences.PREF_IGN_TOUCH)) {
|
||||||
onoffSummary(key, R.string.ignoreTouch_on,
|
onoffSummary(key, R.string.ignoreTouch_on,
|
||||||
@ -150,8 +144,6 @@ public class PreferencesActivity extends PreferenceActivity implements
|
|||||||
onoffSummary(key, R.string.expert_on,
|
onoffSummary(key, R.string.expert_on,
|
||||||
R.string.expert_off);
|
R.string.expert_off);
|
||||||
|
|
||||||
} else if (key.equals(Preferences.PREF_DB_SYNC)) {
|
|
||||||
entrySummary(key);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,8 @@
|
|||||||
package org.fdroid.fdroid;
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
import android.os.Bundle;
|
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.data.Repo;
|
||||||
import org.fdroid.fdroid.updater.RepoUpdater;
|
import org.fdroid.fdroid.updater.RepoUpdater;
|
||||||
import org.xml.sax.Attributes;
|
import org.xml.sax.Attributes;
|
||||||
@ -27,20 +29,18 @@ import org.xml.sax.SAXException;
|
|||||||
import org.xml.sax.helpers.DefaultHandler;
|
import org.xml.sax.helpers.DefaultHandler;
|
||||||
|
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class RepoXMLHandler extends DefaultHandler {
|
public class RepoXMLHandler extends DefaultHandler {
|
||||||
|
|
||||||
// The repo we're processing.
|
// The repo we're processing.
|
||||||
private Repo repo;
|
private Repo repo;
|
||||||
|
|
||||||
private Map<String, DB.App> apps;
|
private List<App> apps = new ArrayList<App>();
|
||||||
private List<DB.App> appsList;
|
private List<Apk> apksList = new ArrayList<Apk>();
|
||||||
|
|
||||||
private DB.App curapp = null;
|
private App curapp = null;
|
||||||
private DB.Apk curapk = null;
|
private Apk curapk = null;
|
||||||
private StringBuilder curchars = new StringBuilder();
|
private StringBuilder curchars = new StringBuilder();
|
||||||
|
|
||||||
// After processing the XML, these will be -1 if the index didn't specify
|
// 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;
|
private int totalAppCount;
|
||||||
|
|
||||||
public RepoXMLHandler(Repo repo, List<DB.App> appsList, ProgressListener listener) {
|
public RepoXMLHandler(Repo repo, ProgressListener listener) {
|
||||||
this.repo = repo;
|
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;
|
pubkey = null;
|
||||||
name = null;
|
name = null;
|
||||||
description = null;
|
description = null;
|
||||||
progressListener = listener;
|
progressListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<App> getApps() {
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Apk> getApks() {
|
||||||
|
return apksList;
|
||||||
|
}
|
||||||
|
|
||||||
public int getMaxAge() { return maxage; }
|
public int getMaxAge() { return maxage; }
|
||||||
|
|
||||||
public int getVersion() { return version; }
|
public int getVersion() { return version; }
|
||||||
@ -103,21 +108,18 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (curel.equals("application") && curapp != null) {
|
if (curel.equals("application") && curapp != null) {
|
||||||
|
apps.add(curapp);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
curapp = null;
|
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) {
|
} else if (curel.equals("package") && curapk != null && curapp != null) {
|
||||||
curapp.apks.add(curapk);
|
apksList.add(curapk);
|
||||||
curapk = null;
|
curapk = null;
|
||||||
} else if (curapk != null && str != null) {
|
} else if (curapk != null && str != null) {
|
||||||
if (curel.equals("version")) {
|
if (curel.equals("version")) {
|
||||||
@ -130,19 +132,19 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
}
|
}
|
||||||
} else if (curel.equals("size")) {
|
} else if (curel.equals("size")) {
|
||||||
try {
|
try {
|
||||||
curapk.detail_size = Integer.parseInt(str);
|
curapk.size = Integer.parseInt(str);
|
||||||
} catch (NumberFormatException ex) {
|
} catch (NumberFormatException ex) {
|
||||||
curapk.detail_size = 0;
|
curapk.size = 0;
|
||||||
}
|
}
|
||||||
} else if (curel.equals("hash")) {
|
} else if (curel.equals("hash")) {
|
||||||
if (hashType == null || hashType.equals("md5")) {
|
if (hashType == null || hashType.equals("md5")) {
|
||||||
if (curapk.detail_hash == null) {
|
if (curapk.hash == null) {
|
||||||
curapk.detail_hash = str;
|
curapk.hash = str;
|
||||||
curapk.detail_hashType = "MD5";
|
curapk.hashType = "MD5";
|
||||||
}
|
}
|
||||||
} else if (hashType.equals("sha256")) {
|
} else if (hashType.equals("sha256")) {
|
||||||
curapk.detail_hash = str;
|
curapk.hash = str;
|
||||||
curapk.detail_hashType = "SHA-256";
|
curapk.hashType = "SHA-256";
|
||||||
}
|
}
|
||||||
} else if (curel.equals("sig")) {
|
} else if (curel.equals("sig")) {
|
||||||
curapk.sig = str;
|
curapk.sig = str;
|
||||||
@ -158,17 +160,17 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
}
|
}
|
||||||
} else if (curel.equals("added")) {
|
} else if (curel.equals("added")) {
|
||||||
try {
|
try {
|
||||||
curapk.added = str.length() == 0 ? null : DB.DATE_FORMAT
|
curapk.added = str.length() == 0 ? null : Utils.DATE_FORMAT
|
||||||
.parse(str);
|
.parse(str);
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
curapk.added = null;
|
curapk.added = null;
|
||||||
}
|
}
|
||||||
} else if (curel.equals("permissions")) {
|
} else if (curel.equals("permissions")) {
|
||||||
curapk.detail_permissions = DB.CommaSeparatedList.make(str);
|
curapk.permissions = Utils.CommaSeparatedList.make(str);
|
||||||
} else if (curel.equals("features")) {
|
} else if (curel.equals("features")) {
|
||||||
curapk.features = DB.CommaSeparatedList.make(str);
|
curapk.features = Utils.CommaSeparatedList.make(str);
|
||||||
} else if (curel.equals("nativecode")) {
|
} else if (curel.equals("nativecode")) {
|
||||||
curapk.nativecode = DB.CommaSeparatedList.make(str);
|
curapk.nativecode = Utils.CommaSeparatedList.make(str);
|
||||||
}
|
}
|
||||||
} else if (curapp != null && str != null) {
|
} else if (curapp != null && str != null) {
|
||||||
if (curel.equals("name")) {
|
if (curel.equals("name")) {
|
||||||
@ -179,33 +181,33 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
// This is the old-style description. We'll read it
|
// This is the old-style description. We'll read it
|
||||||
// if present, to support old repos, but in newer
|
// if present, to support old repos, but in newer
|
||||||
// repos it will get overwritten straight away!
|
// repos it will get overwritten straight away!
|
||||||
curapp.detail_description = "<p>" + str + "</p>";
|
curapp.description = "<p>" + str + "</p>";
|
||||||
} else if (curel.equals("desc")) {
|
} else if (curel.equals("desc")) {
|
||||||
// New-style description.
|
// New-style description.
|
||||||
curapp.detail_description = str;
|
curapp.description = str;
|
||||||
} else if (curel.equals("summary")) {
|
} else if (curel.equals("summary")) {
|
||||||
curapp.summary = str;
|
curapp.summary = str;
|
||||||
} else if (curel.equals("license")) {
|
} else if (curel.equals("license")) {
|
||||||
curapp.license = str;
|
curapp.license = str;
|
||||||
} else if (curel.equals("source")) {
|
} else if (curel.equals("source")) {
|
||||||
curapp.detail_sourceURL = str;
|
curapp.sourceURL = str;
|
||||||
} else if (curel.equals("donate")) {
|
} else if (curel.equals("donate")) {
|
||||||
curapp.detail_donateURL = str;
|
curapp.donateURL = str;
|
||||||
} else if (curel.equals("bitcoin")) {
|
} else if (curel.equals("bitcoin")) {
|
||||||
curapp.detail_bitcoinAddr = str;
|
curapp.bitcoinAddr = str;
|
||||||
} else if (curel.equals("litecoin")) {
|
} else if (curel.equals("litecoin")) {
|
||||||
curapp.detail_litecoinAddr = str;
|
curapp.litecoinAddr = str;
|
||||||
} else if (curel.equals("dogecoin")) {
|
} else if (curel.equals("dogecoin")) {
|
||||||
curapp.detail_dogecoinAddr = str;
|
curapp.dogecoinAddr = str;
|
||||||
} else if (curel.equals("flattr")) {
|
} else if (curel.equals("flattr")) {
|
||||||
curapp.detail_flattrID = str;
|
curapp.flattrID = str;
|
||||||
} else if (curel.equals("web")) {
|
} else if (curel.equals("web")) {
|
||||||
curapp.detail_webURL = str;
|
curapp.webURL = str;
|
||||||
} else if (curel.equals("tracker")) {
|
} else if (curel.equals("tracker")) {
|
||||||
curapp.detail_trackerURL = str;
|
curapp.trackerURL = str;
|
||||||
} else if (curel.equals("added")) {
|
} else if (curel.equals("added")) {
|
||||||
try {
|
try {
|
||||||
curapp.added = str.length() == 0 ? null : DB.DATE_FORMAT
|
curapp.added = str.length() == 0 ? null : Utils.DATE_FORMAT
|
||||||
.parse(str);
|
.parse(str);
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
curapp.added = null;
|
curapp.added = null;
|
||||||
@ -213,7 +215,7 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
} else if (curel.equals("lastupdated")) {
|
} else if (curel.equals("lastupdated")) {
|
||||||
try {
|
try {
|
||||||
curapp.lastUpdated = str.length() == 0 ? null
|
curapp.lastUpdated = str.length() == 0 ? null
|
||||||
: DB.DATE_FORMAT.parse(str);
|
: Utils.DATE_FORMAT.parse(str);
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
curapp.lastUpdated = null;
|
curapp.lastUpdated = null;
|
||||||
}
|
}
|
||||||
@ -226,11 +228,11 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
curapp.curVercode = -1;
|
curapp.curVercode = -1;
|
||||||
}
|
}
|
||||||
} else if (curel.equals("categories")) {
|
} else if (curel.equals("categories")) {
|
||||||
curapp.categories = DB.CommaSeparatedList.make(str);
|
curapp.categories = Utils.CommaSeparatedList.make(str);
|
||||||
} else if (curel.equals("antifeatures")) {
|
} else if (curel.equals("antifeatures")) {
|
||||||
curapp.antiFeatures = DB.CommaSeparatedList.make(str);
|
curapp.antiFeatures = Utils.CommaSeparatedList.make(str);
|
||||||
} else if (curel.equals("requirements")) {
|
} else if (curel.equals("requirements")) {
|
||||||
curapp.requirements = DB.CommaSeparatedList.make(str);
|
curapp.requirements = Utils.CommaSeparatedList.make(str);
|
||||||
}
|
}
|
||||||
} else if (curel.equals("description")) {
|
} else if (curel.equals("description")) {
|
||||||
description = str;
|
description = str;
|
||||||
@ -269,8 +271,7 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
description = dc;
|
description = dc;
|
||||||
|
|
||||||
} else if (localName.equals("application") && curapp == null) {
|
} else if (localName.equals("application") && curapp == null) {
|
||||||
curapp = new DB.App();
|
curapp = new App();
|
||||||
curapp.detail_Populated = true;
|
|
||||||
curapp.id = attributes.getValue("", "id");
|
curapp.id = attributes.getValue("", "id");
|
||||||
Bundle progressData = RepoUpdater.createProgressData(repo.address);
|
Bundle progressData = RepoUpdater.createProgressData(repo.address);
|
||||||
progressCounter ++;
|
progressCounter ++;
|
||||||
@ -280,7 +281,7 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
totalAppCount, progressData));
|
totalAppCount, progressData));
|
||||||
|
|
||||||
} else if (localName.equals("package") && curapp != null && curapk == null) {
|
} else if (localName.equals("package") && curapp != null && curapk == null) {
|
||||||
curapk = new DB.Apk();
|
curapk = new Apk();
|
||||||
curapk.id = curapp.id;
|
curapk.id = curapp.id;
|
||||||
curapk.repo = repo.getId();
|
curapk.repo = repo.getId();
|
||||||
hashType = null;
|
hashType = null;
|
||||||
|
@ -18,12 +18,10 @@
|
|||||||
|
|
||||||
package org.fdroid.fdroid;
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import android.app.ListActivity;
|
import android.app.ListActivity;
|
||||||
import android.app.SearchManager;
|
import android.app.SearchManager;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@ -37,8 +35,11 @@ import android.support.v4.app.NavUtils;
|
|||||||
import android.support.v4.view.MenuItemCompat;
|
import android.support.v4.view.MenuItemCompat;
|
||||||
|
|
||||||
import org.fdroid.fdroid.compat.ActionBarCompat;
|
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.AppListAdapter;
|
||||||
import org.fdroid.fdroid.views.AvailableAppListAdapter;
|
import org.fdroid.fdroid.views.AvailableAppListAdapter;
|
||||||
|
import org.fdroid.fdroid.views.fragments.AppListFragment;
|
||||||
|
|
||||||
public class SearchResults extends ListActivity {
|
public class SearchResults extends ListActivity {
|
||||||
|
|
||||||
@ -46,7 +47,7 @@ public class SearchResults extends ListActivity {
|
|||||||
|
|
||||||
private static final int SEARCH = Menu.FIRST;
|
private static final int SEARCH = Menu.FIRST;
|
||||||
|
|
||||||
private AppListAdapter applist;
|
private AppListAdapter adapter;
|
||||||
|
|
||||||
protected String getQuery() {
|
protected String getQuery() {
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
@ -73,7 +74,6 @@ public class SearchResults extends ListActivity {
|
|||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true);
|
ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true);
|
||||||
applist = new AvailableAppListAdapter(this);
|
|
||||||
setContentView(R.layout.searchresults);
|
setContentView(R.layout.searchresults);
|
||||||
|
|
||||||
// Start a search by just typing
|
// Start a search by just typing
|
||||||
@ -102,53 +102,32 @@ public class SearchResults extends ListActivity {
|
|||||||
if (query == null || query.length() == 0)
|
if (query == null || query.length() == 0)
|
||||||
finish();
|
finish();
|
||||||
|
|
||||||
List<String> matchingids = new ArrayList<String>();
|
Cursor cursor = getContentResolver().query(
|
||||||
try {
|
AppProvider.getSearchUri(query), AppListFragment.APP_PROJECTION,
|
||||||
DB db = DB.getDB();
|
null, null, AppListFragment.APP_SORT);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextView tv = (TextView) findViewById(R.id.description);
|
TextView tv = (TextView) findViewById(R.id.description);
|
||||||
String headertext;
|
String headertext;
|
||||||
if (apps.size() == 0) {
|
int count = cursor != null ? cursor.getCount() : 0;
|
||||||
|
if (count == 0) {
|
||||||
headertext = getString(R.string.searchres_noapps, query);
|
headertext = getString(R.string.searchres_noapps, query);
|
||||||
} else if (apps.size() == 1) {
|
} else if (count == 1) {
|
||||||
headertext = getString(R.string.searchres_oneapp, query);
|
headertext = getString(R.string.searchres_oneapp, query);
|
||||||
} else {
|
} else {
|
||||||
headertext = getString(R.string.searchres_napps, apps.size(), query);
|
headertext = getString(R.string.searchres_napps, count, query);
|
||||||
}
|
}
|
||||||
tv.setText(headertext);
|
tv.setText(headertext);
|
||||||
Log.d("FDroid", "Search for '" + query + "' returned " + apps.size()
|
Log.d("FDroid", "Search for '" + query + "' returned " + count + " results");
|
||||||
+ " results");
|
|
||||||
applist.clear();
|
|
||||||
for (DB.App app : apps) {
|
|
||||||
applist.addItem(app);
|
|
||||||
}
|
|
||||||
getListView().setFastScrollEnabled(true);
|
|
||||||
applist.notifyDataSetChanged();
|
|
||||||
setListAdapter(applist);
|
|
||||||
|
|
||||||
|
adapter = new AvailableAppListAdapter(this, cursor);
|
||||||
|
getListView().setFastScrollEnabled(true);
|
||||||
|
setListAdapter(adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onListItemClick(ListView l, View v, int position, long id) {
|
protected void onListItemClick(ListView l, View v, int position, long id) {
|
||||||
final DB.App app;
|
final App app;
|
||||||
app = (DB.App) applist.getItem(position);
|
app = new App((Cursor) adapter.getItem(position));
|
||||||
|
|
||||||
Intent intent = new Intent(this, AppDetails.class);
|
Intent intent = new Intent(this, AppDetails.class);
|
||||||
intent.putExtra("appid", app.id);
|
intent.putExtra("appid", app.id);
|
||||||
|
@ -18,41 +18,26 @@
|
|||||||
|
|
||||||
package org.fdroid.fdroid;
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
|
|
||||||
import android.app.AlarmManager;
|
import android.app.*;
|
||||||
import android.app.IntentService;
|
import android.content.*;
|
||||||
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.content.SharedPreferences.Editor;
|
import android.content.SharedPreferences.Editor;
|
||||||
|
import android.database.Cursor;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.NetworkInfo;
|
import android.net.NetworkInfo;
|
||||||
import android.os.Build;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.*;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
import android.os.ResultReceiver;
|
|
||||||
import android.os.SystemClock;
|
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
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.NotificationCompat;
|
||||||
import android.support.v4.app.TaskStackBuilder;
|
import android.support.v4.app.TaskStackBuilder;
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.widget.Toast;
|
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 class UpdateService extends IntentService implements ProgressListener {
|
||||||
|
|
||||||
public static final String RESULT_MESSAGE = "msg";
|
public static final String RESULT_MESSAGE = "msg";
|
||||||
@ -202,16 +187,6 @@ public class UpdateService extends IntentService implements ProgressListener {
|
|||||||
return receiver == null;
|
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
|
@Override
|
||||||
protected void onHandleIntent(Intent intent) {
|
protected void onHandleIntent(Intent intent) {
|
||||||
|
|
||||||
@ -255,17 +230,110 @@ public class UpdateService extends IntentService implements ProgressListener {
|
|||||||
} else {
|
} else {
|
||||||
Log.d("FDroid", "Unscheduled (manually requested) update");
|
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();
|
Editor e = prefs.edit();
|
||||||
e.putLong(Preferences.PREF_UPD_LAST, System.currentTimeMillis());
|
e.putLong(Preferences.PREF_UPD_LAST, System.currentTimeMillis());
|
||||||
e.commit();
|
e.commit();
|
||||||
|
if (changes) {
|
||||||
|
sendStatus(STATUS_COMPLETE_WITH_CHANGES);
|
||||||
|
} else {
|
||||||
|
sendStatus(STATUS_COMPLETE_AND_SAME);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e("FDroid",
|
Log.e("FDroid",
|
||||||
"Exception during update processing:\n"
|
"Exception during update processing:\n"
|
||||||
+ Log.getStackTraceString(e));
|
+ Log.getStackTraceString(e));
|
||||||
if (TextUtils.isEmpty(errmsg))
|
if (errmsg.length() == 0)
|
||||||
errmsg = "Unknown error";
|
errmsg = "Unknown error";
|
||||||
sendStatus(STATUS_ERROR, errmsg);
|
sendStatus(STATUS_ERROR, errmsg);
|
||||||
} finally {
|
} finally {
|
||||||
@ -276,147 +344,112 @@ public class UpdateService extends IntentService implements ProgressListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String updateRepos(String address) throws Exception {
|
private void notifyContentProviders() {
|
||||||
SharedPreferences prefs = PreferenceManager
|
getContentResolver().notifyChange(AppProvider.getContentUri(), null);
|
||||||
.getDefaultSharedPreferences(getBaseContext());
|
getContentResolver().notifyChange(ApkProvider.getContentUri(), null);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>();
|
* Get the current version - this will be one of the Apks from 'apks'.
|
||||||
Set<Long> keeprepos = new TreeSet<Long>();
|
* Can return null if there are no available versions.
|
||||||
boolean changes = false;
|
* This should be the 'current' version, as in the most recent stable
|
||||||
boolean update;
|
* 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) {
|
for (Repo repo : repos) {
|
||||||
if (!repo.inuse)
|
if (repo.getId() != apk.repo) continue;
|
||||||
continue;
|
if (repo.version >= Repo.VERSION_DENSITY_SPECIFIC_ICONS) {
|
||||||
// are we updating all repos, or just one?
|
app.iconUrl = repo.address + iconsDir + app.icon;
|
||||||
if (TextUtils.isEmpty(address)) {
|
|
||||||
update = true;
|
|
||||||
} else {
|
} else {
|
||||||
// if only updating one repo, mark the rest as keepers
|
app.iconUrl = repo.address + "/icons/" + app.icon;
|
||||||
if (address.equals(repo.address)) {
|
}
|
||||||
update = true;
|
return;
|
||||||
} else {
|
|
||||||
keeprepos.add(repo.getId());
|
|
||||||
update = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 {
|
private void showAppUpdatesNotification(int updates) throws Exception {
|
||||||
@ -446,6 +479,209 @@ public class UpdateService extends IntentService implements ProgressListener {
|
|||||||
nm.notify(1, builder.build());
|
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
|
* Received progress event from the RepoXMLHandler. It could be progress
|
||||||
* downloading from the repo, or perhaps processing the info from the repo.
|
* 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.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 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.text.SimpleDateFormat;
|
||||||
import java.util.Locale;
|
import java.security.MessageDigest;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import org.fdroid.fdroid.data.Repo;
|
||||||
|
|
||||||
public final class Utils {
|
public final class Utils {
|
||||||
|
|
||||||
public static final int BUFFER_SIZE = 4096;
|
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 = {
|
private static final String[] FRIENDLY_SIZE_FORMAT = {
|
||||||
"%.0f B", "%.0f KiB", "%.1f MiB", "%.2f GiB" };
|
"%.0f B", "%.0f KiB", "%.1f MiB", "%.2f GiB" };
|
||||||
|
|
||||||
public static final SimpleDateFormat LOG_DATE_FORMAT =
|
public static final SimpleDateFormat LOG_DATE_FORMAT =
|
||||||
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
|
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)
|
public static void copy(InputStream input, OutputStream output)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
copy(input, output, null, null);
|
copy(input, output, null, null);
|
||||||
@ -158,4 +196,145 @@ public final class Utils {
|
|||||||
return apkCacheDir;
|
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.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import org.fdroid.fdroid.DB;
|
import org.fdroid.fdroid.*;
|
||||||
import org.fdroid.fdroid.R;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -18,6 +19,12 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
|
|
||||||
public static final String TABLE_REPO = "fdroid_repo";
|
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 "
|
private static final String CREATE_TABLE_REPO = "create table "
|
||||||
+ TABLE_REPO + " (_id integer primary key, "
|
+ TABLE_REPO + " (_id integer primary key, "
|
||||||
+ "address text not null, "
|
+ "address text not null, "
|
||||||
@ -27,33 +34,59 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
+ "version integer not null default 0, "
|
+ "version integer not null default 0, "
|
||||||
+ "lastetag text, lastUpdated string);";
|
+ "lastetag text, lastUpdated string);";
|
||||||
|
|
||||||
private static final String CREATE_TABLE_APK = "create table " + DB.TABLE_APK
|
private static final String CREATE_TABLE_APK =
|
||||||
+ " ( " + "id text not null, " + "version text not null, "
|
"CREATE TABLE " + TABLE_APK + " ( "
|
||||||
+ "repo integer not null, " + "hash text not null, "
|
+ "id text not null, "
|
||||||
+ "vercode int not null," + "apkName text not null, "
|
+ "version text not null, "
|
||||||
+ "size int not null," + "sig string," + "srcname string,"
|
+ "repo integer not null, "
|
||||||
+ "minSdkVersion integer," + "permissions string,"
|
+ "hash text not null, "
|
||||||
+ "features string," + "nativecode string,"
|
+ "vercode int not null,"
|
||||||
+ "hashType string," + "added string,"
|
+ "apkName text not null, "
|
||||||
+ "compatible int not null," + "primary key(id,vercode));";
|
+ "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
|
public static final String TABLE_APP = "fdroid_app";
|
||||||
+ " ( " + "id text not null, " + "name text not null, "
|
private static final String CREATE_TABLE_APP = "CREATE TABLE " + TABLE_APP
|
||||||
+ "summary text not null, " + "icon text, "
|
+ " ( "
|
||||||
+ "description text not null, " + "license text not null, "
|
+ "id text not null, "
|
||||||
+ "webURL text, " + "trackerURL text, " + "sourceURL text, "
|
+ "name text not null, "
|
||||||
+ "curVersion text," + "curVercode integer,"
|
+ "summary text not null, "
|
||||||
+ "antiFeatures string," + "donateURL string,"
|
+ "icon text, "
|
||||||
+ "bitcoinAddr string," + "litecoinAddr string,"
|
+ "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,"
|
+ "dogecoinAddr string,"
|
||||||
+ "flattrID string," + "requirements string,"
|
+ "flattrID string,"
|
||||||
+ "categories string," + "added string,"
|
+ "requirements string,"
|
||||||
+ "lastUpdated string," + "compatible int not null,"
|
+ "categories string,"
|
||||||
|
+ "added string,"
|
||||||
|
+ "lastUpdated string,"
|
||||||
|
+ "compatible int not null,"
|
||||||
+ "ignoreAllUpdates int not null,"
|
+ "ignoreAllUpdates int not null,"
|
||||||
+ "ignoreThisUpdate int not null,"
|
+ "ignoreThisUpdate int not null,"
|
||||||
|
+ "iconUrl text, "
|
||||||
+ "primary key(id));";
|
+ "primary key(id));";
|
||||||
|
|
||||||
private static final int DB_VERSION = 37;
|
private static final int DB_VERSION = 39;
|
||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@ -64,18 +97,20 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
|
|
||||||
private void populateRepoNames(SQLiteDatabase db, int oldVersion) {
|
private void populateRepoNames(SQLiteDatabase db, int oldVersion) {
|
||||||
if (oldVersion < 37) {
|
if (oldVersion < 37) {
|
||||||
|
Log.i("FDroid", "Populating repo names from the url");
|
||||||
String[] columns = { "address", "_id" };
|
String[] columns = { "address", "_id" };
|
||||||
Cursor cursor = db.query(TABLE_REPO, columns,
|
Cursor cursor = db.query(TABLE_REPO, columns,
|
||||||
"name IS NULL OR name = ''", null, null, null, null);
|
"name IS NULL OR name = ''", null, null, null, null);
|
||||||
cursor.moveToFirst();
|
|
||||||
if (cursor.getCount() > 0) {
|
if (cursor.getCount() > 0) {
|
||||||
cursor.moveToFirst();
|
cursor.moveToFirst();
|
||||||
while (!cursor.isAfterLast()) {
|
while (!cursor.isAfterLast()) {
|
||||||
String address = cursor.getString(0);
|
String address = cursor.getString(0);
|
||||||
long id = cursor.getInt(1);
|
long id = cursor.getInt(1);
|
||||||
ContentValues values = new ContentValues(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 ) };
|
String[] args = { Long.toString( id ) };
|
||||||
|
Log.i("FDroid", "Setting repo name to '" + name + "' for repo " + address);
|
||||||
db.update(TABLE_REPO, values, "_id = ?", args);
|
db.update(TABLE_REPO, values, "_id = ?", args);
|
||||||
cursor.moveToNext();
|
cursor.moveToNext();
|
||||||
}
|
}
|
||||||
@ -87,7 +122,6 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
if (oldVersion < 36) {
|
if (oldVersion < 36) {
|
||||||
|
|
||||||
Log.d("FDroid", "Renaming " + TABLE_REPO + ".id to _id");
|
Log.d("FDroid", "Renaming " + TABLE_REPO + ".id to _id");
|
||||||
|
|
||||||
db.beginTransaction();
|
db.beginTransaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -150,7 +184,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
context.getString(R.string.default_repo_description));
|
context.getString(R.string.default_repo_description));
|
||||||
values.put("version", 0);
|
values.put("version", 0);
|
||||||
String pubkey = context.getString(R.string.default_repo_pubkey);
|
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("pubkey", pubkey);
|
||||||
values.put("fingerprint", fingerprint);
|
values.put("fingerprint", fingerprint);
|
||||||
values.put("maxage", 0);
|
values.put("maxage", 0);
|
||||||
@ -280,7 +314,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
c.close();
|
c.close();
|
||||||
for (Repo repo : oldrepos) {
|
for (Repo repo : oldrepos) {
|
||||||
ContentValues values = new ContentValues();
|
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 });
|
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) {
|
private void addLastUpdatedToRepo(SQLiteDatabase db, int oldVersion) {
|
||||||
if (oldVersion < 35 && !columnExists(db, TABLE_REPO, "lastUpdated")) {
|
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");
|
db.execSQL("Alter table " + TABLE_REPO + " add column lastUpdated string");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -307,18 +342,18 @@ public class DBHelper extends SQLiteOpenHelper {
|
|||||||
private void resetTransient(SQLiteDatabase db) {
|
private void resetTransient(SQLiteDatabase db) {
|
||||||
context.getSharedPreferences("FDroid", Context.MODE_PRIVATE).edit()
|
context.getSharedPreferences("FDroid", Context.MODE_PRIVATE).edit()
|
||||||
.putBoolean("triedEmptyUpdate", false).commit();
|
.putBoolean("triedEmptyUpdate", false).commit();
|
||||||
db.execSQL("drop table " + DB.TABLE_APP);
|
db.execSQL("drop table " + TABLE_APP);
|
||||||
db.execSQL("drop table " + DB.TABLE_APK);
|
db.execSQL("drop table " + TABLE_APK);
|
||||||
db.execSQL("update " + TABLE_REPO + " set lastetag = NULL");
|
db.execSQL("update " + TABLE_REPO + " set lastetag = NULL");
|
||||||
createAppApk(db);
|
createAppApk(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void createAppApk(SQLiteDatabase db) {
|
private static void createAppApk(SQLiteDatabase db) {
|
||||||
db.execSQL(CREATE_TABLE_APP);
|
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_TABLE_APK);
|
||||||
db.execSQL("create index apk_vercode on " + DB.TABLE_APK + " (vercode);");
|
db.execSQL("create index apk_vercode on " + TABLE_APK + " (vercode);");
|
||||||
db.execSQL("create index apk_id on " + DB.TABLE_APK + " (id);");
|
db.execSQL("create index apk_id on " + TABLE_APK + " (id);");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean columnExists(SQLiteDatabase db,
|
private static boolean columnExists(SQLiteDatabase db,
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package org.fdroid.fdroid.data;
|
package org.fdroid.fdroid.data;
|
||||||
|
|
||||||
import android.content.ContentProvider;
|
import android.content.*;
|
||||||
import android.content.UriMatcher;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.net.Uri;
|
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";
|
public static final String AUTHORITY = "org.fdroid.fdroid.data";
|
||||||
|
|
||||||
@ -14,10 +15,39 @@ abstract class FDroidProvider extends ContentProvider {
|
|||||||
|
|
||||||
private DBHelper dbHelper;
|
private DBHelper dbHelper;
|
||||||
|
|
||||||
|
private boolean isApplyingBatch = false;
|
||||||
|
|
||||||
abstract protected String getTableName();
|
abstract protected String getTableName();
|
||||||
|
|
||||||
abstract protected String getProviderName();
|
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
|
@Override
|
||||||
public boolean onCreate() {
|
public boolean onCreate() {
|
||||||
dbHelper = new DBHelper(getContext());
|
dbHelper = new DBHelper(getContext());
|
||||||
@ -55,5 +85,15 @@ abstract class FDroidProvider extends ContentProvider {
|
|||||||
|
|
||||||
abstract protected UriMatcher getMatcher();
|
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.content.ContentValues;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import org.fdroid.fdroid.DB;
|
import org.fdroid.fdroid.Utils;
|
||||||
|
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
public class Repo {
|
public class Repo extends ValueObject {
|
||||||
|
|
||||||
|
public static final int VERSION_DENSITY_SPECIFIC_ICONS = 11;
|
||||||
|
|
||||||
private long id;
|
private long id;
|
||||||
|
|
||||||
@ -31,6 +33,9 @@ public class Repo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Repo(Cursor cursor) {
|
public Repo(Cursor cursor) {
|
||||||
|
|
||||||
|
checkCursorPosition(cursor);
|
||||||
|
|
||||||
for(int i = 0; i < cursor.getColumnCount(); i ++ ) {
|
for(int i = 0; i < cursor.getColumnCount(); i ++ ) {
|
||||||
String column = cursor.getColumnName(i);
|
String column = cursor.getColumnName(i);
|
||||||
if (column.equals(RepoProvider.DataColumns._ID)) {
|
if (column.equals(RepoProvider.DataColumns._ID)) {
|
||||||
@ -46,14 +51,7 @@ public class Repo {
|
|||||||
} else if (column.equals(RepoProvider.DataColumns.IN_USE)) {
|
} else if (column.equals(RepoProvider.DataColumns.IN_USE)) {
|
||||||
inuse = cursor.getInt(i) == 1;
|
inuse = cursor.getInt(i) == 1;
|
||||||
} else if (column.equals(RepoProvider.DataColumns.LAST_UPDATED)) {
|
} else if (column.equals(RepoProvider.DataColumns.LAST_UPDATED)) {
|
||||||
String dateString = cursor.getString(i);
|
lastUpdated = toDate(cursor.getString(i));
|
||||||
if (dateString != null) {
|
|
||||||
try {
|
|
||||||
lastUpdated = DB.DATE_FORMAT.parse(dateString);
|
|
||||||
} catch (ParseException e) {
|
|
||||||
Log.e("FDroid", "Error parsing date " + dateString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (column.equals(RepoProvider.DataColumns.MAX_AGE)) {
|
} else if (column.equals(RepoProvider.DataColumns.MAX_AGE)) {
|
||||||
maxage = cursor.getInt(i);
|
maxage = cursor.getInt(i);
|
||||||
} else if (column.equals(RepoProvider.DataColumns.VERSION)) {
|
} else if (column.equals(RepoProvider.DataColumns.VERSION)) {
|
||||||
@ -78,13 +76,6 @@ public class Repo {
|
|||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getNumberOfApps() {
|
|
||||||
DB db = DB.getDB();
|
|
||||||
int count = db.countAppsForRepo(id);
|
|
||||||
DB.releaseDB();
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSigned() {
|
public boolean isSigned() {
|
||||||
return this.pubkey != null && this.pubkey.length() > 0;
|
return this.pubkey != null && this.pubkey.length() > 0;
|
||||||
}
|
}
|
||||||
@ -146,7 +137,7 @@ public class Repo {
|
|||||||
String dateString = values.getAsString(RepoProvider.DataColumns.LAST_UPDATED);
|
String dateString = values.getAsString(RepoProvider.DataColumns.LAST_UPDATED);
|
||||||
if (dateString != null) {
|
if (dateString != null) {
|
||||||
try {
|
try {
|
||||||
lastUpdated = DB.DATE_FORMAT.parse(dateString);
|
lastUpdated = Utils.DATE_FORMAT.parse(dateString);
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
Log.e("FDroid", "Error parsing date " + dateString);
|
Log.e("FDroid", "Error parsing date " + dateString);
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,8 @@ import android.net.Uri;
|
|||||||
import android.provider.BaseColumns;
|
import android.provider.BaseColumns;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import org.fdroid.fdroid.DB;
|
|
||||||
import org.fdroid.fdroid.FDroidApp;
|
import org.fdroid.fdroid.FDroidApp;
|
||||||
|
import org.fdroid.fdroid.Utils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -15,6 +15,7 @@ import java.util.List;
|
|||||||
public class RepoProvider extends FDroidProvider {
|
public class RepoProvider extends FDroidProvider {
|
||||||
|
|
||||||
public static final class Helper {
|
public static final class Helper {
|
||||||
|
|
||||||
public static final String TAG = "RepoProvider.Helper";
|
public static final String TAG = "RepoProvider.Helper";
|
||||||
|
|
||||||
private Helper() {}
|
private Helper() {}
|
||||||
@ -76,6 +77,7 @@ public class RepoProvider extends FDroidProvider {
|
|||||||
repos.add(new Repo(cursor));
|
repos.add(new Repo(cursor));
|
||||||
cursor.moveToNext();
|
cursor.moveToNext();
|
||||||
}
|
}
|
||||||
|
cursor.close();
|
||||||
}
|
}
|
||||||
return repos;
|
return repos;
|
||||||
}
|
}
|
||||||
@ -104,7 +106,7 @@ public class RepoProvider extends FDroidProvider {
|
|||||||
*/
|
*/
|
||||||
if (values.containsKey(DataColumns.PUBLIC_KEY)) {
|
if (values.containsKey(DataColumns.PUBLIC_KEY)) {
|
||||||
String publicKey = values.getAsString(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)) {
|
if (values.containsKey(DataColumns.FINGERPRINT)) {
|
||||||
String fingerprint = values.getAsString(DataColumns.FINGERPRINT);
|
String fingerprint = values.getAsString(DataColumns.FINGERPRINT);
|
||||||
if (!TextUtils.isEmpty(publicKey)) {
|
if (!TextUtils.isEmpty(publicKey)) {
|
||||||
@ -152,17 +154,32 @@ public class RepoProvider extends FDroidProvider {
|
|||||||
resolver.delete(uri, null, null);
|
resolver.delete(uri, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void purgeApps(Repo repo, FDroidApp app) {
|
public static void purgeApps(Context context, Repo repo, FDroidApp app) {
|
||||||
// TODO: Once we have content providers for apps and apks, use them
|
Uri apkUri = ApkProvider.getRepoUri(repo.getId());
|
||||||
// to do this...
|
int apkCount = context.getContentResolver().delete(apkUri, null, null);
|
||||||
DB db = DB.getDB();
|
Log.d("FDroid", "Removed " + apkCount + " apks from repo " + repo.name);
|
||||||
try {
|
|
||||||
db.purgeApps(repo, app);
|
Uri appUri = AppProvider.getNoApksUri();
|
||||||
} finally {
|
int appCount = context.getContentResolver().delete(appUri, null, null);
|
||||||
DB.releaseDB();
|
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 {
|
public interface DataColumns extends BaseColumns {
|
||||||
@ -189,12 +206,12 @@ public class RepoProvider extends FDroidProvider {
|
|||||||
private static final UriMatcher matcher = new UriMatcher(-1);
|
private static final UriMatcher matcher = new UriMatcher(-1);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
matcher.addURI(AUTHORITY, PROVIDER_NAME, CODE_LIST);
|
matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, null, CODE_LIST);
|
||||||
matcher.addURI(AUTHORITY, PROVIDER_NAME + "/#", CODE_SINGLE);
|
matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, "#", CODE_SINGLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Uri getContentUri() {
|
public static Uri getContentUri() {
|
||||||
return Uri.parse("content://" + AUTHORITY + "/" + PROVIDER_NAME);
|
return Uri.parse("content://" + AUTHORITY + "." + PROVIDER_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Uri getContentUri(long repoId) {
|
public static Uri getContentUri(long repoId) {
|
||||||
@ -226,8 +243,8 @@ public class RepoProvider extends FDroidProvider {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case CODE_SINGLE:
|
case CODE_SINGLE:
|
||||||
selection = ( selection == null ? "" : selection ) +
|
selection = ( selection == null ? "" : selection + " AND " ) +
|
||||||
"_ID = " + uri.getLastPathSegment();
|
DataColumns._ID + " = " + uri.getLastPathSegment();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -287,7 +304,7 @@ public class RepoProvider extends FDroidProvider {
|
|||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
case CODE_SINGLE:
|
case CODE_SINGLE:
|
||||||
where = ( where == null ? "" : where ) +
|
where = ( where == null ? "" : where + " AND " ) +
|
||||||
"_ID = " + uri.getLastPathSegment();
|
"_ID = " + uri.getLastPathSegment();
|
||||||
break;
|
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.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import org.fdroid.fdroid.DB;
|
|
||||||
import org.fdroid.fdroid.ProgressListener;
|
import org.fdroid.fdroid.ProgressListener;
|
||||||
import org.fdroid.fdroid.RepoXMLHandler;
|
import org.fdroid.fdroid.RepoXMLHandler;
|
||||||
import org.fdroid.fdroid.Utils;
|
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.Repo;
|
||||||
import org.fdroid.fdroid.data.RepoProvider;
|
import org.fdroid.fdroid.data.RepoProvider;
|
||||||
import org.fdroid.fdroid.net.Downloader;
|
import org.fdroid.fdroid.net.Downloader;
|
||||||
@ -40,7 +41,8 @@ abstract public class RepoUpdater {
|
|||||||
|
|
||||||
protected final Context context;
|
protected final Context context;
|
||||||
protected final Repo repo;
|
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 boolean hasChanged = false;
|
||||||
protected ProgressListener progressListener;
|
protected ProgressListener progressListener;
|
||||||
|
|
||||||
@ -57,10 +59,14 @@ abstract public class RepoUpdater {
|
|||||||
return hasChanged;
|
return hasChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<DB.App> getApps() {
|
public List<App> getApps() {
|
||||||
return apps;
|
return apps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Apk> getApks() {
|
||||||
|
return apks;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isInteractive() {
|
public boolean isInteractive() {
|
||||||
return progressListener != null;
|
return progressListener != null;
|
||||||
}
|
}
|
||||||
@ -173,7 +179,7 @@ abstract public class RepoUpdater {
|
|||||||
// Process the index...
|
// Process the index...
|
||||||
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
|
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
|
||||||
XMLReader reader = parser.getXMLReader();
|
XMLReader reader = parser.getXMLReader();
|
||||||
RepoXMLHandler handler = new RepoXMLHandler(repo, apps, progressListener);
|
RepoXMLHandler handler = new RepoXMLHandler(repo, progressListener);
|
||||||
|
|
||||||
if (isInteractive()) {
|
if (isInteractive()) {
|
||||||
// Only bother spending the time to count the expected apps
|
// Only bother spending the time to count the expected apps
|
||||||
@ -186,6 +192,8 @@ abstract public class RepoUpdater {
|
|||||||
new BufferedReader(new FileReader(indexFile)));
|
new BufferedReader(new FileReader(indexFile)));
|
||||||
|
|
||||||
reader.parse(is);
|
reader.parse(is);
|
||||||
|
apps = handler.getApps();
|
||||||
|
apks = handler.getApks();
|
||||||
updateRepo(handler, downloader.getETag());
|
updateRepo(handler, downloader.getETag());
|
||||||
}
|
}
|
||||||
} catch (SAXException e) {
|
} catch (SAXException e) {
|
||||||
@ -216,7 +224,7 @@ abstract public class RepoUpdater {
|
|||||||
|
|
||||||
ContentValues values = new ContentValues();
|
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)) {
|
if (repo.lastetag == null || !repo.lastetag.equals(etag)) {
|
||||||
values.put(RepoProvider.DataColumns.LAST_ETAG, etag);
|
values.put(RepoProvider.DataColumns.LAST_ETAG, etag);
|
||||||
|
@ -2,7 +2,6 @@ package org.fdroid.fdroid.updater;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import org.fdroid.fdroid.DB;
|
|
||||||
import org.fdroid.fdroid.Hasher;
|
import org.fdroid.fdroid.Hasher;
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
@ -30,7 +29,7 @@ public class SignedRepoUpdater extends RepoUpdater {
|
|||||||
boolean match = false;
|
boolean match = false;
|
||||||
for (Certificate cert : certs) {
|
for (Certificate cert : certs) {
|
||||||
String certdata = Hasher.hex(cert);
|
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;
|
repo.pubkey = certdata;
|
||||||
}
|
}
|
||||||
if (repo.pubkey != null && repo.pubkey.equals(certdata)) {
|
if (repo.pubkey != null && repo.pubkey.equals(certdata)) {
|
||||||
|
@ -2,9 +2,7 @@ package org.fdroid.fdroid.updater;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import org.fdroid.fdroid.DB;
|
|
||||||
import org.fdroid.fdroid.data.Repo;
|
import org.fdroid.fdroid.data.Repo;
|
||||||
import org.fdroid.fdroid.net.Downloader;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
|
@ -1,40 +1,50 @@
|
|||||||
package org.fdroid.fdroid.views;
|
package org.fdroid.fdroid.views;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import android.content.Context;
|
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.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.*;
|
import android.widget.ImageView;
|
||||||
import android.text.SpannableString;
|
import android.widget.RelativeLayout;
|
||||||
import android.text.style.StyleSpan;
|
import android.widget.TextView;
|
||||||
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 com.nostra13.universalimageloader.core.DisplayImageOptions;
|
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
|
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 Context mContext;
|
||||||
private LayoutInflater mInflater;
|
private LayoutInflater mInflater;
|
||||||
private DisplayImageOptions displayImageOptions;
|
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;
|
mContext = context;
|
||||||
mInflater = (LayoutInflater) mContext.getSystemService(
|
mInflater = (LayoutInflater) mContext.getSystemService(
|
||||||
Context.LAYOUT_INFLATER_SERVICE);
|
Context.LAYOUT_INFLATER_SERVICE);
|
||||||
|
|
||||||
displayImageOptions = new DisplayImageOptions.Builder()
|
displayImageOptions = new DisplayImageOptions.Builder()
|
||||||
.cacheInMemory(true)
|
.cacheInMemory(true)
|
||||||
.cacheOnDisc(true)
|
.cacheOnDisc(true)
|
||||||
@ -52,33 +62,6 @@ abstract public class AppListAdapter extends BaseAdapter {
|
|||||||
|
|
||||||
abstract protected boolean showStatusInstalled();
|
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 {
|
private static class ViewHolder {
|
||||||
TextView name;
|
TextView name;
|
||||||
TextView summary;
|
TextView summary;
|
||||||
@ -88,26 +71,32 @@ abstract public class AppListAdapter extends BaseAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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();
|
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.name.setText(app.name);
|
||||||
holder.summary.setText(app.summary);
|
holder.summary.setText(app.summary);
|
||||||
@ -121,18 +110,16 @@ abstract public class AppListAdapter extends BaseAdapter {
|
|||||||
|
|
||||||
// Disable it all if it isn't compatible...
|
// Disable it all if it isn't compatible...
|
||||||
View[] views = {
|
View[] views = {
|
||||||
convertView,
|
view,
|
||||||
holder.status,
|
holder.status,
|
||||||
holder.summary,
|
holder.summary,
|
||||||
holder.license,
|
holder.license,
|
||||||
holder.name
|
holder.name
|
||||||
};
|
};
|
||||||
|
|
||||||
for (View view : views) {
|
for (View v : views) {
|
||||||
view.setEnabled(app.compatible && !app.filtered);
|
v.setEnabled(app.compatible && !app.isFiltered());
|
||||||
}
|
}
|
||||||
|
|
||||||
return convertView;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String ellipsize(String input, int maxLength) {
|
private String ellipsize(String input, int maxLength) {
|
||||||
@ -142,26 +129,31 @@ abstract public class AppListAdapter extends BaseAdapter {
|
|||||||
return input.substring(0, maxLength) + "…";
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app.installedVersion == null) {
|
PackageInfo installedInfo = app.getInstalledInfo(mContext);
|
||||||
return ellipsize(app.curApk.version, 12);
|
|
||||||
|
if (installedInfo == null) {
|
||||||
|
return ellipsize(app.curVersion, 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app.toUpdate && showStatusUpdate()) {
|
String installedVersionString = installedInfo.versionName;
|
||||||
return ellipsize(app.installedVersion, 8) +
|
int installedVersionCode = installedInfo.versionCode;
|
||||||
" → " + ellipsize(app.curApk.version, 8);
|
|
||||||
|
if (app.canAndWantToUpdate(mContext) && showStatusUpdate()) {
|
||||||
|
return ellipsize(installedVersionString, 8) +
|
||||||
|
" → " + ellipsize(app.curVersion, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app.installedVerCode > 0 && showStatusInstalled()) {
|
if (installedVersionCode > 0 && showStatusInstalled()) {
|
||||||
return ellipsize(app.installedVersion, 12) + " ✔";
|
return ellipsize(installedVersionString, 12) + " ✔";
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.installedVersion;
|
return installedVersionString;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void layoutIcon(ImageView icon, boolean compact) {
|
private void layoutIcon(ImageView icon, boolean compact) {
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
package org.fdroid.fdroid.views;
|
package org.fdroid.fdroid.views;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.app.FragmentPagerAdapter;
|
import android.support.v4.app.FragmentPagerAdapter;
|
||||||
|
|
||||||
import org.fdroid.fdroid.FDroid;
|
import org.fdroid.fdroid.FDroid;
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
|
import org.fdroid.fdroid.data.AppProvider;
|
||||||
import org.fdroid.fdroid.views.fragments.AvailableAppsFragment;
|
import org.fdroid.fdroid.views.fragments.AvailableAppsFragment;
|
||||||
import org.fdroid.fdroid.views.fragments.CanUpdateAppsFragment;
|
import org.fdroid.fdroid.views.fragments.CanUpdateAppsFragment;
|
||||||
import org.fdroid.fdroid.views.fragments.InstalledAppsFragment;
|
import org.fdroid.fdroid.views.fragments.InstalledAppsFragment;
|
||||||
@ -22,6 +25,19 @@ public class AppListFragmentPageAdapter extends FragmentPagerAdapter {
|
|||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getUpdateTabTitle() {
|
||||||
|
Uri uri = AppProvider.getCanUpdateUri();
|
||||||
|
String[] projection = new String[] { AppProvider.DataColumns._COUNT };
|
||||||
|
Cursor cursor = parent.getContentResolver().query(uri, projection, null, null, null);
|
||||||
|
String suffix = "";
|
||||||
|
if (cursor != null && cursor.getCount() == 1) {
|
||||||
|
cursor.moveToFirst();
|
||||||
|
int count = cursor.getInt(0);
|
||||||
|
suffix = " (" + count + ")";
|
||||||
|
}
|
||||||
|
return parent.getString(R.string.tab_updates) + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Fragment getItem(int i) {
|
public Fragment getItem(int i) {
|
||||||
if ( i == 0 ) {
|
if ( i == 0 ) {
|
||||||
@ -46,8 +62,7 @@ public class AppListFragmentPageAdapter extends FragmentPagerAdapter {
|
|||||||
case 1:
|
case 1:
|
||||||
return parent.getString(R.string.inst);
|
return parent.getString(R.string.inst);
|
||||||
case 2:
|
case 2:
|
||||||
return parent.getString(R.string.tab_updates) + " ("
|
return getUpdateTabTitle();
|
||||||
+ parent.getManager().getCanUpdateAdapter().getCount() + ")";
|
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,20 @@
|
|||||||
package org.fdroid.fdroid.views;
|
package org.fdroid.fdroid.views;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
|
||||||
public class AvailableAppListAdapter extends AppListAdapter {
|
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
|
@Override
|
||||||
|
@ -1,10 +1,20 @@
|
|||||||
package org.fdroid.fdroid.views;
|
package org.fdroid.fdroid.views;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
|
||||||
public class CanUpdateAppListAdapter extends AppListAdapter {
|
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
|
@Override
|
||||||
|
@ -1,10 +1,20 @@
|
|||||||
package org.fdroid.fdroid.views;
|
package org.fdroid.fdroid.views;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
|
||||||
public class InstalledAppListAdapter extends AppListAdapter {
|
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
|
@Override
|
||||||
|
@ -8,6 +8,7 @@ import android.os.Bundle;
|
|||||||
import android.support.v4.app.FragmentActivity;
|
import android.support.v4.app.FragmentActivity;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import org.fdroid.fdroid.FDroidApp;
|
||||||
import org.fdroid.fdroid.compat.ActionBarCompat;
|
import org.fdroid.fdroid.compat.ActionBarCompat;
|
||||||
import org.fdroid.fdroid.data.Repo;
|
import org.fdroid.fdroid.data.Repo;
|
||||||
import org.fdroid.fdroid.data.RepoProvider;
|
import org.fdroid.fdroid.data.RepoProvider;
|
||||||
@ -20,6 +21,9 @@ public class RepoDetailsActivity extends FragmentActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
||||||
|
((FDroidApp) getApplication()).applyTheme(this);
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
long repoId = getIntent().getLongExtra(RepoDetailsFragment.ARG_REPO_ID, 0);
|
long repoId = getIntent().getLongExtra(RepoDetailsFragment.ARG_REPO_ID, 0);
|
||||||
|
@ -1,31 +1,114 @@
|
|||||||
package org.fdroid.fdroid.views.fragments;
|
package org.fdroid.fdroid.views.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
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.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.ListView;
|
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.*;
|
||||||
|
import org.fdroid.fdroid.data.App;
|
||||||
|
import org.fdroid.fdroid.data.AppProvider;
|
||||||
import org.fdroid.fdroid.views.AppListAdapter;
|
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 AppListAdapter getAppListAdapter();
|
||||||
|
|
||||||
protected abstract String getFromTitle();
|
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
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
Preferences.get().registerCompactLayoutChangeListener(this);
|
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
|
@Override
|
||||||
@ -34,48 +117,9 @@ abstract class AppListFragment extends Fragment implements AdapterView.OnItemCli
|
|||||||
Preferences.get().unregisterCompactLayoutChangeListener(this);
|
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
|
@Override
|
||||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
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 intent = new Intent(getActivity(), AppDetails.class);
|
||||||
intent.putExtra("appid", app.id);
|
intent.putExtra("appid", app.id);
|
||||||
intent.putExtra("from", getFromTitle());
|
intent.putExtra("from", getFromTitle());
|
||||||
@ -86,4 +130,22 @@ abstract class AppListFragment extends Fragment implements AdapterView.OnItemCli
|
|||||||
public void onPreferenceChange() {
|
public void onPreferenceChange() {
|
||||||
getAppListAdapter().notifyDataSetChanged();
|
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;
|
package org.fdroid.fdroid.views.fragments;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.support.v4.app.LoaderManager;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.*;
|
import android.widget.*;
|
||||||
|
import org.fdroid.fdroid.Preferences;
|
||||||
import org.fdroid.fdroid.views.AppListAdapter;
|
|
||||||
import org.fdroid.fdroid.R;
|
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
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
AppListView view = new AppListView(getActivity());
|
LinearLayout view = new LinearLayout(getActivity());
|
||||||
view.setOrientation(LinearLayout.VERTICAL);
|
view.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
final List<String> categories = AppProvider.Helper.categories(getActivity());
|
||||||
|
|
||||||
Spinner spinner = new Spinner(getActivity());
|
Spinner spinner = new Spinner(getActivity());
|
||||||
// Giving it an ID lets the default save/restore state
|
// Giving it an ID lets the default save/restore state
|
||||||
// functionality do its stuff.
|
// functionality do its stuff.
|
||||||
spinner.setId(R.id.categorySpinner);
|
spinner.setId(R.id.categorySpinner);
|
||||||
spinner.setAdapter(getAppListManager().getCategoriesAdapter());
|
spinner.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, categories));
|
||||||
spinner.setOnItemSelectedListener(this);
|
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(
|
view.addView(
|
||||||
spinner,
|
spinner,
|
||||||
@ -30,8 +76,10 @@ public class AvailableAppsFragment extends AppListFragment implements AdapterVie
|
|||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT));
|
LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||||
|
|
||||||
ListView list = createAppListView();
|
ListView list = new ListView(getActivity());
|
||||||
view.setAppList(list);
|
list.setId(android.R.id.list);
|
||||||
|
list.setFastScrollEnabled(true);
|
||||||
|
list.setOnItemClickListener(this);
|
||||||
view.addView(
|
view.addView(
|
||||||
list,
|
list,
|
||||||
new ViewGroup.LayoutParams(
|
new ViewGroup.LayoutParams(
|
||||||
@ -42,24 +90,14 @@ public class AvailableAppsFragment extends AppListFragment implements AdapterVie
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onItemSelected(AdapterView<?> parent, View view, int pos,
|
protected Uri getDataUri() {
|
||||||
long id) {
|
if (currentCategory == null || currentCategory.equals(AppProvider.Helper.getCategoryAll(getActivity())))
|
||||||
String category = parent.getItemAtPosition(pos).toString();
|
return AppProvider.getContentUri();
|
||||||
getAppListManager().setCurrentCategory(category);
|
else if (currentCategory.equals(AppProvider.Helper.getCategoryRecentlyUpdated(getActivity())))
|
||||||
}
|
return AppProvider.getRecentlyUpdatedUri();
|
||||||
|
else if (currentCategory.equals(AppProvider.Helper.getCategoryWhatsNew(getActivity())))
|
||||||
@Override
|
return AppProvider.getNewlyAddedUri();
|
||||||
public void onNothingSelected(AdapterView<?> parent) {
|
else
|
||||||
|
return AppProvider.getCategoryUri(currentCategory);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected AppListAdapter getAppListAdapter() {
|
|
||||||
return getAppListManager().getAvailableAdapter();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getFromTitle() {
|
|
||||||
return getAppListManager().getCurrentCategory();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,30 @@
|
|||||||
package org.fdroid.fdroid.views.fragments;
|
package org.fdroid.fdroid.views.fragments;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.support.v4.content.CursorLoader;
|
||||||
import android.view.View;
|
import android.support.v4.content.Loader;
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
|
import org.fdroid.fdroid.data.AppProvider;
|
||||||
import org.fdroid.fdroid.views.AppListAdapter;
|
import org.fdroid.fdroid.views.AppListAdapter;
|
||||||
|
import org.fdroid.fdroid.views.CanUpdateAppListAdapter;
|
||||||
|
|
||||||
public class CanUpdateAppsFragment extends AppListFragment {
|
public class CanUpdateAppsFragment extends AppListFragment {
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
|
||||||
return createPlainAppList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected AppListAdapter getAppListAdapter() {
|
protected AppListAdapter getAppListAdapter() {
|
||||||
return getAppListManager().getCanUpdateAdapter();
|
return new CanUpdateAppListAdapter(getActivity(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String getFromTitle() {
|
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;
|
package org.fdroid.fdroid.views.fragments;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.support.v4.content.CursorLoader;
|
||||||
import android.view.View;
|
import android.support.v4.content.Loader;
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
|
import org.fdroid.fdroid.data.AppProvider;
|
||||||
import org.fdroid.fdroid.views.AppListAdapter;
|
import org.fdroid.fdroid.views.AppListAdapter;
|
||||||
|
import org.fdroid.fdroid.views.InstalledAppListAdapter;
|
||||||
|
|
||||||
public class InstalledAppsFragment extends AppListFragment {
|
public class InstalledAppsFragment extends AppListFragment {
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
|
||||||
return createPlainAppList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected AppListAdapter getAppListAdapter() {
|
protected AppListAdapter getAppListAdapter() {
|
||||||
return getAppListManager().getInstalledAdapter();
|
return new InstalledAppListAdapter(getActivity(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String getFromTitle() {
|
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);
|
TextView lastUpdated = (TextView)repoView.findViewById(R.id.text_last_update);
|
||||||
|
|
||||||
name.setText(repo.getName());
|
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);
|
setupDescription(repoView, repo);
|
||||||
setupRepoFingerprint(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
|
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
|
||||||
|
|
||||||
# Project target.
|
# 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