From 0b71cb7e7331fb73ee762225d0225fd92ce99010 Mon Sep 17 00:00:00 2001 From: Ciaran Gultnieks Date: Tue, 19 Oct 2010 23:24:04 +0100 Subject: [PATCH] Initial files --- .classpath | 7 + .gitignore | 4 + .project | 33 ++ AndroidManifest.xml | 22 + build.properties | 17 + build.xml | 67 +++ default.properties | 13 + res/drawable/btn_check_off.png | Bin 0 -> 1325 bytes res/drawable/btn_check_on.png | Bin 0 -> 1661 bytes res/drawable/icon.png | Bin 0 -> 2017 bytes res/layout/about.xml | 38 ++ res/layout/addrepo.xml | 37 ++ res/layout/apklistitem.xml | 20 + res/layout/appdetails.xml | 41 ++ res/layout/applistitem.xml | 39 ++ res/layout/fdroid.xml | 16 + res/layout/remrepo.xml | 36 ++ res/layout/repolist.xml | 32 ++ res/layout/repolisticons.xml | 46 ++ res/values/alertstr.xml | 37 ++ res/values/menu.xml | 37 ++ res/values/path.xml | 23 + res/values/strings.xml | 61 +++ src/org/fdroid/fdroid/AppDetails.java | 354 ++++++++++++++ src/org/fdroid/fdroid/DB.java | 445 ++++++++++++++++++ src/org/fdroid/fdroid/FDroid.java | 538 ++++++++++++++++++++++ src/org/fdroid/fdroid/ManageRepo.java | 205 +++++++++ src/org/fdroid/fdroid/Md5Handler.java | 41 ++ src/org/fdroid/fdroid/RepoXMLHandler.java | 164 +++++++ 29 files changed, 2373 insertions(+) create mode 100644 .classpath create mode 100644 .gitignore create mode 100644 .project create mode 100644 AndroidManifest.xml create mode 100644 build.properties create mode 100644 build.xml create mode 100644 default.properties create mode 100644 res/drawable/btn_check_off.png create mode 100644 res/drawable/btn_check_on.png create mode 100644 res/drawable/icon.png create mode 100644 res/layout/about.xml create mode 100644 res/layout/addrepo.xml create mode 100644 res/layout/apklistitem.xml create mode 100644 res/layout/appdetails.xml create mode 100644 res/layout/applistitem.xml create mode 100644 res/layout/fdroid.xml create mode 100644 res/layout/remrepo.xml create mode 100644 res/layout/repolist.xml create mode 100644 res/layout/repolisticons.xml create mode 100644 res/values/alertstr.xml create mode 100644 res/values/menu.xml create mode 100644 res/values/path.xml create mode 100644 res/values/strings.xml create mode 100644 src/org/fdroid/fdroid/AppDetails.java create mode 100644 src/org/fdroid/fdroid/DB.java create mode 100644 src/org/fdroid/fdroid/FDroid.java create mode 100644 src/org/fdroid/fdroid/ManageRepo.java create mode 100644 src/org/fdroid/fdroid/Md5Handler.java create mode 100644 src/org/fdroid/fdroid/RepoXMLHandler.java 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 0000000000000000000000000000000000000000..aa585d4056fde73e5c5217ee21e608af27af61c2 GIT binary patch literal 1325 zcmZXU3sjP47{@V}Yo<2Wyku>qIlGu=N@aE|Qv%B}L$obz&fLh&OL{uCWivTt7qpty zM5m$|UJymmQ1F5mP*D^SQBd)U;tfR<(B^FQ?QK-2bM~C`{-59Tf4=ja_kF+bd`W1Z z6UK&ChA=ova-CqtkGz| z1H;3^TCG;cEEY?xRxd3r>HNjTMU_gmu&@A1rBXRRKM!DTZcd?40GOSfotc>d)97@1 zQBe_?49w~2>8Yuy$;nBXOh%*8m`o;{&91Dh92pq_-HC~b>gwv++FFrF)Z5$3<#K^J zK0ZD+Ha0psT2WEa(b3V`+6qjttD&Kx=g*&)mzTG-wRLrMH8nK>Un-Rj3=EW(mbSFC z@cDcmk5^w`KR7rDT77+e5{aa$s)|ab78e)S)zx)(clY=AgO*qbAxmgEb4*+i7ya^QCkIn>&$K%0daH99ldteZe5>Whc731Rv zgKc&|_-^-R$RKFVd=P?;R723W6X3=E1VIrH6b%SOj4lEFRY$S96!+H0z0J_AcX0O| zBZI%uEsMAMS@O`Z#cp*LfUHA*) z`QRS3CHsn~4!h-hjPGY=2KRaHdg`XoiS$`18tsfV>&mb}hbCDp;XE7!YmbPl&(gG) z%I02bvc|5v|Ohl;?Mo+?5Wh$&#C@*OG@e{!lw4GPZipC@C^gbDK;_!gG!O5 zIN!ka0cS4%GQvM0KG)yp{>@l7MRZ_K&(Bs{=d2^5dcuQ`BB+3 z+VrI*)-OfQLGPZ-BqJ``MgHKGygl(wf;`3VvA{DoK0$`lz6kQ>?e9v8Y9Vo+?N~hC2Zc^ydb@&v@ z@m*C9lrN#VBZ1L56x&9E&$(w&)M zXBzL|_G3jyx#QO&*b~JW9&2z&VSH|un za>K3}&@bP4pyyaseP9l8>w--((|SnC)&K66q}^{g8`i+mmp(nRpjt4~nEZ<3Ci!Z{YN2?t4e$9c*7cZH?dn4$gGY-5Z9RLjbLs4~os8?MJ(1!m3| y3HQ=8i_U*gJj5;N2zs?zq}+RrHTL>t1Wu1tVto3MGV0Aw2IhtGLDCQx6aEEA{JvZO literal 0 HcmV?d00001 diff --git a/res/drawable/btn_check_on.png b/res/drawable/btn_check_on.png new file mode 100644 index 0000000000000000000000000000000000000000..fdaab13449468fd94b923ae67b83073c7ebafeb6 GIT binary patch literal 1661 zcmZXT3pCVu6vuBI7B#99J%%lbokD6Wt8LS%JThWplE+SO6IpgQc^pyJYrG#5i;yWH z86=f8#dtNC2Qyy7cuRSUkkbSG_h;qQIeX9f-TV1|?>*=LKll88;g+OhI0d`{0N{wG zMr2ffKxem{G|FbKAMgNDVV36B#u$bn=l@YzS^4VKtJ&GvckkX!Pfw$PuCA_`nVBUn zDJhwnntJ>8?UFw^IXN*g@#f7NB*(|c$HvAGjE;_ujEo={9v&VV8fx!ow~ewraN~dn z!vmRvgM$MD1Fv7dZk4oZ2W!JkfO`ORU~}21i$2}o-+wCp6eI&w0dN4A@QCvWwO+n_ z+1J?FsMfd^0w+UeSo4hd?tx3^0q z602w{r~@bgMTX+DnrF!G=;&x|Z54~fTppLkpymIQZy99?B7h2jd`K-$ZEI^oV=XN$ z&CShCO-;2-v9Y0{K_n94Bk_;{Pz~@9AQFP_ z1`CBkiwFzjd&X$2zP`S$t`2t_2W0?4q#@`|ka@WIjypSq4M>APAmH=)Zs~6D0w4-J z6Fkjn=7sD+)Z+1YTrQWyAVDJl1E3NT3KNiDTU%RGQ$t8202iPcfDdOg&LY3Mx|+k` zu%5GUX*g&BXp@2V)p`s))T*kgV&pOaZM_xnY4}fMcB6@>Z^!d#`rIg_@$;SH`10M{lAjAm>-~%y9f_& zPx-~Z^EkozXRvRna8ddEhwh_WF8W!%b zNz|rm#FHrsI6Y41-%3}h@|w8x9r}BBIrA*Ed1iY|h&cmiv+OaFt&gQ(OmJ5Ge&FYF z_d`Ra*Hl$MYbr%#G^;yZCB-O37`VH6QpU@IgXhTlwi|p0Uyc*A$ZW1<4nw$^er?YC zqb|k2ASqU#>i2m?K){hqDQW5X1+|rm_Zky-`J1d%2+;{P$qv=^CuP$PuQzepT{T20 zyeb~OA{(?>*51flqghvBcd|?CLrVFDZF~0|lzM#GQAyj8VUb$Q`J>xJH7rCYM)?l? zauPNE7AviuI5;5}9qv?pBr%T8=_Hz_Ic!LcujyrnRv$Mr4bVwTO1XLSV=CKKShnz; zVhxp=Io-5Xz!xgX(W(qXW92HXej*U`C+$j2w{z=+fjZ|jbPv;C5lD;!(URL2+!Y)& zoz}_7k=51@7}{v)Ci1C6W@ZAu$aanHnp1n;tDjJd=Vey+t8=y1CZA4cc|0?-xA8Mh zXPH?$&%3nACDIWM}z}A~pVzvmc!V<|Sv!Y3n%oH^j@nyG#ryJ^j=O^uSp0_QySSbOWd&2Rg_Ag0LBv1tV7_~cT@FGSZfwZ1t$(~<424P)kX&wklRmB~uLX{3; ztu1>?c(Qhjs=q6DU*3@)cgnA7UX>c6rn2dK6)aFG{%~1LY;=r-5Le5|98r13laEcV oG~Sf_>2pw53%Bn2++@K=`S2{3msC=4-{MOq8k3A39z7rUFW0$C>;M1& literal 0 HcmV?d00001 diff --git a/res/drawable/icon.png b/res/drawable/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..717577be38a89e6617caeb0ed2678ae4ff000271 GIT binary patch literal 2017 zcmV<72Oju|P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L00buh00bui*-ssS000McNkl4GKn36e>VP+Z zjm~R;hEPE7lkeOvI>+V$mw*)D&N6;0Fcnw^n9jUoI%8rbk5zB}|1+QwkgWutZP6rf zI2;M?$^5Qvqfj^!#Nqt=z-L+v%Qn7OW|{XQFbX)^Q6;;k9moj2CL{Rcz-_>sj38TE zu-(82V5Rd}5pWXdbuBy5Apon^j%yD7EucJO=sDmhu+7PXwrnPFEELd}WPox>DQiLj zofq_JT}z>*b4TCNcg~=LLRB)KJ_1Y!hB|7~*1~K99tUOtFJulRs-oc^&fE9>E&=dl zupC=)DVTPm-=h^St*^T+p68kaX-owCQrr+lC`md*josPRx zr{nI_nYcSum(WsY6Pj^0;WFwIE~7rF8VyO+_#~+s|4AxVV@k2k8O?yLRXeNi{=J+_ zt_4oCqVbP8{fq@X6AI`%oblO4V`_z`!drE6(b4Hh5K4aSl;Tq{n^F|tKcHUms>QzB z{0Bn;eUcOW?Z7&xWv>hc^e38Q%t|3A_y>VsWhllU!;#>OP(a@a4ADy5L+vZp(9(wn zlc5X%sfDod?ya$1QVllqaeq6;`e`!jPOmsYY0Uu<<@wsp$I1;$G)f@;%f&%zW z3n`Sq3SeiZ7}t z#Of@PI}^|VqqQ6m4sBXWT|u_M-2FZDnKgi3(@XIT_JT`X4OU8-{<9@Q5%@;=L4kok z8b;C75(>u_Av_AIzf~P6Yej-Z=eE-}ct`ZWJE8|x%%XAADM}wL=ggYpxO!?7-`9_e zd(YA58v{wy#KD4|ll$aSLS=WJ2ar;tcojz1zg|3l1`PxSxf;yo>%7 zJ@6K~h+Q_QuZgl{^$`|)v696bCQvrKC`XcX?VoY=@7~Hkch&Ke zZ~mP(x7YH~p>tGjJ;B21dpKQniAR=>p=XJQKZHNRr71TCfav)YHAgQnZ~0h+B6w-- z5qy4)HGiH;_{|_sZBEywryt%&GGXzxc_TQq`z*F~gX}=`V!E!}GNu^IwE0CO$j>XM z;VaUJM@6@QJVWC0ku_^ z5F)Ksu}kSwr{+;_3>7Nw=LKNrrsD)39LAQ_9}vA@FyYICnDo^$UfXean2j{ns-*^Vk87?E8d~ zcl6=Ojpb>ue{Dv znW{S$ywa)_v~)ha(Nt*F-tEtBz;q@8`#}A`H8|H%pwIXse|N z1@!H}iy0r8z=%*lm+b+tLjir5bC?(&3h3i=zi<*PUO$0(Pmcv5*tNBqjmzI>_qG}| zx61srpXUe9-PvyN8HcO+&SB=xP(XLLSlEu=3prQfYjUZ~E=|FuDJY7-wk1Z&#xm`k z8o5wF7u~8t*(hb163eu*Hhi6F3jgQUQ2_6CLrk?@0>DXR-MI(tl+#ozeq+1za@PUV z-^rf@@G3Aqi(7d{0W_SOj{CFt4Y_*F1!p??8$sS#6^;ZWz!$O!L@1zJ8Gc)UZ)Iaa zOKrcI#5V!l1kk+zjMlHb+Ha5ZN#M=`us4ggAIcKgZ?pJSU)LRQirXsSK#R8b?ks*0 zV0#M*zvsGhFyj~dvEfK?d=7r2!ja&_&YJ(fH>2yW{ZcYh00000NkvXXu0mjfFOIsU literal 0 HcmV?d00001 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) { + + } + } + +}