commit 0b71cb7e7331fb73ee762225d0225fd92ce99010 Author: Ciaran Gultnieks Date: Tue Oct 19 23:24:04 2010 +0100 Initial files diff --git a/.classpath b/.classpath new file mode 100644 index 000000000..609aa00eb --- /dev/null +++ b/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..bc7bb0169 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +local.properties +bin/* +gen/* + diff --git a/.project b/.project new file mode 100644 index 000000000..7045783ad --- /dev/null +++ b/.project @@ -0,0 +1,33 @@ + + + fdroid + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 000000000..dc93213b0 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + android:label="@string/app_name" + android:icon="@drawable/icon"> + + + + + + + + + + + + + + diff --git a/build.properties b/build.properties new file mode 100644 index 000000000..edc7f2305 --- /dev/null +++ b/build.properties @@ -0,0 +1,17 @@ +# This file is used to override default values used by the Ant build system. +# +# This file must be checked in 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. + diff --git a/build.xml b/build.xml new file mode 100644 index 000000000..96b0cec02 --- /dev/null +++ b/build.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/default.properties b/default.properties new file mode 100644 index 000000000..19c96655d --- /dev/null +++ b/default.properties @@ -0,0 +1,13 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "build.properties", and override values to adapt the script to your +# project structure. + +# Indicates whether an apk should be generated for each density. +split.density=false +# Project target. +target=android-4 diff --git a/res/drawable/btn_check_off.png b/res/drawable/btn_check_off.png new file mode 100644 index 000000000..aa585d405 Binary files /dev/null and b/res/drawable/btn_check_off.png differ diff --git a/res/drawable/btn_check_on.png b/res/drawable/btn_check_on.png new file mode 100644 index 000000000..fdaab1344 Binary files /dev/null and b/res/drawable/btn_check_on.png differ diff --git a/res/drawable/icon.png b/res/drawable/icon.png new file mode 100644 index 000000000..717577be3 Binary files /dev/null and b/res/drawable/icon.png differ diff --git a/res/layout/about.xml b/res/layout/about.xml new file mode 100644 index 000000000..2f7a96a78 --- /dev/null +++ b/res/layout/about.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout/addrepo.xml b/res/layout/addrepo.xml new file mode 100644 index 000000000..3cdd07869 --- /dev/null +++ b/res/layout/addrepo.xml @@ -0,0 +1,37 @@ + + + + + + + \ No newline at end of file diff --git a/res/layout/apklistitem.xml b/res/layout/apklistitem.xml new file mode 100644 index 000000000..1ad4055bf --- /dev/null +++ b/res/layout/apklistitem.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/res/layout/appdetails.xml b/res/layout/appdetails.xml new file mode 100644 index 000000000..057645189 --- /dev/null +++ b/res/layout/appdetails.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/applistitem.xml b/res/layout/applistitem.xml new file mode 100644 index 000000000..dacff8aef --- /dev/null +++ b/res/layout/applistitem.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/fdroid.xml b/res/layout/fdroid.xml new file mode 100644 index 000000000..a48ecc318 --- /dev/null +++ b/res/layout/fdroid.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/res/layout/remrepo.xml b/res/layout/remrepo.xml new file mode 100644 index 000000000..e3c660289 --- /dev/null +++ b/res/layout/remrepo.xml @@ -0,0 +1,36 @@ + + + + + + + \ No newline at end of file diff --git a/res/layout/repolist.xml b/res/layout/repolist.xml new file mode 100644 index 000000000..9043a0fa1 --- /dev/null +++ b/res/layout/repolist.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/res/layout/repolisticons.xml b/res/layout/repolisticons.xml new file mode 100644 index 000000000..62a037c2a --- /dev/null +++ b/res/layout/repolisticons.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values/alertstr.xml b/res/values/alertstr.xml new file mode 100644 index 000000000..2f77c7ba4 --- /dev/null +++ b/res/values/alertstr.xml @@ -0,0 +1,37 @@ + + + Servers: + Server version: + Installed: + Installed version: + Install + Uninstall + Update! + + There updates available for some installed applications.\nDo you wish to see them? + The list of repositories in use has been changed.\nDo you wish to update them? + + Could not connect to server or apk file is corrupt! + Getting application from:\n + + + + + diff --git a/res/values/menu.xml b/res/values/menu.xml new file mode 100644 index 000000000..bca9bc6bb --- /dev/null +++ b/res/values/menu.xml @@ -0,0 +1,37 @@ + + + + + + Update + Manage Repos + + + About + + + + New Repository + Remove Repository + + + + + \ No newline at end of file diff --git a/res/values/path.xml b/res/values/path.xml new file mode 100644 index 000000000..afacd7ae9 --- /dev/null +++ b/res/values/path.xml @@ -0,0 +1,23 @@ + + + /sdcard/.fdroid/icons/ + + + diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 000000000..879c66ccb --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,61 @@ + + + FDroid + About FDroid + Based on Aptoide.\nReleased under the GNU GPL v2 license. + Home: + e-Mail: + Web Site + + No application found! + You have no repositories configured!\n\nA repository is a source of applications. To add one, press the MENU button now and enter the URL.\n\nA repository URL looks something like this: http://f-droid.org/repo + + Not Installed + Installed - Ver.: + Update possible - Ver.: + + + + + + + + + Error + Ok + + Yes + No + Add new repository + Add + + Cancel + Chose repository to remove + http://f-droid.org + Could not connect to server! + + Update repositories + Installed + Available + Updates + Updates available + Please Wait + Updating application list... + Could not connect to the network. + Timeout + Could not connect to server! + Download + Getting application from + Market + available v +Sort application list by: +Alphabetically +Installed / Not Installed +Most recent first +Rating +Show applications: +By category +All applications +Save +Repository URL + diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java new file mode 100644 index 000000000..a3357525e --- /dev/null +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2010 Ciaran Gultnieks, ciaran@ciarang.com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.fdroid.fdroid; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import org.fdroid.fdroid.R; + +import android.app.AlertDialog; +import android.app.ListActivity; +import android.app.ProgressDialog; +import android.graphics.drawable.BitmapDrawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; +import android.content.pm.PackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +public class AppDetails extends ListActivity { + + private String LOCAL_PATH = "/sdcard/.fdroid"; + + private static final int REQUEST_INSTALL = 0; + private static final int REQUEST_UNINSTALL = 1; + + private class ApkListAdapter extends BaseAdapter { + + private List items = new ArrayList(); + + public ApkListAdapter(Context context) { + } + + public void addItem(DB.Apk apk) { + items.add(apk); + } + + @Override + public int getCount() { + return items.size(); + } + + @Override + public Object getItem(int position) { + return items.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View v = convertView; + if (v == null) { + LayoutInflater vi = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + v = vi.inflate(R.layout.apklistitem, null); + } + DB.Apk apk = items.get(position); + TextView version = (TextView) v.findViewById(R.id.version); + version.setText("Version " + apk.version); + TextView status = (TextView) v.findViewById(R.id.status); + if (apk.version.equals(app.installedVersion)) + status.setText("Installed"); + else + status.setText("Not installed"); + return v; + } + } + + private DB db; + private DB.App app; + private DB.Apk curapk; + private String appid; + private PackageManager mPm; + private ProgressDialog pd; + + private Context mctx = this; + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + setContentView(R.layout.appdetails); + + db = new DB(this); + mPm = getPackageManager(); + + Intent i = getIntent(); + appid = ""; + if (!i.hasExtra("appid")) { + Log.d("FDroid", "No application ID in AppDetails!?"); + } else { + appid = i.getStringExtra("appid"); + } + + reset(false); + + } + + // Reset the display and list contents. Used when entering the activity, and + // also when something has been installed/uninstalled. In the latter case, + // 'update' is true to make the installed status get refreshed. + private void reset(boolean update) { + + Log.d("FDroid", "Getting application details for " + appid); + app = db.getApps(appid, null, update).get(0); + + // Set the icon... + ImageView iv = (ImageView) findViewById(R.id.icon); + String icon_path = this.getString(R.string.icons_path) + app.icon; + File test_icon = new File(icon_path); + if (test_icon.exists()) { + iv.setImageDrawable(new BitmapDrawable(icon_path)); + } else { + iv.setImageResource(android.R.drawable.sym_def_app_icon); + } + + // Set the title and other header details... + TextView tv = (TextView) findViewById(R.id.title); + tv.setText(app.name); + tv = (TextView) findViewById(R.id.license); + tv.setText(app.license); + tv = (TextView) findViewById(R.id.status); + int vnum = app.apks.size(); + String v = vnum == 1 ? "version" : "versions"; + tv.setText("" + vnum + " " + v + ", " + + (app.installedVersion == null ? "not" : "1") + " installed"); + tv = (TextView) findViewById(R.id.description); + tv.setText(app.description); + + // Set up the list... + ApkListAdapter la = new ApkListAdapter(this); + for (DB.Apk apk : app.apks) + la.addItem(apk); + setListAdapter(la); + + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + // Create alert dialog... + final AlertDialog p = new AlertDialog.Builder(this).create(); + + curapk = app.apks.get(position); + + // Set the title and icon... + String icon_path = this.getString(R.string.icons_path) + app.icon; + File test_icon = new File(icon_path); + if (test_icon.exists()) { + p.setIcon(new BitmapDrawable(icon_path)); + } else { + p.setIcon(android.R.drawable.sym_def_app_icon); + } + p.setTitle(app.name + " " + curapk.version); + + boolean caninstall = true; + String installed = getString(R.string.no); + if (app.installedVersion != null) { + if (app.installedVersion.equals(curapk.version)) { + installed = getString(R.string.yes); + caninstall = false; + } else { + installed += " - " + app.installedVersion; + } + } + p.setMessage(getString(R.string.isinst) + " " + installed); + + p.setButton(getString(R.string.ok), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + return; + } + }); + + if (caninstall) { + p.setButton2(getString(R.string.install), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + p.dismiss(); + new Thread() { + public void run() { + String apk_pkg = downloadFile(app, curapk); + if (apk_pkg == null) { + Message msg = new Message(); + msg.arg1 = 1; + download_handler.sendMessage(msg); + download_error_handler + .sendEmptyMessage(0); + } else { + installApk(apk_pkg); + } + } + }.start(); + } + }); + } + + p.setButton3(getString(R.string.apk_market_view), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + startActivity(new Intent(Intent.ACTION_VIEW, Uri + .parse("market://search?q=pname:" + app.id))); + } + }); + + if (!caninstall) { + p.setButton2(getString(R.string.rem), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + removeApk(app.id); + } + }); + } + + p.show(); + } + + // Download the requested apk file, given the DB.App and DB.Apk + // that refer to it. Returns the path to the downloaded file, or + // null if the download was not successful. + private String downloadFile(DB.App app, DB.Apk apk) { + try { + + String apkname = apk.apkName; + String localfile = new String(LOCAL_PATH + "/" + apkname); + String remotefile = apk.server + "/" + apkname.replace(" ", "%20"); + + Log.d("FDroid", "Downloading apk from " + remotefile); + + Message msg = new Message(); + msg.arg1 = 0; + msg.obj = new String(remotefile); + download_handler.sendMessage(msg); + + BufferedInputStream getit = new BufferedInputStream(new URL( + remotefile).openStream()); + + FileOutputStream saveit = new FileOutputStream(localfile); + BufferedOutputStream bout = new BufferedOutputStream(saveit, 1024); + byte data[] = new byte[1024]; + + int readed = getit.read(data, 0, 1024); + while (readed != -1) { + bout.write(data, 0, readed); + readed = getit.read(data, 0, 1024); + } + bout.close(); + getit.close(); + saveit.close(); + File f = new File(localfile); + Md5Handler hash = new Md5Handler(); + + if (apk.hash.equalsIgnoreCase(hash.md5Calc(f))) { + return localfile; + } else { + return null; + } + } catch (Exception e) { + Log.d("FDroid", "Download failed - " + e.getMessage()); + return null; + } + } + + private Handler download_handler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (msg.arg1 == 0) { + pd = ProgressDialog.show(mctx, getString(R.string.download), + getString(R.string.download_server) + ":\n " + + msg.obj.toString(), true); + } else { + pd.dismiss(); + } + } + }; + + private Handler download_error_handler = new Handler() { + @Override + public void handleMessage(Message msg) { + Toast.makeText(mctx, getString(R.string.connection_error_msg), + Toast.LENGTH_LONG).show(); + } + }; + + private void removeApk(String id) { + PackageInfo pkginfo; + try { + pkginfo = mPm.getPackageInfo(id, 0); + } catch (NameNotFoundException e) { + Log.d("FDroid", "Couldn't find package " + id + " to uninstall."); + return; + } + Uri uri = Uri.fromParts("package", pkginfo.packageName, null); + Intent intent = new Intent(Intent.ACTION_DELETE, uri); + startActivityForResult(intent, REQUEST_UNINSTALL); + } + + private void installApk(String id) { + Intent intent = new Intent(); + intent.setAction(android.content.Intent.ACTION_VIEW); + intent.setDataAndType(Uri.parse("file://" + id), + "application/vnd.android.package-archive"); + + Message msg = new Message(); + msg.arg1 = 1; + download_handler.sendMessage(msg); + + startActivityForResult(intent, REQUEST_INSTALL); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + reset(true); + } + +} diff --git a/src/org/fdroid/fdroid/DB.java b/src/org/fdroid/fdroid/DB.java new file mode 100644 index 000000000..6b84f07ed --- /dev/null +++ b/src/org/fdroid/fdroid/DB.java @@ -0,0 +1,445 @@ +/* +a * Copyright (C) 2010 Ciaran Gultnieks, ciaran@ciarang.com + * Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.fdroid.fdroid; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Vector; + +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; + +public class DB { + + private static final String DATABASE_NAME = "fdroid_db"; + + private SQLiteDatabase db; + + // The TABLE_APP table stores details of all the applications we know about. + // This information is retrieved from the repositories. + private static final String TABLE_APP = "fdroid_app"; + private static final String CREATE_TABLE_APP = "create table " + TABLE_APP + + "( " + "id text not null, " + "name text not null, " + + "summary text not null, " + "icon text, " + + "description text not null, " + "license text not null, " + + "webURL text, " + "trackerURL text, " + "sourceURL text, " + + "installedVersion text," + "hasUpdates int not null," + + "primary key(id));"; + + public static class App { + + public App() { + name = "Unknown"; + summary = "Unknown application"; + icon = "noicon.png"; + id = "unknown"; + hasUpdates = false; + updated = false; + apks = new Vector(); + } + + public String id; + public String name; + public String summary; + public String icon; + public String description; + public String license; + public String webURL; + public String trackerURL; + public String sourceURL; + public String installedVersion; + + // True if there are new versions (apks) that the user hasn't + // explicitly ignored. + public boolean hasUpdates; + + // Used internally for tracking during repo updates. + public boolean updated; + + public Vector apks; + } + + // 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. + private static final String TABLE_APK = "fdroid_apk"; + private static final String CREATE_TABLE_APK = "create table " + TABLE_APK + + "( " + "id text not null, " + "version text not null, " + + "server text not null, " + "hash text not null, " + + "apkName text not null, " + "primary key(id,version));"; + + public static class Apk { + + public Apk() { + updated = false; + } + + public String id; + public String version; + public String server; + public String hash; + public String apkName; + + // Used internally for tracking during repo updates. + public boolean updated; + + public String getURL() { + String path = apkName.replace(" ", "%20"); + return server + "/" + path; + } + } + + // The TABLE_REPO table stores the details of the repositories in use. + private static final String TABLE_REPO = "fdroid_repo"; + private static final String CREATE_TABLE_REPO = "create table " + + TABLE_REPO + " (" + "address text primary key, " + + "inuse integer not null, " + "priority integer not null);"; + + public static class Repo { + public String address; + public boolean inuse; + public int priority; + } + + private PackageManager mPm; + + public DB(Context ctx) { + db = ctx.openOrCreateDatabase(DATABASE_NAME, 0, null); + + Cursor c = db.rawQuery( + "SELECT name FROM sqlite_master WHERE type='table' AND name= '" + + TABLE_REPO + "'", null); + if (c.getCount() == 0) { + reset(); + } + c.close(); + + mPm = ctx.getPackageManager(); + } + + // Reset the database, i.e. (re-)create all the tables from scratch and + // populate any initial data. + public void reset() { + db.execSQL("drop table if exists " + TABLE_REPO); + db.execSQL("drop table if exists " + TABLE_APP); + db.execSQL("drop table if exists " + TABLE_APK); + db.execSQL(CREATE_TABLE_REPO); + db.execSQL(CREATE_TABLE_APP); + db.execSQL(CREATE_TABLE_APK); + addServer("http://f-droid.org/repo", 10); + } + + public void close() { + db.close(); + db = null; + } + + // Return a list of apps matching the given criteria. + // 'appid' - specific app id to retrieve, or null + // 'filter' - search text to filter on. + // 'update' - update installed version information from device, rather than + // simply using values cached in the database. Slower. + public Vector getApps(String appid, String filter, boolean update) { + Vector result = new Vector(); + Cursor c = null; + Cursor c2 = null; + try { + + String query = "select * from " + TABLE_APP; + if (appid != null) { + query += " where id = '" + appid + "'"; + } else if (filter != null) { + query += " where name like '%" + filter + "%'" + + " or description like '%" + filter + "%'"; + } + query += " order by name collate nocase"; + + c = db.rawQuery(query, null); + c.moveToFirst(); + while (!c.isAfterLast()) { + + App app = new App(); + app.id = c.getString(c.getColumnIndex("id")); + app.name = c.getString(c.getColumnIndex("name")); + app.summary = c.getString(c.getColumnIndex("summary")); + app.icon = c.getString(c.getColumnIndex("icon")); + app.description = c.getString(c.getColumnIndex("description")); + app.license = c.getString(c.getColumnIndex("license")); + app.webURL = c.getString(c.getColumnIndex("webURL")); + app.trackerURL = c.getString(c.getColumnIndex("trackerURL")); + app.sourceURL = c.getString(c.getColumnIndex("sourceURL")); + app.installedVersion = c.getString(c + .getColumnIndex("installedVersion")); + app.hasUpdates = c.getInt(c.getColumnIndex("hasUpdates")) == 1; + + c2 = db.rawQuery("select * from " + TABLE_APK + " where " + + "id = '" + app.id + "'", null); + c2.moveToFirst(); + while (!c2.isAfterLast()) { + Apk apk = new Apk(); + apk.id = app.id; + apk.version = c2.getString(c2.getColumnIndex("version")); + apk.server = c2.getString(c2.getColumnIndex("server")); + apk.hash = c2.getString(c2.getColumnIndex("hash")); + apk.apkName = c2.getString(c2.getColumnIndex("apkName")); + app.apks.add(apk); + c2.moveToNext(); + } + + result.add(app); + c.moveToNext(); + } + + } catch (Exception e) { + Log.d("FDroid", "Exception during database reading - " + + e.getMessage()); + } finally { + if (c != null) { + c.close(); + } + if (c2 != null) { + c2.close(); + } + } + + if (update) { + getUpdates(result); + } + + return result; + } + + // Verify installed status against the system's package list. + private void getUpdates(Vector apps) { + List installedPackages = mPm.getInstalledPackages(0); + Map systemApks = new HashMap(); + for (PackageInfo appInfo : installedPackages) { + systemApks.put(appInfo.packageName, appInfo); + } + + for (DB.App app : apps) { + if (systemApks.containsKey(app.id)) { + String version = systemApks.get(app.id).versionName; + if (app.installedVersion == null + || !app.installedVersion.equals(version)) { + setInstalledVersion(app.id, version); + app.installedVersion = version; + } + } else { + if (app.installedVersion != null) { + setInstalledVersion(app.id, null); + app.installedVersion = null; + } + } + } + } + + private Vector updateApps = null; + + // Called before a repo update starts. + public void beginUpdate() { + updateApps = getApps(null, null, true); + Log.d("FDroid", "AppUpdate: " + updateApps.size() + + " apps before starting."); + } + + // Called when a repo update ends. Any applications that have not been + // updated (by a call to updateApplication) are assumed to be no longer + // in the repos. + public void endUpdate() { + for (App app : updateApps) { + if (!app.updated) { + // The application hasn't been updated, so it's no longer + // in the repos. + db.delete(TABLE_APP, "id = '" + app.id + "'", null); + } else { + for (Apk apk : app.apks) { + if (!apk.updated) { + // The package hasn't been updated, so this is a + // version that's no longer available. + db.delete(TABLE_APK, "id = '" + app.id + + " and version ='" + apk.version + "'", null); + } + } + } + } + updateApps = null; + } + + // Called during update to supply new details for an application (or + // details of a completely new one). + public void updateApplication(App upapp) { + + if (updateApps == null) { + return; + } + + boolean found = false; + for (App app : updateApps) { + if (app.id.equals(upapp.id)) { + Log.d("FDroid", "AppUpdate: " + app.id + + " is already in the database."); + updateAppIfDifferent(app, upapp); + app.updated = true; + found = true; + for (Apk upapk : upapp.apks) { + boolean afound = false; + for (Apk apk : app.apks) { + if (apk.version.equals(upapk.version)) { + Log.d("FDroid", "AppUpdate: " + apk.version + + " is a known version."); + updateApkIfDifferent(apk, upapk); + apk.updated = true; + afound = true; + break; + } + } + if (!afound) { + // A new version of this application. + Log.d("FDroid", "AppUpdate: " + upapk.version + + " is a new version."); + updateApkIfDifferent(null, upapk); + upapk.updated = true; + app.apks.add(upapk); + app.hasUpdates = true; + } + } + } + } + if (!found) { + // It's a brand new application... + Log + .d("FDroid", "AppUpdate: " + upapp.id + + " is a new application."); + updateAppIfDifferent(null, upapp); + for (Apk upapk : upapp.apks) { + updateApkIfDifferent(null, upapk); + } + upapp.updated = true; + updateApps.add(upapp); + } + + } + + // Update application details in the database, if different to the + // previous ones. + // 'oldapp' - previous details - i.e. what's in the database. + // If null, this app is not in the database at all and + // should be added. + // 'upapp' - updated details + private void updateAppIfDifferent(App oldapp, App upapp) { + ContentValues values = new ContentValues(); + values.put("id", upapp.id); + values.put("name", upapp.name); + values.put("summary", upapp.summary); + values.put("icon", upapp.icon); + values.put("description", upapp.description); + values.put("license", upapp.license); + values.put("webURL", upapp.webURL); + values.put("trackerURL", upapp.trackerURL); + values.put("sourceURL", upapp.sourceURL); + values.put("installedVersion", upapp.installedVersion); + values.put("hasUpdates", upapp.hasUpdates ? 1 : 0); + if (oldapp != null) { + db.update(TABLE_APP, values, "id = '" + oldapp.id + "'", null); + } else { + db.insert(TABLE_APP, null, values); + } + } + + // Update apk details in the database, if different to the + // previous ones. + // 'oldapk' - previous details - i.e. what's in the database. + // If null, this apk is not in the database at all and + // should be added. + // 'upapk' - updated details + private void updateApkIfDifferent(Apk oldapk, Apk upapk) { + ContentValues values = new ContentValues(); + values.put("id", upapk.id); + values.put("version", upapk.version); + values.put("server", upapk.server); + values.put("hash", upapk.hash); + values.put("apkName", upapk.apkName); + if (oldapk != null) { + db.update(TABLE_APK, values, "id = '" + oldapk.id + + "' and version = '" + oldapk.version + "'", null); + } else { + db.insert(TABLE_APK, null, values); + } + } + + public void setInstalledVersion(String id, String version) { + ContentValues values = new ContentValues(); + values.put("installedVersion", version); + db.update(TABLE_APP, values, "id = '" + id + "'", null); + } + + // Get a list of the configured repositories. + public Vector getRepos() { + Vector repos = new Vector(); + Cursor c = null; + try { + c = db.rawQuery("select address, inuse, priority from " + + TABLE_REPO + " order by priority", null); + c.moveToFirst(); + while(!c.isAfterLast()) { + Repo repo = new Repo(); + repo.address = c.getString(0); + repo.inuse = (c.getInt(1) == 1); + repo.priority = c.getInt(2); + repos.add(repo); + c.moveToNext(); + } + } catch (Exception e) { + } finally { + if (c != null) { + c.close(); + } + } + return repos; + } + + public void changeServerStatus(String address) { + db.rawQuery("update " + TABLE_REPO + + " set inuse=1-inuse where address='" + address + "'", null); + } + + public void addServer(String address, int priority) { + ContentValues values = new ContentValues(); + values.put("address", address); + values.put("inuse", 1); + values.put("priority", priority); + db.insert(TABLE_REPO, null, values); + } + + public void removeServers(Vector addresses) { + for (String address : addresses) { + db.delete(TABLE_REPO, "address = '" + address + "'", null); + } + } + +} diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java new file mode 100644 index 000000000..2281d90e1 --- /dev/null +++ b/src/org/fdroid/fdroid/FDroid.java @@ -0,0 +1,538 @@ +/* + * Copyright (C) 2010 Ciaran Gultnieks, ciaran@ciarang.com + * Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.fdroid.fdroid; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; +import java.util.Vector; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; + +import org.fdroid.fdroid.R; + +import android.R.drawable; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.app.TabActivity; +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TabHost; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.TabHost.TabSpec; + +public class FDroid extends TabActivity implements OnItemClickListener { + + private class AppListAdapter extends BaseAdapter { + + private List items = new ArrayList(); + + public AppListAdapter(Context context) { + } + + public void addItem(DB.App app) { + items.add(app); + } + + 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; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View v = convertView; + if (v == null) { + LayoutInflater vi = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + v = vi.inflate(R.layout.applistitem, null); + } + DB.App app = items.get(position); + + TextView name = (TextView) v.findViewById(R.id.name); + name.setText(app.name); + + String vs = " versions available"; + int numav = app.apks.size(); + if (numav == 1) + vs = " version available"; + TextView status = (TextView) v.findViewById(R.id.status); + status.setText(numav + vs); + + TextView license = (TextView) v.findViewById(R.id.license); + license.setText(app.license); + + TextView summary = (TextView) v.findViewById(R.id.summary); + summary.setText(app.summary); + + ImageView icon = (ImageView) v.findViewById(R.id.icon); + String iconpath = new String(FDroid.this + .getString(R.string.icons_path) + + app.icon); + File icn = new File(iconpath); + if (icn.exists() && icn.length() > 0) { + new Uri.Builder().build(); + icon.setImageURI(Uri.parse(iconpath)); + } else { + icon.setImageResource(android.R.drawable.sym_def_app_icon); + } + + return v; + } + } + + private String LOCAL_PATH = "/sdcard/.fdroid"; + private String XML_PATH = LOCAL_PATH + "/remapklst.xml"; + + private static final int REQUEST_APPDETAILS = 0; + private static final int REQUEST_MANAGEREPOS = 1; + + private static final int UPDATE_REPO = Menu.FIRST; + private static final int MANAGE_REPO = Menu.FIRST + 1; + private static final int RESET_DB = Menu.FIRST + 2; + private static final int ABOUT = Menu.FIRST + 3; + + private DB db = null; + + // Apps that are available to be installed + private AppListAdapter apps_av = new AppListAdapter(this); + + // Apps that are installed + private AppListAdapter apps_in = new AppListAdapter(this); + + // Apps that can be upgraded + private AppListAdapter apps_up = new AppListAdapter(this); + + private ProgressDialog pd; + + private Context mctx = this; + + private static final String TAB_IN = "INST"; + private static final String TAB_UN = "UNIN"; + private static final String TAB_UP = "UPDT"; + private TabHost tabHost; + private TabSpec ts; + private TabSpec ts1; + private TabSpec tsUp; + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + setContentView(R.layout.fdroid); + + File local_path = new File(LOCAL_PATH); + if (!local_path.exists()) + local_path.mkdir(); + + File icon_path = new File(this.getString(R.string.icons_path)); + if (!icon_path.exists()) + icon_path.mkdir(); + + db = new DB(this); + + tabHost = getTabHost(); + createTabs(); + + Intent i = getIntent(); + if (i.hasExtra("uri")) { + Intent call = new Intent(this, ManageRepo.class); + call.putExtra("uri", i.getStringExtra("uri")); + startActivityForResult(call, REQUEST_MANAGEREPOS); + } + + } + + @Override + protected void onStart() { + super.onStart(); + populateLists(true); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + + super.onCreateOptionsMenu(menu); + menu.add(Menu.NONE, UPDATE_REPO, 1, R.string.menu_update_repo).setIcon( + android.R.drawable.ic_menu_rotate); + menu.add(Menu.NONE, MANAGE_REPO, 2, R.string.menu_manage).setIcon( + android.R.drawable.ic_menu_agenda); + menu.add(Menu.NONE, RESET_DB, 4, "Reset DB").setIcon( + android.R.drawable.ic_menu_revert); + menu.add(Menu.NONE, ABOUT, 5, R.string.menu_about).setIcon( + android.R.drawable.ic_menu_help); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + + switch (item.getItemId()) { + + case UPDATE_REPO: + updateRepos(); + return true; + + case MANAGE_REPO: + Intent i = new Intent(this, ManageRepo.class); + startActivityForResult(i, REQUEST_MANAGEREPOS); + return true; + + case RESET_DB: + db.reset(); + populateLists(true); + return true; + + case ABOUT: + LayoutInflater li = LayoutInflater.from(this); + View view = li.inflate(R.layout.about, null); + Builder p = new AlertDialog.Builder(this).setView(view); + final AlertDialog alrt = p.create(); + alrt.setIcon(R.drawable.icon); + alrt.setTitle(getString(R.string.about_title)); + alrt.setButton(AlertDialog.BUTTON_NEUTRAL, + getString(R.string.about_website), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int whichButton) { + Uri uri = Uri + .parse(getString(R.string.url_website)); + startActivity(new Intent(Intent.ACTION_VIEW, uri)); + } + }); + alrt.setButton(AlertDialog.BUTTON_NEGATIVE, "Ok", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int whichButton) { + } + }); + alrt.show(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + + switch (requestCode) { + case REQUEST_APPDETAILS: + populateLists(false); + break; + case REQUEST_MANAGEREPOS: + if (data.hasExtra("update")) { + AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this); + ask_alrt.setTitle(getString(R.string.repo_update_title)); + ask_alrt.setIcon(android.R.drawable.ic_menu_rotate); + ask_alrt.setMessage(getString(R.string.repo_alrt)); + ask_alrt.setPositiveButton(getString(R.string.yes), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int whichButton) { + updateRepos(); + } + }); + ask_alrt.setNegativeButton(getString(R.string.no), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int whichButton) { + return; + } + }); + AlertDialog alert = ask_alrt.create(); + alert.show(); + } + break; + + } + } + + private void createTabs() { + tabHost.clearAllTabs(); + + // TabContentFactory that can generate the appropriate list for each + // tab... + TabHost.TabContentFactory tf = new TabHost.TabContentFactory() { + @Override + public View createTabContent(String tag) { + + AppListAdapter ad; + if (tag.equals(TAB_IN)) + ad = apps_in; + else if (tag.equals(TAB_UP)) + ad = apps_up; + else + ad = apps_av; + + ListView lst = new ListView(FDroid.this); + lst.setOnItemClickListener(FDroid.this); + lst.setAdapter(ad); + return lst; + } + }; + + // Create the tab of installed apps... + ts = tabHost.newTabSpec(TAB_IN); + ts.setIndicator(getString(R.string.tab_installed), getResources() + .getDrawable(drawable.star_off)); + ts.setContent(tf); + + // Create the tab of apps with updates... + tsUp = tabHost.newTabSpec(TAB_UP); + tsUp.setIndicator(getString(R.string.tab_updates), getResources() + .getDrawable(drawable.star_on)); + tsUp.setContent(tf); + + // Create the tab of available apps... + ts1 = tabHost.newTabSpec(TAB_UN); + ts1.setIndicator(getString(R.string.tab_noninstalled), getResources() + .getDrawable(drawable.ic_input_add)); + ts1.setContent(tf); + + tabHost.addTab(ts1); + tabHost.addTab(ts); + tabHost.addTab(tsUp); + + } + + // Populate the lists. + // 'update' - true to update the installed status of the applications + // by asking the system. + private void populateLists(boolean update) { + + Vector apps = db.getApps(null, null, update); + Log.d("FDroid", "Updating lists - " + apps.size() + " apps in total"); + + apps_in.clear(); + apps_av.clear(); + apps_up.clear(); + for (DB.App app : apps) { + if (app.installedVersion == null) { + apps_av.addItem(app); + } else { + apps_in.addItem(app); + if (app.hasUpdates) + apps_up.addItem(app); + } + } + + // Tell the lists that the data behind the adapter has changed, so + // they can refresh... + apps_av.notifyDataSetChanged(); + apps_in.notifyDataSetChanged(); + apps_up.notifyDataSetChanged(); + + } + + public boolean updateRepos() { + pd = ProgressDialog.show(this, getString(R.string.process_wait_title), + getString(R.string.process_update_msg), true); + pd.setIcon(android.R.drawable.ic_dialog_info); + + // Check for connection first! + ConnectivityManager netstate = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + if (netstate.getNetworkInfo(1).getState() == NetworkInfo.State.CONNECTED + || netstate.getNetworkInfo(0).getState() == NetworkInfo.State.CONNECTED) { + new Thread() { + public void run() { + try { + db.beginUpdate(); + Vector repos = db.getRepos(); + for (DB.Repo repo : repos) { + if (repo.inuse) { + downloadRepoIndex(repo.address); + xmlPass(repo.address); + } + } + db.endUpdate(); + } catch (Exception e) { + Log.d("FDroid", "Exception while updating - " + + e.getMessage()); + } + update_handler.sendEmptyMessage(0); + } + }.start(); + return true; + } else { + pd.dismiss(); + Toast.makeText(FDroid.this, getString(R.string.connection_error), + Toast.LENGTH_LONG).show(); + return false; + } + } + + /* + * Pass XML info to BD a xml file must exists... + */ + private void xmlPass(String srv) { + SAXParserFactory spf = SAXParserFactory.newInstance(); + try { + SAXParser sp = spf.newSAXParser(); + XMLReader xr = sp.getXMLReader(); + RepoXMLHandler handler = new RepoXMLHandler(this, srv); + xr.setContentHandler(handler); + + InputStreamReader isr = new FileReader(new File(XML_PATH)); + InputSource is = new InputSource(isr); + xr.parse(is); + File xml_file = new File(XML_PATH); + xml_file.delete(); + } catch (IOException e) { + e.printStackTrace(); + } catch (SAXException e) { + e.printStackTrace(); + } catch (ParserConfigurationException e) { + e.printStackTrace(); + } + } + + // Download a repo index to a temporary file on the SD card. + private void downloadRepoIndex(String srv) { + try { + BufferedInputStream getit = new BufferedInputStream(new URL(srv + + "/index.xml").openStream()); + + File file_teste = new File(XML_PATH); + if (file_teste.exists()) + file_teste.delete(); + + FileOutputStream saveit = new FileOutputStream(XML_PATH); + BufferedOutputStream bout = new BufferedOutputStream(saveit, 1024); + byte data[] = new byte[1024]; + + int readed = getit.read(data, 0, 1024); + while (readed != -1) { + bout.write(data, 0, readed); + readed = getit.read(data, 0, 1024); + } + bout.close(); + getit.close(); + saveit.close(); + } catch (UnknownHostException e) { + Message msg = new Message(); + msg.obj = new String(srv); + error_handler.sendMessage(msg); + } catch (Exception e) { + } + } + + /* + * Handlers for thread functions that need to access GUI + */ + private Handler update_handler = new Handler() { + @Override + public void handleMessage(Message msg) { + populateLists(true); + if (pd.isShowing()) + pd.dismiss(); + } + }; + + private Handler error_handler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (pd.isShowing()) + pd.dismiss(); + AlertDialog p = new AlertDialog.Builder(mctx).create(); + p.setTitle(getString(R.string.connection_timeout)); + p.setIcon(android.R.drawable.ic_dialog_alert); + p.setMessage(getString(R.string.connection_error_msg) + ": < " + + msg.obj.toString() + " >"); + p.setButton(getString(R.string.ok), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + return; + } + }); + p.show(); + } + }; + + // Handler for a click on one of the items in an application list. Pops + // up a dialog that shows the details of the application and all its + // available versions, with buttons to allow installation etc. + public void onItemClick(AdapterView arg0, View arg1, final int arg2, + long arg3) { + + final DB.App app; + String curtab = tabHost.getCurrentTabTag(); + if (curtab.equalsIgnoreCase(TAB_IN)) { + app = (DB.App) apps_in.getItem(arg2); + } else if (curtab.equalsIgnoreCase(TAB_UP)) { + app = (DB.App) apps_up.getItem(arg2); + } else { + app = (DB.App) apps_av.getItem(arg2); + } + + Intent intent = new Intent(this, AppDetails.class); + intent.putExtra("appid", app.id); + startActivityForResult(intent, REQUEST_APPDETAILS); + + } + +} diff --git a/src/org/fdroid/fdroid/ManageRepo.java b/src/org/fdroid/fdroid/ManageRepo.java new file mode 100644 index 000000000..c83ea9b73 --- /dev/null +++ b/src/org/fdroid/fdroid/ManageRepo.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt + * Copyright (C) 2010 Ciaran Gultnieks, ciaran@ciarang.com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.fdroid.fdroid; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Vector; + +import org.fdroid.fdroid.R; + +import android.app.AlertDialog; +import android.app.ListActivity; +import android.app.AlertDialog.Builder; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.SimpleAdapter; + +public class ManageRepo extends ListActivity { + + private DB db = null; + + private final int ADD_REPO = 1; + private final int REM_REPO = 2; + + private boolean changed = false; + + private Vector repos; + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + setContentView(R.layout.repolist); + + db = new DB(this); + + } + + @Override + protected void onResume() { + + super.onResume(); + redraw(); + } + + private void redraw() { + repos = db.getRepos(); + + List> result = new ArrayList>(); + Map server_line; + + for (DB.Repo repo : repos) { + server_line = new HashMap(); + server_line.put("address", repo.address); + if (repo.inuse) { + server_line.put("inuse", R.drawable.btn_check_on); + } else { + server_line.put("inuse", R.drawable.btn_check_off); + } + result.add(server_line); + } + SimpleAdapter show_out = new SimpleAdapter(this, result, + R.layout.repolisticons, new String[] { "address", "inuse" }, + new int[] { R.id.uri, R.id.img }); + + setListAdapter(show_out); + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + + super.onListItemClick(l, v, position, id); + db.changeServerStatus(repos.get(position).address); + changed = true; + redraw(); + } + + public boolean onCreateOptionsMenu(Menu menu) { + + super.onCreateOptionsMenu(menu); + menu.add(Menu.NONE, ADD_REPO, 1, R.string.menu_add_repo).setIcon( + android.R.drawable.ic_menu_add); + menu.add(Menu.NONE, REM_REPO, 2, R.string.menu_rem_repo).setIcon( + android.R.drawable.ic_menu_close_clear_cancel); + return true; + } + + @Override + public boolean onMenuItemSelected(int featureId, MenuItem item) { + + super.onMenuItemSelected(featureId, item); + LayoutInflater li = LayoutInflater.from(this); + + switch (item.getItemId()) { + case ADD_REPO: + View view = li.inflate(R.layout.addrepo, null); + Builder p = new AlertDialog.Builder(this).setView(view); + final AlertDialog alrt = p.create(); + + alrt.setIcon(android.R.drawable.ic_menu_add); + alrt.setTitle(getString(R.string.repo_add_title)); + alrt.setButton(getString(R.string.repo_add_add), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + EditText uri = (EditText) alrt + .findViewById(R.id.edit_uri); + String uri_str = uri.getText().toString(); + db.addServer(uri_str, 10); + changed = true; + redraw(); + } + }); + + alrt.setButton2(getString(R.string.cancel), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + return; + } + }); + alrt.show(); + return true; + + case REM_REPO: + final Vector rem_lst = new Vector(); + CharSequence[] b = new CharSequence[repos.size()]; + for (int i = 0; i < repos.size(); i++) { + b[i] = repos.get(i).address; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.repo_delete_title)); + builder.setIcon(android.R.drawable.ic_menu_close_clear_cancel); + builder.setMultiChoiceItems(b, null, + new DialogInterface.OnMultiChoiceClickListener() { + public void onClick(DialogInterface dialog, + int whichButton, boolean isChecked) { + if (isChecked) { + rem_lst + .addElement(repos.get(whichButton).address); + } else { + rem_lst + .removeElement(repos.get(whichButton).address); + } + } + }); + builder.setPositiveButton(getString(R.string.ok), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int whichButton) { + db.removeServers(rem_lst); + changed = true; + redraw(); + } + }); + builder.setNegativeButton(getString(R.string.cancel), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int whichButton) { + return; + } + }); + AlertDialog alert = builder.create(); + alert.show(); + return true; + } + return true; + } + + @Override + public void finish() { + Intent ret = new Intent(); + if (changed) + ret.putExtra("update", true); + this.setResult(RESULT_OK, ret); + db.close(); + super.finish(); + } + +} diff --git a/src/org/fdroid/fdroid/Md5Handler.java b/src/org/fdroid/fdroid/Md5Handler.java new file mode 100644 index 000000000..8ae5864e5 --- /dev/null +++ b/src/org/fdroid/fdroid/Md5Handler.java @@ -0,0 +1,41 @@ +package org.fdroid.fdroid; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class Md5Handler { + + private MessageDigest digest; + + public Md5Handler() { + try { + digest = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + } + + public String md5Calc(File f) { + String md5hash = null; + byte[] buffer = new byte[1024]; + int read = 0; + + try { + InputStream is = new FileInputStream(f); + while ((read = is.read(buffer)) > 0) { + digest.update(buffer, 0, read); + } + byte[] md5sum = digest.digest(); + BigInteger bigInt = new BigInteger(1, md5sum); + md5hash = bigInt.toString(16); + } catch (Exception e) { + } + + return md5hash; + } + +} diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java b/src/org/fdroid/fdroid/RepoXMLHandler.java new file mode 100644 index 000000000..ba7729d4d --- /dev/null +++ b/src/org/fdroid/fdroid/RepoXMLHandler.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2010 Ciaran Gultnieks, ciaran@ciarang.com + * Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.fdroid.fdroid; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.net.URL; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import org.fdroid.fdroid.R; + +import android.content.Context; +import android.util.Log; + +public class RepoXMLHandler extends DefaultHandler { + + Context mctx; + String mserver; + + private DB db = null; + + private DB.App curapp = null; + private DB.Apk curapk = null; + private String curel = null; + + public RepoXMLHandler(Context ctx, String srv) { + mctx = ctx; + mserver = srv; + db = new DB(mctx); + db.beginUpdate(); + } + + @Override + public void endDocument() throws SAXException { + super.endDocument(); + db.endUpdate(); + db.close(); + } + + @Override + public void characters(char[] ch, int start, int length) + throws SAXException { + + super.characters(ch, start, length); + + String str = new String(ch).substring(start, start + length); + if (curapk != null && curel != null) { + if (curel == "version") + curapk.version = str; + else if (curel == "hash") + curapk.hash = str; + else if (curel == "apkname") + curapk.apkName = str; + } else if (curapp != null && curel != null) { + if (curel == "id") + curapp.id = str; + else if (curel == "name") + curapp.name = str; + else if (curel == "icon") + curapp.icon = str; + else if (curel == "description") + curapp.description = str; + else if (curel == "summary") + curapp.summary = str; + else if (curel == "license") + curapp.license = str; + else if (curel == "source") + curapp.sourceURL = str; + else if (curel == "web") + curapp.webURL = str; + else if (curel == "tracker") + curapp.trackerURL = str; + } + } + + @Override + public void endElement(String uri, String localName, String qName) + throws SAXException { + + super.endElement(uri, localName, qName); + + if (localName == "application" && curapp != null) { + Log.d("FDroid", "Repo: Updating application " + curapp.id); + db.updateApplication(curapp); + getIcon(curapp); + curapp = null; + } else if (localName == "package" && curapk != null && curapp != null) { + Log.d("FDroid", "Repo: Package added (" + curapk.version + ")"); + curapp.apks.add(curapk); + curapk = null; + } else { + curel = null; + } + + } + + @Override + public void startElement(String uri, String localName, String qName, + Attributes attributes) throws SAXException { + + super.startElement(uri, localName, qName, attributes); + if (localName == "application" && curapp == null) { + Log.d("FDroid", "Repo: Found application at " + mserver); + curapp = new DB.App(); + } else if (localName == "package" && curapp != null && curapk == null) { + Log.d("FDroid", "Repo: Found package for " + curapp.id); + curapk = new DB.Apk(); + curapk.id = curapp.id; + curapk.server = mserver; + } else { + curel = localName; + } + } + + private void getIcon(DB.App app) { + try { + + String destpath = mctx.getString(R.string.icons_path) + app.icon; + BufferedInputStream getit = new BufferedInputStream(new URL(mserver + + "/icons/" + app.icon).openStream()); + File f = new File(destpath); + if (f.exists()) + f.delete(); + + FileOutputStream saveit = new FileOutputStream(destpath); + BufferedOutputStream bout = new BufferedOutputStream(saveit, 1024); + byte data[] = new byte[1024]; + + int readed = getit.read(data, 0, 1024); + while (readed != -1) { + bout.write(data, 0, readed); + readed = getit.read(data, 0, 1024); + } + bout.close(); + getit.close(); + saveit.close(); + } catch (Exception e) { + + } + } + +}