Merge branch 'development'

This commit is contained in:
Daniel Martí 2014-01-10 22:06:43 +01:00
commit 7c472c8e18
73 changed files with 2751 additions and 954 deletions

10
.gitmodules vendored
View File

@ -1,4 +1,12 @@
[submodule "extern/Universal-Image-Loader"]
path = extern/Universal-Image-Loader
url = https://github.com/nostra13/Android-Universal-Image-Loader
ignore = dirty
ignore = dirty
[submodule "extern/MemorizingTrustManager"]
path = extern/MemorizingTrustManager
url = https://github.com/ge0rg/MemorizingTrustManager.git
ignore = dirty
[submodule "extern/AndroidPinning"]
path = extern/AndroidPinning
url = https://github.com/binaryparadox/AndroidPinning.git
ignore = dirty

View File

@ -112,6 +112,10 @@
</intent-filter>
</activity>
<activity
android:name=".views.RepoDetailsActivity"
android:label="@string/menu_manage" />
<activity
android:name=".AppDetails"
android:label="@string/app_details"
@ -196,6 +200,9 @@
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<!--Used for SSL TOFU, supported by extern/MemorizingTrustManager lib -->
<activity android:name="de.duenndns.ssl.MemorizingActivity" />
<receiver android:name=".StartupReceiver" >
<intent-filter>

View File

@ -12,8 +12,7 @@ The only required tools are the Android SDK and Apache Ant.
```
git submodule update --init
android update project -p .
android update project -p extern/Universal-Image-Loader/library
./ant-prepare.sh # This runs 'android update' on the libs and the main project
ant clean release
```

6
ant-prepare.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash -ex
android update lib-project -p extern/Universal-Image-Loader/library
android update lib-project -p extern/AndroidPinning -t android-17
android update lib-project -p extern/MemorizingTrustManager
android update project -p . --name F-Droid

1
ant.properties Normal file
View File

@ -0,0 +1 @@
java.encoding=UTF-8

View File

@ -38,4 +38,8 @@ android {
proguardFile getDefaultProguardFile('proguard-android.txt')
}
}
tasks.withType(Compile) {
options.encoding = "UTF-8"
}
}

1
extern/AndroidPinning vendored Submodule

@ -0,0 +1 @@
Subproject commit 526654e1b9997b32e513d58d9094d4c1102a6cb3

1
extern/MemorizingTrustManager vendored Submodule

@ -0,0 +1 @@
Subproject commit 49452f67a760dfef77ddaa7e0b7d88c713c4a195

8
lint.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- Remove severity="ignore" to see the missing translations -->
<issue id="MissingTranslation" severity="ignore" >
<ignore path="res/values/no_trans.xml" />
<ignore path="res/values/default_repo.xml" />
</issue>
</lint>

View File

@ -3,3 +3,5 @@ proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.
target=android-19
android.library.reference.1=extern/Universal-Image-Loader/library
android.library.reference.2=extern/MemorizingTrustManager
android.library.reference.3=extern/AndroidPinning

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -2,7 +2,8 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical" >
android:orientation="vertical"
android:padding="6dp">
<TextView
android:layout_width="match_parent"
@ -15,7 +16,7 @@
android:layout_height="wrap_content"
android:inputType="textUri"
android:maxLines="2"
android:text="https://" />
android:text="@string/https" />
<TextView
android:layout_width="match_parent"

38
res/layout/repo_item.xml Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="25dp"
android:padding="8dp"
android:descendantFocusability="blocksDescendants">
<!--
descendantFocusability is here because if you have a child that responds
to touch events (in our case, the switch/toggle button) then the list item
itself will not respond to touch events.
http://syedasaraahmed.wordpress.com/2012/10/03/android-onitemclicklistener-not-responding-clickable-rowitem-of-custom-listview/
-->
<ImageView android:id="@+id/img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true" />
<TextView android:id="@+id/repo_name"
android:textSize="21sp"
android:textStyle="bold"
android:singleLine="true"
android:ellipsize="marquee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/img"
android:layout_alignParentLeft="true"/>
<TextView android:id="@+id/repo_unsigned"
android:textSize="14sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/repo_name"
android:text="@string/unsigned"
android:textColor="@color/unsigned"/>
</RelativeLayout>

131
res/layout/repodetails.xml Normal file
View File

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
android:paddingTop="@dimen/padding_top"
android:paddingLeft="@dimen/padding_side"
android:paddingRight="@dimen/padding_side">
<!-- Editable URL of this repo -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/label_repo_url"
android:text="@string/repo_url"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" />
<EditText
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/input_repo_url"
android:inputType="textUri"
android:layout_below="@id/label_repo_url" />
<!-- Name of this repo -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/label_repo_name"
android:text="@string/repo_name"
android:layout_below="@id/input_repo_url"
android:layout_alignParentLeft="true"
android:paddingTop="@dimen/form_label_top"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="false"
android:id="@+id/text_repo_name"
android:layout_below="@id/label_repo_name" android:textStyle="bold"/>
<!-- Description - as pulled from the index file during last update... -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/label_description"
android:text="@string/repo_description"
android:layout_below="@id/text_repo_name"
android:layout_alignParentLeft="true"
android:paddingTop="@dimen/form_label_top"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="false"
android:scrollHorizontally="false"
android:id="@+id/text_description"
android:layout_below="@id/label_description" android:textStyle="bold"/>
<!-- Number of apps belonging to this repo -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/label_num_apps"
android:text="@string/repo_num_apps"
android:layout_below="@id/text_description"
android:layout_alignParentLeft="true"
android:paddingTop="@dimen/form_label_top" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/text_num_apps"
android:layout_below="@id/label_num_apps" android:textStyle="bold"/>
<!-- The last time this repo was updated -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/label_last_update"
android:text="@string/repo_last_update"
android:layout_below="@id/text_num_apps"
android:layout_alignParentLeft="true"
android:paddingTop="@dimen/form_label_top" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/text_last_update"
android:layout_below="@id/label_last_update" android:textStyle="bold"/>
<!-- Signature (or "unsigned" if none) -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/label_signature"
android:text="@string/repo_signature"
android:layout_below="@id/text_last_update"
android:layout_alignParentLeft="true"
android:paddingTop="@dimen/form_label_top" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="false"
android:scrollHorizontally="false"
android:id="@+id/text_signature"
android:layout_below="@id/label_signature" android:textStyle="bold"/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="false"
android:scrollHorizontally="false"
android:id="@+id/text_signature_description"
android:layout_below="@id/text_signature"/>
<!-- The last time this repo was updated -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/text_not_yet_updated"
android:layout_below="@id/input_repo_url"
android:text="@string/repo_not_yet_updated"
android:textStyle="bold"
android:paddingTop="@dimen/form_label_top"/>
<Button
android:id="@+id/btn_update"
android:layout_centerHorizontal="true"
android:text="@string/repo_update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/text_not_yet_updated"/>
</RelativeLayout>

View File

@ -1,36 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/vw1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/vw1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView android:id="@+id/img"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView android:id="@+id/img"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView android:id="@+id/uri"
android:textSize="21sp"
android:textStyle="bold"
android:singleLine="true"
android:ellipsize="marquee"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
<TextView android:id="@+id/uri"
android:textSize="21sp"
android:textStyle="bold"
android:singleLine="true"
android:ellipsize="marquee"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
<TextView android:id="@+id/fingerprint"
android:textSize="14sp"
android:typeface="monospace"
android:singleLine="false"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
<TextView android:id="@+id/fingerprint"
android:textSize="14sp"
android:typeface="monospace"
android:singleLine="false"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!--

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/edit_repo"
android:title="@string/edit"
android:icon="@android:drawable/ic_menu_edit" />
<item
android:id="@+id/delete_repo"
android:title="@string/delete"
android:icon="@android:drawable/ic_menu_delete" />
</menu>

View File

@ -46,7 +46,7 @@
<string name="many_updates_available">%d налични актуализации.</string>
<string name="fdroid_updates_available">Актуализации на F-Droid са налични</string>
<string name="process_wait_title">Моля изчакай</string>
<string name="process_update_msg">Обновявани на списъка с приложения...</string>
<string name="process_update_msg">Обновявани на списъка с приложения</string>
<string name="download_server">Взимане на приложението от</string>
<string name="repo_add_url">Адрес на хранилището</string>
<string name="repo_alrt">Списъкът на хранилищата е променен.

View File

@ -47,7 +47,7 @@ L\'adreça d\'un dipòsit té un aspecte com ara: http://f-droid.org/repo</strin
<string name="many_updates_available">Hi ha %d actualitzacions disponibles.</string>
<string name="fdroid_updates_available">Hi ha actualitzacions de l\'F-Droid disponibles</string>
<string name="process_wait_title">Un moment si us plau</string>
<string name="process_update_msg">S\'està actualitzant la llista d\'aplicacions...</string>
<string name="process_update_msg">S\'està actualitzant la llista d\'aplicacions</string>
<string name="download_server">S\'està obtenint l\'aplicació des de</string>
<string name="repo_add_url">Adreça del dipòsit</string>
<string name="repo_alrt">La llista de dipòsits ha canviat.
@ -98,7 +98,7 @@ La voleu actualitzar?</string>
%1$s</string>
<string name="status_connecting_to_repo">S\'està connectant a
%1$s</string>
<string name="status_checking_compatibility">S\'està comprovant la compatibilitat de les aplicacions amb el vostre dispositiu...</string>
<string name="status_checking_compatibility">S\'està comprovant la compatibilitat de les aplicacions amb el vostre dispositiu</string>
<string name="no_permissions">No es fa servir cap permís.</string>
<string name="permissions_for_long">Permisos de la versió %s</string>
<string name="showPermissions">Mostra els permisos</string>

View File

@ -47,7 +47,7 @@
<string name="many_updates_available">%d διαθέσιμες ενημερώσεις.</string>
<string name="fdroid_updates_available">Διαθέσιμες ενημερώσεις για το F-Droid</string>
<string name="process_wait_title">Παρακαλώ περιμένετε</string>
<string name="process_update_msg">Ενημέρωση λίστα εφαρμογών...</string>
<string name="process_update_msg">Ενημέρωση λίστα εφαρμογών</string>
<string name="download_server">Λήψη εφαρμογών από</string>
<string name="repo_add_url">Διεύθυνση αποθετηρίου</string>
<string name="repo_alrt">Η λίστα με τα χρησιμοποιούμενα αποθετήρια έχει αλλάξει.
@ -98,7 +98,7 @@
%1$s</string>
<string name="status_connecting_to_repo">Σύνδεση με
%1$s</string>
<string name="status_checking_compatibility">Έλεγχος συμβατότητας εφαρμογών με τη συσκευή σας...</string>
<string name="status_checking_compatibility">Έλεγχος συμβατότητας εφαρμογών με τη συσκευή σας</string>
<string name="no_permissions">Δεν χρησιμοποιείται καμία άδεια.</string>
<string name="permissions_for_long">Άδειες για την έκδοση %s</string>
<string name="showPermissions">Εμφάνιση αδειών</string>

View File

@ -47,7 +47,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo
<string name="many_updates_available">%d actualizaciones disponibles.</string>
<string name="fdroid_updates_available">Actualizaciones de F-Droid disponibles</string>
<string name="process_wait_title">Por favor, espera</string>
<string name="process_update_msg">Actualizando la lista de aplicaciones...</string>
<string name="process_update_msg">Actualizando la lista de aplicaciones</string>
<string name="download_server">Obteniendo la aplicación de</string>
<string name="repo_add_url">Dirección del repositorio</string>
<string name="repo_alrt">La lista de repositorios usada ha cambiado.
@ -98,7 +98,7 @@ La dirección de un repositorio es algo similar a esto: https://f-droid.org/repo
%1$s</string>
<string name="status_connecting_to_repo">Conectando a
%1$s</string>
<string name="status_checking_compatibility">Comprobando la compatibilidad de las aplicaciones con tu dispositivo...</string>
<string name="status_checking_compatibility">Comprobando la compatibilidad de las aplicaciones con tu dispositivo</string>
<string name="no_permissions">No se usan permisos.</string>
<string name="permissions_for_long">Permisos para la versión %s</string>
<string name="showPermissions">Mostrar permisos</string>

View File

@ -35,7 +35,7 @@ GNU GPLv3 lizentziapean argitaratua.</string>
<string name="many_updates_available">%d eguneraketa eskuragarri.</string>
<string name="fdroid_updates_available">F-Droid eguneraketak eskuragarri</string>
<string name="process_wait_title">Mesedez itxaron</string>
<string name="process_update_msg">Aplikazio-zerrenda eguneratzen...</string>
<string name="process_update_msg">Aplikazio-zerrenda eguneratzen</string>
<string name="download_server">Aplikazioa eskuratzen hemendik</string>
<string name="repo_add_url">Biltegiaren helbidea</string>
<string name="repo_alrt">Erabilitako biltegien zerrenda aldatu egin da.
@ -71,7 +71,7 @@ Eguneratu nahi dituzu?</string>
<string name="category_recentlyupdated">Azkenaldian eguneratua</string>
<string name="status_connecting_to_repo">%1$s(e)ra
konektatzen</string>
<string name="status_checking_compatibility">Aplikazioak zure gailuarekin bateragarriak diren egiaztatzen...</string>
<string name="status_checking_compatibility">Aplikazioak zure gailuarekin bateragarriak diren egiaztatzen</string>
<string name="no_permissions">Ez da baimenik erabiltzen.</string>
<string name="permissions_for_long">%s bertsioarentzako baimenak</string>
<string name="showPermissions">Erakutsi baimenak</string>

View File

@ -47,7 +47,7 @@
<string name="many_updates_available">%d به‌روزرسانی موجود است.</string>
<string name="fdroid_updates_available">به‌روزرسانی‌های F-Droid موجود هستند</string>
<string name="process_wait_title">لطفاً صبر کنید</string>
<string name="process_update_msg">به‌روزرسانی فهرست برنامه‌ها...</string>
<string name="process_update_msg">به‌روزرسانی فهرست برنامه‌ها</string>
<string name="download_server">گرفتن برنامه از</string>
<string name="repo_add_url">نشانی مخزن</string>
<string name="repo_alrt">فهرست مخزن‌ها تغییر یافته‌است.
@ -98,7 +98,7 @@
%1$s</string>
<string name="status_connecting_to_repo">اتصال به
%1$s</string>
<string name="status_checking_compatibility">بررسی سازگاری برنامه‌ها با دستگاه شما...</string>
<string name="status_checking_compatibility">بررسی سازگاری برنامه‌ها با دستگاه شما</string>
<string name="no_permissions">دسترسی‌ای استفاده نشده‌است.</string>
<string name="permissions_for_long">دسترسی‌های نسخهٔ %s</string>
<string name="showPermissions">نمایش دسترسی‌ها</string>

View File

@ -33,7 +33,7 @@
<string name="many_updates_available">%d päivitystä saatavilla.</string>
<string name="fdroid_updates_available">F-Droid: Päivityksiä saatavilla</string>
<string name="process_wait_title">Odota hetki</string>
<string name="process_update_msg">Päivitetään sovelluslistaa...</string>
<string name="process_update_msg">Päivitetään sovelluslistaa</string>
<string name="download_server">Haetaan sovellusta lähteestä</string>
<string name="repo_add_url">Sovelluslähteen osoite</string>
<string name="repo_alrt">Lista käytetyistä sovelluslähteistä on muuttumut.
@ -69,6 +69,6 @@ Tahdotko päivittää ne?</string>
<string name="category_all">Kaikki</string>
<string name="category_whatsnew">Uutta</string>
<string name="category_recentlyupdated">Viimeaikoina päivitetty</string>
<string name="status_checking_compatibility">Tarkistetaan ohjelman yhteensopivuutta laitteesi kanssa...</string>
<string name="status_checking_compatibility">Tarkistetaan ohjelman yhteensopivuutta laitteesi kanssa</string>
<string name="theme">Teema</string>
</resources>

View File

@ -47,7 +47,7 @@ L\'URL d\'un dépôt ressemble à ceci : http://f-droid.org/repo</string>
<string name="many_updates_available">%d mises à jour sont disponibles.</string>
<string name="fdroid_updates_available">Des mises à jour F-Droid sont disponibles</string>
<string name="process_wait_title">Patientez</string>
<string name="process_update_msg">Mise à jour de la liste d\'applications...</string>
<string name="process_update_msg">Mise à jour de la liste d\'applications</string>
<string name="download_server">Réception d\'application de</string>
<string name="repo_add_url">Adresse du dépôt</string>
<string name="repo_alrt">La liste des dépôts utilisés a changé.

View File

@ -51,7 +51,7 @@ Un enderezo a un repositorio sería algo
<string name="many_updates_available">%d actualizacións dispoñíbeis</string>
<string name="fdroid_updates_available">Actualizacións de F-Droid dispoñíbeis</string>
<string name="process_wait_title">Agarde por favor</string>
<string name="process_update_msg">Actualizando a lista de aplicativos...</string>
<string name="process_update_msg">Actualizando a lista de aplicativos</string>
<string name="download_server">Obtención do aplicativo desde</string>
<string name="repo_add_url">Enderezo do repositorio</string>
<string name="repo_alrt">Cambiou a lista de repositorios usados.

View File

@ -47,7 +47,7 @@ Un indirizzo URL di esempio è: https://f-droid.org/repo</string>
<string name="many_updates_available">%d aggiornamenti disponibili.</string>
<string name="fdroid_updates_available">Aggiornamenti per F-Droid Disponibili</string>
<string name="process_wait_title">Attendere prego</string>
<string name="process_update_msg">Aggiornamento elenco applicazioni...</string>
<string name="process_update_msg">Aggiornamento elenco applicazioni</string>
<string name="download_server">Scaricamento applicazione da</string>
<string name="repo_add_url">Indirizzo repository</string>
<string name="repo_alrt">L\'elenco dei repository in uso è cambiato.
@ -98,7 +98,7 @@ Vuoi aggiornarlo?</string>
%1$s</string>
<string name="status_connecting_to_repo">Connessione a
%1$s</string>
<string name="status_checking_compatibility">Controllo compatibilità applicazioni con il tuo dispositivo...</string>
<string name="status_checking_compatibility">Controllo compatibilità applicazioni con il tuo dispositivo</string>
<string name="no_permissions">Non viene usata alcuna autorizzazione.</string>
<string name="permissions_for_long">Autorizzazioni per la versione %s</string>
<string name="showPermissions">Mostra autorizzazioni</string>

View File

@ -39,7 +39,7 @@
<string name="many_updates_available">%d개의 업데이트를 사용할 수 있습니다.</string>
<string name="fdroid_updates_available">F-Droid 업데이트를 사용할 수 있습니다.</string>
<string name="process_wait_title">잠시만 기다려주세요</string>
<string name="process_update_msg">응용 프로그램 목록 업데이트중...</string>
<string name="process_update_msg">응용 프로그램 목록 업데이트중</string>
<string name="download_server">에서 응용프로그램 가져오기</string>
<string name="repo_add_url">저장소 주소</string>
<string name="repo_alrt">사용된 저장소의 목록이 변경되었습니다.

View File

@ -43,7 +43,7 @@ Lisensiert GNU GPLv3.</string>
<string name="many_updates_available">%d oppdateringer tilgjengelig.</string>
<string name="fdroid_updates_available">F-Droid: Oppdateringer tilgjengelig</string>
<string name="process_wait_title">Vennligst vent</string>
<string name="process_update_msg">Oppdaterer applikasjonsliste...</string>
<string name="process_update_msg">Oppdaterer applikasjonsliste</string>
<string name="download_server">Henter program fra</string>
<string name="repo_add_url">Registeradresse</string>
<string name="repo_alrt">Listen over brukte register har endret seg. Vil du oppdatere dem?</string>
@ -93,7 +93,7 @@ Lisensiert GNU GPLv3.</string>
%1$s</string>
<string name="status_connecting_to_repo">Kobler til
%1$s</string>
<string name="status_checking_compatibility">Sjekker programstøtte for ditt utstyr...</string>
<string name="status_checking_compatibility">Sjekker programstøtte for ditt utstyr</string>
<string name="no_permissions">Krever ingen tillatelser.</string>
<string name="permissions_for_long">Tillatelser for versjon %s</string>
<string name="showPermissions">Vis tillatelser</string>

View File

@ -50,7 +50,7 @@ Een bron-adres ziet er ongeveer
<string name="many_updates_available">%d vernieuwingen zijn beschikbaar</string>
<string name="fdroid_updates_available">F-Droid Vernieuwingen Beschikbaar</string>
<string name="process_wait_title">Even geduld aub</string>
<string name="process_update_msg">Applicatie-lijst vernieuwen...</string>
<string name="process_update_msg">Applicatie-lijst vernieuwen</string>
<string name="download_server">downloaden applicatie van</string>
<string name="repo_add_url">Bron-adres</string>
<string name="repo_alrt">De lijst van gebruikte bronnen is veranderd.
@ -101,7 +101,7 @@ Wilt u ze vernieuwen?</string>
%1$s</string>
<string name="status_connecting_to_repo">Connecteren naar
%1$s</string>
<string name="status_checking_compatibility">Controleer app compatibiliteit met uw apparaat...</string>
<string name="status_checking_compatibility">Controleer app compatibiliteit met uw apparaat</string>
<string name="no_permissions">Geen permissies worden gebruikt</string>
<string name="permissions_for_long">Permissies voor versie %s</string>
<string name="showPermissions">Laat permissies zien</string>

View File

@ -34,7 +34,7 @@
<string name="tab_noninstalled">Dostępne</string>
<string name="tab_updates">Aktualizacje</string>
<string name="process_wait_title">Proszę czekać</string>
<string name="process_update_msg">Aktualizowanie listy aplikacji...</string>
<string name="process_update_msg">Aktualizowanie listy aplikacji</string>
<string name="download_server">Pobieranie aplikacji z</string>
<string name="repo_add_url">Adres repozytorium</string>
<string name="repo_alrt">Lista wykorzystywanych repozytoriów uległa zmianie.

View File

@ -47,7 +47,7 @@ Um endereço do repositório é algo similar a isto: http://f-droid.org/repo</st
<string name="many_updates_available">%d atualizações disponíveis.</string>
<string name="fdroid_updates_available">Atualizações do F-Droid Disponíveis</string>
<string name="process_wait_title">Aguarde</string>
<string name="process_update_msg">Atualizando a lista de aplicativos...</string>
<string name="process_update_msg">Atualizando a lista de aplicativos</string>
<string name="download_server">Baixando aplicativo de</string>
<string name="repo_add_url">Endereço do repositório</string>
<string name="repo_alrt">A lista de repositórios usados mudou.
@ -98,7 +98,7 @@ Você deseja atualizá-los?</string>
%1$s</string>
<string name="status_connecting_to_repo">Conectando-se a
%1$s</string>
<string name="status_checking_compatibility">Verificando compatibilidade de aplicativos com o seu dispositivo...</string>
<string name="status_checking_compatibility">Verificando compatibilidade de aplicativos com o seu dispositivo</string>
<string name="no_permissions">Nenhuma permissão utilizada.</string>
<string name="permissions_for_long">Permissões para a versão %s</string>
<string name="showPermissions">Mostrar permissões</string>

View File

@ -25,6 +25,6 @@ Distribuit sub licenta GNU GPLv3.</string>
<string name="repo_update_title">Actualizare depozit aplicatii</string>
<string name="tab_noninstalled">Disponibil</string>
<string name="tab_updates">Actualizare</string>
<string name="process_wait_title">Asteptati ...</string>
<string name="process_update_msg">Se actualizeaza lista ...</string>
<string name="process_wait_title">Asteptati </string>
<string name="process_update_msg">Se actualizeaza lista </string>
</resources>

View File

@ -43,7 +43,7 @@
<string name="one_update_available">Доступно 1 обновление.</string>
<string name="many_updates_available">Обновлений доступно - %d.</string>
<string name="process_wait_title">Подождите</string>
<string name="process_update_msg">Список приложений обновляется...</string>
<string name="process_update_msg">Список приложений обновляется</string>
<string name="download_server">Взять приложение из</string>
<string name="repo_add_url">Адрес репозитория</string>
<string name="repo_alrt">Список репозиториев изменился.
@ -82,7 +82,7 @@
%1$s</string>
<string name="status_connecting_to_repo">Соединение с
%1$s</string>
<string name="status_checking_compatibility">Проверка совместимости приложений с устройством...</string>
<string name="status_checking_compatibility">Проверка совместимости приложений с устройством</string>
<string name="no_permissions">Разрешений не требуется.</string>
<string name="permissions_for_long">Разрешения для версии %s</string>
<string name="showPermissions">Показывать разрешения</string>

View File

@ -28,7 +28,7 @@ Izdan z licenco GNU GPLv3.</string>
<string name="tab_noninstalled">Na razpolago</string>
<string name="tab_updates">Posodobitve</string>
<string name="process_wait_title">Počakajte prosim</string>
<string name="process_update_msg">Poteka posodobitev spiska aplikacij ...</string>
<string name="process_update_msg">Poteka posodobitev spiska aplikacij </string>
<string name="download_server">Prejem aplikacije iz</string>
<string name="repo_add_url">Naslov skladišča</string>
<string name="repo_alrt">Spisek uporabljenih skladišč se je spremenil.

View File

@ -47,7 +47,7 @@
<string name="many_updates_available">%d нове/нових верзија на располагању</string>
<string name="fdroid_updates_available">Ажурирање Ф-Дроида на располагању.</string>
<string name="process_wait_title">Сачекајте</string>
<string name="process_update_msg">Ажурира се листа апликација...</string>
<string name="process_update_msg">Ажурира се листа апликација</string>
<string name="download_server">Скида се апликација са</string>
<string name="repo_add_url">Адреса ризнице</string>
<string name="repo_alrt">Промењена је листа ризница у употреби.
@ -96,7 +96,7 @@
%1$s</string>
<string name="status_connecting_to_repo">Повезивање са
%1$s</string>
<string name="status_checking_compatibility">Проверава се да ли је апликација компатибилна са вашим уређајем...</string>
<string name="status_checking_compatibility">Проверава се да ли је апликација компатибилна са вашим уређајем</string>
<string name="no_permissions">Не захтевају се никакве дозволе.</string>
<string name="permissions_for_long">Дозволе за верзију %s</string>
<string name="showPermissions">Прикажи дозволе</string>

View File

@ -47,7 +47,7 @@ En förrådsadress ser ut så här: https://f-droid.org/repo</string>
<string name="many_updates_available">%d uppdateringar finns tillgängliga.</string>
<string name="fdroid_updates_available">Uppdateringar för F-Droid tillgängliga</string>
<string name="process_wait_title">Var vänlig vänta</string>
<string name="process_update_msg">Uppdaterar programlistan...</string>
<string name="process_update_msg">Uppdaterar programlistan</string>
<string name="download_server">Hämtar program från</string>
<string name="repo_add_url">Förrådsadress</string>
<string name="repo_alrt">Listan över förråd har ändrats.

View File

@ -47,7 +47,7 @@ Bir depo adresi şuna benzer: https://f-droid.org/repo</string>
<string name="many_updates_available">%d güncelleme bulunmaktadır.</string>
<string name="fdroid_updates_available">F-Droid güncellemeleri bulunmaktadır</string>
<string name="process_wait_title">Bekleyiniz</string>
<string name="process_update_msg">Uygulama listesi güncelleniyor...</string>
<string name="process_update_msg">Uygulama listesi güncelleniyor</string>
<string name="download_server">Uygulama buradan alınıyor:</string>
<string name="repo_add_url">Depo adresi</string>
<string name="repo_alrt">Kullanılan depoların listesi değişti.

View File

@ -28,7 +28,7 @@
<string name="tab_noninstalled">Наявне</string>
<string name="tab_updates">Оновлення</string>
<string name="process_wait_title">Зачекайте</string>
<string name="process_update_msg">Оновлюю список програм...</string>
<string name="process_update_msg">Оновлюю список програм</string>
<string name="download_server">Звантажую програму</string>
<string name="repo_add_url">Адреса репозиторію</string>
<string name="repo_alrt">Список репозиторіїв змінено.

5
res/values/colors.xml Normal file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="signed">#ffcccccc</color>
<color name="unsigned">#ffCC0000</color>
</resources>

6
res/values/dimens.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="padding_side">8dp</dimen>
<dimen name="padding_top">5dp</dimen>
<dimen name="form_label_top">5dp</dimen>
</resources>

View File

@ -12,6 +12,8 @@
<string name="menu_dogecoin">Dogecoin</string>
<string name="menu_flattr">Flattr</string>
<string name="https">https://</string>
<string-array name="updateIntervalValues">
<item>0</item>
<item>1</item>

View File

@ -7,10 +7,11 @@
<string name="installIncompatible">It seems like this package is not compatible with your device. Do you want to try and install it anyway?</string>
<string name="installDowngrade">You are trying to downgrade this application. Doing so might get it to malfunction and even lose your data. Do you want to try and downgrade it anyway?</string>
<string name="version">Version</string>
<string name="edit">Edit</string>
<string name="delete">Delete</string>
<string name="cache_downloaded">App cache</string>
<string name="cache_downloaded_on">Keep downloaded apk files on SD card</string>
<string name="cache_downloaded_off">Do not keep any apk files</string>
<string name="updates">Updates</string>
<string name="other">Other</string>
<string name="last_update_check">Last repo scan: %s</string>
@ -70,11 +71,11 @@
<string name="many_updates_available">%d updates are available.</string>
<string name="fdroid_updates_available">F-Droid Updates Available</string>
<string name="process_wait_title">Please Wait</string>
<string name="process_update_msg">Updating application list...</string>
<string name="process_update_msg">Updating application list</string>
<string name="download_server">Getting application from</string>
<string name="repo_add_url">Repository address</string>
<string name="repo_add_fingerprint">fingerprint (optional)</string>
<string name="repo_add_fingerprint">Fingerprint (optional)</string>
<string name="repo_exists">This repo already exists!</string>
<string name="repo_exists_add_fingerprint">This repo is already setup, this will add new key information.</string>
<string name="repo_exists_enable">This repo is already setup, confirm that you want to re-enable it.</string>
@ -86,7 +87,7 @@
want to update them?</string>
<string name="menu_update_repo">Update Repos</string>
<string name="menu_manage">Manage Repos</string>
<string name="menu_manage">Repositories</string>
<string name="menu_preferences">Preferences</string>
<string name="menu_about">About</string>
<string name="menu_search">Search</string>
@ -163,6 +164,36 @@
<string name="compactlayout_on">Show icons at a smaller size</string>
<string name="compactlayout_off">Show icons at regular size</string>
<string name="theme">Theme</string>
<string name="unsigned">Unsigned</string>
<string name="repo_url">URL</string>
<string name="repo_num_apps">Number of apps</string>
<string name="repo_signature">Signature</string>
<string name="repo_description">Description</string>
<string name="repo_last_update">Last update</string>
<string name="repo_update">Update</string>
<string name="repo_name">Name</string>
<string name="unsigned_description">This means that the list of
applications could not be verified. You should be careful
with applications downloaded from unsigned indexes.</string>
<string name="repo_not_yet_updated">This repository has not been used yet.
In order to view the apps it provides, you will need to update
it.\n\nOnce updated, the description and other details will
become available here.
</string>
<string name="repo_delete_details">Do you want to delete the \"{0}\"
repository, which has {1} apps in it? Any installed apps will NOT be
removed, but you will not be able to update them through F-Droid any
more.
</string>
<string name="unknown">Unknown</string>
<string name="repo_confirm_delete_title">Delete Repository?</string>
<string name="repo_confirm_delete_body">Deleting a repository means
apps from it will no longer be available from F-Droid.\n\nNote: All
previously installed apps will remain on your device.
</string>
<string name="repo_disabled_notification">Disabled "%1$s".\n\nYou will
need to re-enable this repository to install apps from it.
</string>
<string name="minsdk_or_later">Android %s or later</string>
</resources>

View File

@ -85,9 +85,20 @@ public class AppDetails extends ListActivity {
private static final int REQUEST_INSTALL = 0;
private static final int REQUEST_UNINSTALL = 1;
private static class ViewHolder {
TextView version;
TextView status;
TextView size;
TextView api;
TextView buildtype;
TextView added;
TextView nativecode;
}
private class ApkListAdapter extends BaseAdapter {
private List<DB.Apk> items;
private LayoutInflater mInflater;
public ApkListAdapter(Context context, List<DB.Apk> items) {
this.items = new ArrayList<DB.Apk>();
@ -96,6 +107,8 @@ public class AppDetails extends ListActivity {
this.addItem(apk);
}
}
mInflater = (LayoutInflater) mctx.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}
public void addItem(DB.Apk apk) {
@ -128,72 +141,85 @@ public class AppDetails extends ListActivity {
java.text.DateFormat df = DateFormat.getDateFormat(mctx);
DB.Apk apk = items.get(position);
ViewHolder holder;
View v = convertView;
if (v == null) {
LayoutInflater vi = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate(R.layout.apklistitem, null);
if (convertView == null) {
convertView = mInflater.inflate(R.layout.apklistitem, null);
holder = new ViewHolder();
holder.version = (TextView) convertView.findViewById(R.id.version);
holder.status = (TextView) convertView.findViewById(R.id.status);
holder.size = (TextView) convertView.findViewById(R.id.size);
holder.api = (TextView) convertView.findViewById(R.id.api);
holder.buildtype = (TextView) convertView.findViewById(R.id.buildtype);
holder.added = (TextView) convertView.findViewById(R.id.added);
holder.nativecode = (TextView) convertView.findViewById(R.id.nativecode);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
v.setEnabled(apk.compatible);
TextView tv = (TextView) v.findViewById(R.id.version);
tv.setText(getString(R.string.version) + " " + apk.version
holder.version.setText(getString(R.string.version)
+ " " + apk.version
+ (apk == app.curApk ? "" : ""));
tv.setEnabled(apk.compatible);
tv = (TextView) v.findViewById(R.id.status);
if (apk.vercode == app.installedVerCode
&& apk.sig.equals(mInstalledSigID)) {
tv.setText(getString(R.string.inst));
holder.status.setText(getString(R.string.inst));
} else {
tv.setText(getString(R.string.not_inst));
}
tv.setEnabled(apk.compatible);
tv = (TextView) v.findViewById(R.id.size);
if (apk.detail_size == 0) {
tv.setText("");
} else {
tv.setText(Utils.getFriendlySize(apk.detail_size));
tv.setEnabled(apk.compatible);
holder.status.setText(getString(R.string.not_inst));
}
tv = (TextView) v.findViewById(R.id.api);
if (apk.minSdkVersion == 0) {
tv.setText("");
if (apk.detail_size > 0) {
holder.size.setText(Utils.getFriendlySize(apk.detail_size));
} else {
tv.setText(getString(R.string.minsdk_or_later,
holder.size.setText("");
}
if (apk.minSdkVersion > 0) {
holder.api.setText(getString(R.string.minsdk_or_later,
Utils.getAndroidVersionName(apk.minSdkVersion)));
tv.setEnabled(apk.compatible);
} else {
holder.api.setText("");
}
tv = (TextView) v.findViewById(R.id.buildtype);
if (apk.srcname != null) {
tv.setText("source");
holder.buildtype.setText("source");
} else {
tv.setText("bin");
holder.buildtype.setText("bin");
}
tv.setEnabled(apk.compatible);
tv = (TextView) v.findViewById(R.id.added);
if (apk.added != null) {
tv.setVisibility(View.VISIBLE);
tv.setText(getString(R.string.added_on, df.format(apk.added)));
tv.setEnabled(apk.compatible);
holder.added.setText(getString(R.string.added_on,
df.format(apk.added)));
} else {
tv.setVisibility(View.GONE);
holder.added.setText("");
}
tv = (TextView) v.findViewById(R.id.nativecode);
if (pref_expert && apk.nativecode != null) {
tv.setVisibility(View.VISIBLE);
tv.setText(apk.nativecode.toString().replaceAll(","," "));
tv.setEnabled(apk.compatible);
holder.nativecode.setText(apk.nativecode.toString().replaceAll(","," "));
} else {
tv.setVisibility(View.GONE);
holder.nativecode.setText("");
}
return v;
// Disable it all if it isn't compatible...
View[] views = {
convertView,
holder.version,
holder.status,
holder.size,
holder.api,
holder.buildtype,
holder.added,
holder.nativecode
};
for (View view : views) {
view.setEnabled(apk.compatible);
}
return convertView;
}
}

View File

@ -19,9 +19,11 @@
package org.fdroid.fdroid;
import android.annotation.SuppressLint;
import java.io.File;
import java.security.MessageDigest;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
@ -31,6 +33,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Semaphore;
@ -43,7 +46,6 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.preference.PreferenceManager;
import android.text.TextUtils.SimpleStringSplitter;
import android.util.DisplayMetrics;
@ -51,6 +53,8 @@ import android.util.Log;
import org.fdroid.fdroid.compat.Compatibility;
import org.fdroid.fdroid.compat.ContextCompat;
import org.fdroid.fdroid.compat.SupportedArchitectures;
import org.fdroid.fdroid.data.DBHelper;
public class DB {
@ -65,7 +69,7 @@ public class DB {
// Get access to the database. Must be called before any database activity,
// and releaseDB must be called subsequently. Returns null in the event of
// failure.
static DB getDB() {
public static DB getDB() {
try {
dbSync.acquire();
return dbInstance;
@ -75,12 +79,10 @@ public class DB {
}
// Release database access lock acquired via getDB().
static void releaseDB() {
public static void releaseDB() {
dbSync.release();
}
private static final String DATABASE_NAME = "fdroid";
// Possible values of the SQLite flag "synchronous"
public static final int SYNC_OFF = 0;
public static final int SYNC_NORMAL = 1;
@ -88,24 +90,7 @@ public class 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, "
+ "curVersion text," + "curVercode integer,"
+ "antiFeatures string," + "donateURL string,"
+ "bitcoinAddr string," + "litecoinAddr string,"
+ "dogecoinAddr string,"
+ "flattrID string," + "requirements string,"
+ "categories string," + "added string,"
+ "lastUpdated string," + "compatible int not null,"
+ "ignoreAllUpdates int not null,"
+ "ignoreThisUpdate int not null,"
+ "primary key(id));";
public static final String TABLE_APP = "fdroid_app";
public static class App implements Comparable<App> {
@ -286,16 +271,7 @@ public class DB {
// 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, "
+ "repo integer not null, " + "hash text not null, "
+ "vercode int not null," + "apkName text not null, "
+ "size int not null," + "sig string," + "srcname string,"
+ "minSdkVersion integer," + "permissions string,"
+ "features string," + "nativecode string,"
+ "hashType string," + "added string,"
+ "compatible int not null," + "primary key(id,vercode));";
public static final String TABLE_APK = "fdroid_apk";
public static class Apk {
@ -347,10 +323,10 @@ public class DB {
private static class CompatibilityChecker extends Compatibility {
private HashSet<String> features;
private List<String> cpuAbis;
private HashSet<String> cpuAbis;
private String cpuAbisDesc;
private boolean ignoreTouchscreen;
//@SuppressLint("NewApi")
public CompatibilityChecker(Context ctx) {
SharedPreferences prefs = PreferenceManager
@ -370,11 +346,17 @@ public class DB {
}
}
cpuAbis = new ArrayList<String>(2);
cpuAbis.add(android.os.Build.CPU_ABI);
if (hasApi(8)) {
cpuAbis.add(android.os.Build.CPU_ABI2);
cpuAbis = SupportedArchitectures.getAbis();
StringBuilder builder = new StringBuilder();
boolean first = true;
for (String abi : cpuAbis) {
if (first) first = false;
else builder.append(", ");
builder.append(abi);
}
cpuAbisDesc = builder.toString();
builder = null;
Log.d("FDroid", logMsg.toString());
}
@ -408,7 +390,7 @@ public class DB {
if (!compatibleApi(apk.nativecode)) {
Log.d("FDroid", apk.id + " vercode " + apk.vercode
+ " only supports " + CommaSeparatedList.str(apk.nativecode)
+ " while your architecture is " + cpuAbis.get(0));
+ " while your architectures are " + cpuAbisDesc);
return false;
}
return true;
@ -417,14 +399,7 @@ public class DB {
}
// 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 + " (id integer primary key, address text not null, "
+ "name text, description text, inuse integer not null, "
+ "priority integer not null, pubkey text, fingerprint text, "
+ "maxage integer not null default 0, "
+ "version integer not null default 0, "
+ "lastetag text);";
public static final String TABLE_REPO = "fdroid_repo";
public static class Repo {
public int id;
@ -438,16 +413,104 @@ public class DB {
public String fingerprint; // always null for an unsigned repo
public int maxage; // maximum age of index that will be accepted - 0 for any
public String lastetag; // last etag we updated from, null forces update
public Date lastUpdated;
/**
* If we haven't run an update for this repo yet, then the name
* will be unknown, in which case we will just take a guess at an
* appropriate name based on the url (e.g. "fdroid.org/archive")
*/
public String getName() {
if (name == null) {
String tempName = null;
try {
URL url = new URL(address);
tempName = url.getHost() + url.getPath();
} catch (MalformedURLException e) {
tempName = address;
}
return tempName;
} else {
return name;
}
}
public String toString() {
return address;
}
public int getNumberOfApps() {
DB db = DB.getDB();
int count = db.countAppsForRepo(id);
DB.releaseDB();
return count;
}
/**
* @param application In order invalidate the list of apps, we require
* a reference to the top level application.
*/
public void enable(FDroidApp application) {
try {
DB db = DB.getDB();
List<DB.Repo> toEnable = new ArrayList<DB.Repo>(1);
toEnable.add(this);
db.enableRepos(toEnable);
} finally {
DB.releaseDB();
}
application.invalidateAllApps();
}
/**
* @param application See DB.Repo.enable(application)
*/
public void disable(FDroidApp application) {
disableRemove(application, false);
}
/**
* @param application See DB.Repo.enable(application)
*/
public void remove(FDroidApp application) {
disableRemove(application, true);
}
/**
* @param application See DB.Repo.enable(application)
*/
private void disableRemove(FDroidApp application, boolean removeAfterDisabling) {
try {
DB db = DB.getDB();
List<DB.Repo> toDisable = new ArrayList<DB.Repo>(1);
toDisable.add(this);
db.doDisableRepos(toDisable, removeAfterDisabling);
} finally {
DB.releaseDB();
}
application.invalidateAllApps();
}
public boolean isSigned() {
return this.pubkey != null && this.pubkey.length() > 0;
}
public boolean hasBeenUpdated() {
return this.lastetag != null;
}
}
private final int DBVersion = 34;
private static void createAppApk(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_APP);
db.execSQL("create index app_id on " + TABLE_APP + " (id);");
db.execSQL(CREATE_TABLE_APK);
db.execSQL("create index apk_vercode on " + TABLE_APK + " (vercode);");
db.execSQL("create index apk_id on " + TABLE_APK + " (id);");
private int countAppsForRepo(int id) {
String[] selection = { "COUNT(distinct id)" };
String[] selectionArgs = { Integer.toString(id) };
Cursor result = db.query(
TABLE_APK, selection, "repo = ?", selectionArgs, "repo", null, null);
if (result.getCount() > 0) {
result.moveToFirst();
return result.getInt(0);
} else {
return 0;
}
}
public static String calcFingerprint(String pubkey) {
@ -472,156 +535,12 @@ public class DB {
return ret;
}
public void resetTransient(SQLiteDatabase db) {
mContext.getSharedPreferences("FDroid", Context.MODE_PRIVATE).edit()
.putBoolean("triedEmptyUpdate", false).commit();
db.execSQL("drop table " + TABLE_APP);
db.execSQL("drop table " + TABLE_APK);
db.execSQL("update " + TABLE_REPO + " set lastetag = NULL");
createAppApk(db);
}
private static boolean columnExists(SQLiteDatabase db,
String table, String column) {
return (db.rawQuery( "select * from " + table + " limit 0,1", null )
.getColumnIndex(column) != -1);
}
private class DBHelper extends SQLiteOpenHelper {
public DBHelper(Context context) {
super(context, DATABASE_NAME, null, DBVersion);
}
@Override
public void onCreate(SQLiteDatabase db) {
createAppApk(db);
db.execSQL(CREATE_TABLE_REPO);
ContentValues values = new ContentValues();
values.put("address",
mContext.getString(R.string.default_repo_address));
values.put("name",
mContext.getString(R.string.default_repo_name));
values.put("description",
mContext.getString(R.string.default_repo_description));
values.put("version", 0);
String pubkey = mContext.getString(R.string.default_repo_pubkey);
String fingerprint = DB.calcFingerprint(pubkey);
values.put("pubkey", pubkey);
values.put("fingerprint", fingerprint);
values.put("maxage", 0);
values.put("inuse", 1);
values.put("priority", 10);
values.put("lastetag", (String) null);
db.insert(TABLE_REPO, null, values);
values = new ContentValues();
values.put("address",
mContext.getString(R.string.default_repo_address2));
values.put("name",
mContext.getString(R.string.default_repo_name2));
values.put("description",
mContext.getString(R.string.default_repo_description2));
values.put("version", 0);
// default #2 is /archive which has the same key as /repo
values.put("pubkey", pubkey);
values.put("fingerprint", fingerprint);
values.put("maxage", 0);
values.put("inuse", 0);
values.put("priority", 20);
values.put("lastetag", (String) null);
db.insert(TABLE_REPO, null, values);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Migrate repo list to new structure. (No way to change primary
// key in sqlite - table must be recreated)
if (oldVersion < 20) {
List<Repo> oldrepos = new ArrayList<Repo>();
Cursor c = db.query(TABLE_REPO,
new String[] { "address", "inuse", "pubkey" },
null, null, null, null, null);
c.moveToFirst();
while (!c.isAfterLast()) {
Repo repo = new Repo();
repo.address = c.getString(0);
repo.inuse = (c.getInt(1) == 1);
repo.pubkey = c.getString(2);
oldrepos.add(repo);
c.moveToNext();
}
c.close();
db.execSQL("drop table " + TABLE_REPO);
db.execSQL(CREATE_TABLE_REPO);
for (Repo repo : oldrepos) {
ContentValues values = new ContentValues();
values.put("address", repo.address);
values.put("inuse", repo.inuse);
values.put("priority", 10);
values.put("pubkey", repo.pubkey);
values.put("lastetag", (String) null);
db.insert(TABLE_REPO, null, values);
}
}
// The other tables are transient and can just be reset. Do this after
// the repo table changes though, because it also clears the lastetag
// fields which didn't always exist.
resetTransient(db);
if (oldVersion < 21) {
if (!columnExists(db, TABLE_REPO, "name"))
db.execSQL("alter table " + TABLE_REPO + " add column name text");
if (!columnExists(db, TABLE_REPO, "description"))
db.execSQL("alter table " + TABLE_REPO + " add column description text");
ContentValues values = new ContentValues();
values.put("name", mContext.getString(R.string.default_repo_name));
values.put("description", mContext.getString(R.string.default_repo_description));
db.update(TABLE_REPO, values, "address = ?", new String[] {
mContext.getString(R.string.default_repo_address) });
values.clear();
values.put("name", mContext.getString(R.string.default_repo_name2));
values.put("description", mContext.getString(R.string.default_repo_description2));
db.update(TABLE_REPO, values, "address = ?", new String[] {
mContext.getString(R.string.default_repo_address2) });
}
if (oldVersion < 29) {
if (!columnExists(db, TABLE_REPO, "fingerprint"))
db.execSQL("alter table " + TABLE_REPO + " add column fingerprint text");
List<Repo> oldrepos = new ArrayList<Repo>();
Cursor c = db.query(TABLE_REPO,
new String[] { "address", "pubkey" },
null, null, null, null, null);
c.moveToFirst();
while (!c.isAfterLast()) {
Repo repo = new Repo();
repo.address = c.getString(0);
repo.pubkey = c.getString(1);
oldrepos.add(repo);
c.moveToNext();
}
c.close();
for (Repo repo : oldrepos) {
ContentValues values = new ContentValues();
values.put("fingerprint", DB.calcFingerprint(repo.pubkey));
db.update(TABLE_REPO, values, "address = ?", new String[] { repo.address });
}
}
if (oldVersion < 30) {
db.execSQL("alter table " + TABLE_REPO + " add column maxage integer not null default 0");
}
if (oldVersion < 34) {
db.execSQL("alter table " + TABLE_REPO + " add column version integer not null default 0");
}
}
/**
* Get the local storage (cache) path. This will also create it if
* it doesn't exist. It can return null if it's currently unavailable.
*/
public static File getDataPath(Context ctx) {
return ContextCompat.create(ctx).getExternalCacheDir();
}
private Context mContext;
@ -629,7 +548,8 @@ public class DB {
// The date format used for storing dates (e.g. lastupdated, added) in the
// database.
private SimpleDateFormat mDateFormat = new SimpleDateFormat("yyyy-MM-dd");
public static SimpleDateFormat dateFormat = new SimpleDateFormat(
"yyyy-MM-dd", Locale.ENGLISH);
private DB(Context ctx) {
@ -656,16 +576,11 @@ public class DB {
db = null;
}
// Reset the transient data in the database.
public void reset() {
resetTransient(db);
}
// Delete the database, which should cause it to be re-created next time
// it's used.
public static void delete(Context ctx) {
try {
ctx.deleteDatabase(DATABASE_NAME);
ctx.deleteDatabase(DBHelper.DATABASE_NAME);
// Also try and delete the old one, from versions 0.13 and earlier.
ctx.deleteDatabase("fdroid_db");
} catch (Exception ex) {
@ -783,17 +698,6 @@ public class DB {
}
}
// Repopulate the details for the given app.
// If 'apkrepo' is non-zero, only apks from that repo are
// populated.
public void repopulateDetails(App app, int apkRepo) {
populateAppDetails(app);
for (Apk apk : app.apks) {
populateApkDetails(apk, apkRepo);
}
}
// Return a list of apps matching the given criteria. Filtering is
// also done based on compatibility and anti-features according to
// the user's current preferences.
@ -837,10 +741,10 @@ public class DB {
app.curVercode = c.getInt(9);
String sAdded = c.getString(10);
app.added = (sAdded == null || sAdded.length() == 0) ? null
: mDateFormat.parse(sAdded);
: dateFormat.parse(sAdded);
String sLastUpdated = c.getString(11);
app.lastUpdated = (sLastUpdated == null || sLastUpdated
.length() == 0) ? null : mDateFormat
.length() == 0) ? null : dateFormat
.parse(sLastUpdated);
app.compatible = c.getInt(12) == 1;
app.ignoreAllUpdates = c.getInt(13) == 1;
@ -921,7 +825,7 @@ public class DB {
apk.minSdkVersion = c.getInt(6);
String sApkAdded = c.getString(7);
apk.added = (sApkAdded == null || sApkAdded.length() == 0) ? null
: mDateFormat.parse(sApkAdded);
: dateFormat.parse(sApkAdded);
apk.features = CommaSeparatedList.make(c.getString(8));
apk.nativecode = CommaSeparatedList.make(c.getString(9));
apk.compatible = compatible;
@ -1245,10 +1149,10 @@ public class DB {
values.put("dogecoinAddr", upapp.detail_dogecoinAddr);
values.put("flattrID", upapp.detail_flattrID);
values.put("added",
upapp.added == null ? "" : mDateFormat.format(upapp.added));
upapp.added == null ? "" : dateFormat.format(upapp.added));
values.put(
"lastUpdated",
upapp.added == null ? "" : mDateFormat
upapp.added == null ? "" : dateFormat
.format(upapp.lastUpdated));
values.put("curVersion", upapp.curVersion);
values.put("curVercode", upapp.curVercode);
@ -1293,7 +1197,7 @@ public class DB {
values.put("apkName", upapk.apkName);
values.put("minSdkVersion", upapk.minSdkVersion);
values.put("added",
upapk.added == null ? "" : mDateFormat.format(upapk.added));
upapk.added == null ? "" : dateFormat.format(upapk.added));
values.put("permissions",
CommaSeparatedList.str(upapk.detail_permissions));
values.put("features", CommaSeparatedList.str(upapk.features));
@ -1315,7 +1219,7 @@ public class DB {
try {
c = db.query(TABLE_REPO, new String[] { "address", "name",
"description", "version", "inuse", "priority", "pubkey",
"fingerprint", "maxage", "lastetag" },
"fingerprint", "maxage", "lastetag", "lastUpdated" },
"id = ?", new String[] { Integer.toString(id) }, null, null, null);
if (!c.moveToFirst())
return null;
@ -1331,6 +1235,13 @@ public class DB {
repo.fingerprint = c.getString(7);
repo.maxage = c.getInt(8);
repo.lastetag = c.getString(9);
try {
repo.lastUpdated = c.getString(10) != null ?
dateFormat.parse( c.getString(10)) :
null;
} catch (ParseException e) {
Log.e("FDroid", "Error parsing date " + c.getString(10));
}
return repo;
} finally {
if (c != null)
@ -1373,6 +1284,27 @@ public class DB {
return repos;
}
public void enableRepos(List<DB.Repo> repos) {
if (repos.isEmpty()) return;
ContentValues values = new ContentValues(1);
values.put("inuse", 1);
String[] whereArgs = new String[repos.size()];
StringBuilder where = new StringBuilder("address IN (");
for (int i = 0; i < repos.size(); i ++) {
Repo repo = repos.get(i);
repo.inuse = true;
whereArgs[i] = repo.address;
where.append('?');
if ( i < repos.size() - 1 ) {
where.append(',');
}
}
where.append(")");
db.update(TABLE_REPO, values, where.toString(), whereArgs);
}
public void changeServerStatus(String address) {
db.execSQL("update " + TABLE_REPO
+ " set inuse=1-inuse, lastetag=null where address = ?",
@ -1387,8 +1319,17 @@ public class DB {
}
public void updateRepoByAddress(Repo repo) {
updateRepo(repo, "address", repo.address);
}
public void updateRepo(Repo repo) {
updateRepo(repo, "id", repo.id + "");
}
private void updateRepo(Repo repo, String field, String value) {
ContentValues values = new ContentValues();
values.put("name", repo.name);
values.put("address", repo.address);
values.put("description", repo.description);
values.put("version", repo.version);
values.put("inuse", repo.inuse);
@ -1402,13 +1343,24 @@ public class DB {
}
values.put("maxage", repo.maxage);
values.put("lastetag", (String) null);
db.update(TABLE_REPO, values, "address = ?",
new String[] { repo.address });
db.update(TABLE_REPO, values, field + " = ?",
new String[] { value });
}
/**
* Updates the lastUpdated time for every enabled repo.
*/
public void refreshLastUpdates() {
ContentValues values = new ContentValues();
values.put("lastUpdated", dateFormat.format(new Date()));
db.update(TABLE_REPO, values, "inuse = 1",
new String[] {});
}
public void writeLastEtag(Repo repo) {
ContentValues values = new ContentValues();
values.put("lastetag", repo.lastetag);
values.put("lastUpdated", dateFormat.format(new Date()));
db.update(TABLE_REPO, values, "address = ?",
new String[] { repo.address });
}
@ -1429,7 +1381,7 @@ public class DB {
if (fingerprint == null) {
fingerprint = calcedFingerprint;
} else if (calcedFingerprint != null) {
fingerprint = fingerprint.toUpperCase();
fingerprint = fingerprint.toUpperCase(Locale.ENGLISH);
if (!fingerprint.equals(calcedFingerprint)) {
throw new SecurityException("Given fingerprint does not match calculated one! ("
+ fingerprint + " != " + calcedFingerprint);
@ -1441,18 +1393,23 @@ public class DB {
db.insert(TABLE_REPO, null, values);
}
public void doDisableRepos(List<String> addresses, boolean remove) {
if (addresses.isEmpty()) return;
public void doDisableRepos(List<Repo> repos, boolean remove) {
if (repos.isEmpty()) return;
db.beginTransaction();
try {
for (String address : addresses) {
// TODO: Replace with
// "delete from apk join repo where repo in (?, ?, ...)
// "update repo set inuse = 0 | delete from repo ] where repo in (?, ?, ...)
try {
for (Repo repo : repos) {
String address = repo.address;
// Before removing the repo, remove any apks that are
// connected to it...
Cursor c = null;
try {
c = db.query(TABLE_REPO, new String[] { "id" },
"address = ?", new String[] { address },
c = db.query(TABLE_REPO, new String[]{"id"},
"address = ?", new String[]{address},
null, null, null, null);
c.moveToFirst();
if (!c.isAfterLast()) {
@ -1467,6 +1424,13 @@ public class DB {
if (remove)
db.delete(TABLE_REPO, "address = ?",
new String[] { address });
else {
ContentValues values = new ContentValues(2);
values.put("inuse", 0);
values.put("lastetag", (String)null);
db.update(TABLE_REPO, values, "address = ?",
new String[] { address });
}
}
List<App> apps = getApps(false);
for (App app : apps) {

View File

@ -19,22 +19,15 @@
package org.fdroid.fdroid;
import android.content.*;
import android.content.res.Configuration;
import android.support.v4.view.MenuItemCompat;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.NotificationManager;
import android.app.ProgressDialog;
import android.content.*;
import android.content.pm.PackageInfo;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.ResultReceiver;
import android.support.v4.app.FragmentActivity;
import android.support.v4.view.ViewPager;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
@ -42,6 +35,11 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.*;
import android.support.v4.app.FragmentActivity;
import android.support.v4.view.MenuItemCompat;
import android.support.v4.view.ViewPager;
import org.fdroid.fdroid.compat.TabManager;
import org.fdroid.fdroid.views.AppListFragmentPageAdapter;
@ -59,8 +57,6 @@ public class FDroid extends FragmentActivity {
private static final int ABOUT = Menu.FIRST + 3;
private static final int SEARCH = Menu.FIRST + 4;
private ProgressDialog pd;
private ViewPager viewPager;
private AppListManager manager = null;
@ -131,8 +127,6 @@ public class FDroid extends FragmentActivity {
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);
MenuItem search = menu.add(Menu.NONE, SEARCH, 3, R.string.menu_search).setIcon(
@ -231,7 +225,7 @@ public class FDroid extends FragmentActivity {
case REQUEST_APPDETAILS:
break;
case REQUEST_MANAGEREPOS:
if (data.hasExtra("update")) {
if (data.hasExtra(ManageRepo.REQUEST_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);
@ -241,7 +235,7 @@ public class FDroid extends FragmentActivity {
@Override
public void onClick(DialogInterface dialog,
int whichButton) {
updateRepos();
updateRepos();
}
});
ask_alrt.setNegativeButton(getString(R.string.no),
@ -249,6 +243,7 @@ public class FDroid extends FragmentActivity {
@Override
public void onClick(DialogInterface dialog,
int whichButton) {
// do nothing
}
});
AlertDialog alert = ask_alrt.create();
@ -293,34 +288,6 @@ public class FDroid extends FragmentActivity {
});
}
// For receiving results from the UpdateService when we've told it to
// update in response to a user request.
private class UpdateReceiver extends ResultReceiver {
public UpdateReceiver(Handler handler) {
super(handler);
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
String message = resultData.getString(UpdateService.RESULT_MESSAGE);
boolean finished = false;
if (resultCode == UpdateService.STATUS_ERROR) {
Toast.makeText(FDroid.this, message, Toast.LENGTH_LONG).show();
finished = true;
} else if (resultCode == UpdateService.STATUS_CHANGES) {
repopulateViews();
finished = true;
} else if (resultCode == UpdateService.STATUS_SAME) {
finished = true;
} else if (resultCode == UpdateService.STATUS_INFO) {
pd.setMessage(message);
}
if (finished && pd.isShowing())
pd.dismiss();
}
}
/**
* The first time the app is run, we will have an empty app list.
* If this is the case, we will attempt to update with the default repo.
@ -346,16 +313,14 @@ public class FDroid extends FragmentActivity {
// is told to do the update, which will result in the database changing. The
// UpdateReceiver class should get told when this is finished.
public void updateRepos() {
pd = ProgressDialog.show(this, getString(R.string.process_wait_title),
getString(R.string.process_update_msg), true, true);
pd.setIcon(android.R.drawable.ic_dialog_info);
pd.setCanceledOnTouchOutside(false);
Intent intent = new Intent(this, UpdateService.class);
UpdateReceiver mUpdateReceiver = new UpdateReceiver(new Handler());
intent.putExtra("receiver", mUpdateReceiver);
startService(intent);
UpdateService.updateNow(this).setListener(new ProgressListener() {
@Override
public void onProgress(Event event) {
if (event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES){
repopulateViews();
}
}
});
}
private TabManager getTabManager() {

View File

@ -19,26 +19,45 @@
package org.fdroid.fdroid;
import java.io.File;
import java.lang.Runtime;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;
import android.app.Application;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import android.app.Activity;
import android.preference.PreferenceManager;
import android.util.Log;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import org.fdroid.fdroid.Utils;
import android.graphics.Bitmap;
import android.preference.PreferenceManager;
import android.util.Log;
import com.nostra13.universalimageloader.cache.disc.impl.LimitedAgeDiscCache;
import com.nostra13.universalimageloader.cache.disc.impl.UnlimitedDiscCache;
import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
import com.nostra13.universalimageloader.utils.StorageUtils;
import de.duenndns.ssl.MemorizingTrustManager;
import org.thoughtcrime.ssl.pinning.PinningTrustManager;
import org.thoughtcrime.ssl.pinning.SystemKeyStore;
public class FDroidApp extends Application {
private static enum Theme {
@ -117,6 +136,44 @@ public class FDroidApp extends Application {
.threadPoolSize(Runtime.getRuntime().availableProcessors() * 2)
.build();
ImageLoader.getInstance().init(config);
try {
SSLContext sc = SSLContext.getInstance("TLS");
X509TrustManager defaultTrustManager = null;
/*
* init a trust manager factory with a null keystore to access the system trust managers
*/
TrustManagerFactory tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
KeyStore ks = null;
tmf.init(ks);
TrustManager[] mgrs = tmf.getTrustManagers();
if(mgrs.length > 0 && mgrs[0] instanceof X509TrustManager)
defaultTrustManager = (X509TrustManager) mgrs[0];
/*
* compose a chain of trust managers as follows:
* MemorizingTrustManager -> Pinning Trust Manager -> System Trust Manager
*/
PinningTrustManager pinMgr = new PinningTrustManager(SystemKeyStore.getInstance(ctx),FDroidCertPins.getPinList(), 0);
MemorizingTrustManager memMgr = new MemorizingTrustManager(ctx, pinMgr, defaultTrustManager);
/*
* initialize a SSLContext with the outermost trust manager, use this
* context to set the default SSL socket factory for the HTTPSURLConnection
* class.
*/
sc.init(null, new TrustManager[] {memMgr}, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (KeyManagementException e) {
Log.e("FDroid", "Unable to set up trust manager chain. KeyManagementException");
} catch (NoSuchAlgorithmException e) {
Log.e("FDroid", "Unable to set up trust manager chain. NoSuchAlgorithmException");
} catch (KeyStoreException e) {
Log.e("FDroid", "Unable to set up trust manager chain. KeyStoreException");
}
}
private Context ctx;

View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2010-12 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 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.fdroid.fdroid;
import java.util.ArrayList;
import java.util.Arrays;
public class FDroidCertPins {
public static final String[] DEFAULT_PINS =
{
/*
* SubjectDN: CN=f-droid.org, OU=PositiveSSL, OU=Domain Control Validated
* IssuerDN: CN=PositiveSSL CA 2, O=COMODO CA Limited, L=Salford, ST=Greater Manchester, C=GB
* Fingerprint: 84B91CDF2312CB9BA7F3BE803783302F8D8C299F
* SPKI Pin: 638F93856E1F5EDFCBD40C46D4160CFF21B0713A
*/
"638F93856E1F5EDFCBD40C46D4160CFF21B0713A",
/*
* SubjectDN: CN=guardianproject.info, OU=Gandi Standard SSL, OU=Domain Control Validated
* IssuerDN: CN=Gandi Standard SSL CA, O=GANDI SAS, C=FR
* Fingerprint: 187C2573E924DFCBFF2A781A2F99D71C6E031828
* SPKI Pin: EB6BBC6C6BAEEA20CB0F3357720D86E0F3A526F4
*/
"EB6BBC6C6BAEEA20CB0F3357720D86E0F3A526F4",
};
public static ArrayList<String> PINLIST = null;
public static String[] getPinList()
{
if(PINLIST == null)
{
PINLIST = new ArrayList<String>();
PINLIST.addAll(Arrays.asList(DEFAULT_PINS));
}
return PINLIST.toArray(new String[PINLIST.size()]);
}
}

View File

@ -26,6 +26,9 @@ import java.io.FileInputStream;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.Locale;
public class Hasher {
@ -86,7 +89,7 @@ public class Hasher {
if (hashCache == null) getHash();
if (otherHash == null || hashCache.equals(""))
return false;
return hashCache.equals(otherHash.toLowerCase());
return hashCache.equals(otherHash.toLowerCase(Locale.ENGLISH));
}
public void reset() {
@ -94,6 +97,16 @@ public class Hasher {
digest.reset();
}
public static String hex(Certificate cert) {
byte[] encoded = null;
try {
encoded = cert.getEncoded();
} catch(CertificateEncodingException e) {
encoded = new byte[0];
}
return hex(encoded);
}
public static String hex(byte[] sig) {
byte[] csig = new byte[sig.length * 2];
for (int j = 0; j < sig.length; j++) {

View File

@ -19,72 +19,59 @@
package org.fdroid.fdroid;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.ListActivity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils;
import android.support.v4.view.MenuItemCompat;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.*;
import org.fdroid.fdroid.DB.Repo;
import org.fdroid.fdroid.compat.ActionBarCompat;
import org.fdroid.fdroid.compat.ClipboardCompat;
import org.fdroid.fdroid.views.RepoAdapter;
import org.fdroid.fdroid.views.RepoDetailsActivity;
import org.fdroid.fdroid.views.fragments.RepoDetailsFragment;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
public class ManageRepo extends ListActivity {
private final int ADD_REPO = 1;
private final int REM_REPO = 2;
private static final String DEFAULT_NEW_REPO_TEXT = "https://";
private final int ADD_REPO = 1;
private final int UPDATE_REPOS = 2;
private boolean changed = false;
/**
* If we have a new repo added, or the address of a repo has changed, then
* we when we're finished, we'll set this boolean to true in the intent
* that we finish with, to signify that we want the main list of apps
* updated.
*/
public static final String REQUEST_UPDATE = "update";
private enum PositiveAction {
ADD_NEW, ENABLE, IGNORE
}
private PositiveAction positiveAction;
private List<DB.Repo> repos;
private boolean changed = false;
private static List<String> reposToDisable;
private static List<String> reposToRemove;
private RepoAdapter repoAdapter;
public void disableRepo(String address) {
if (reposToDisable.contains(address)) return;
reposToDisable.add(address);
}
public void removeRepo(String address) {
if (reposToRemove.contains(address)) return;
reposToRemove.add(address);
}
public void removeRepos(List<String> addresses) {
for (String address : addresses)
removeRepo(address);
}
/**
* True if activity started with an intent such as from QR code. False if
* opened from, e.g. the main menu.
*/
private boolean isImportingRepo = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -92,11 +79,15 @@ public class ManageRepo extends ListActivity {
((FDroidApp) getApplication()).applyTheme(this);
super.onCreate(savedInstanceState);
ActionBarCompat abCompat = ActionBarCompat.create(this);
abCompat.setDisplayHomeAsUpEnabled(true);
setContentView(R.layout.repolist);
repoAdapter = new RepoAdapter(this);
setListAdapter(repoAdapter);
/*
TODO: Find some other way to display this info, now that we use the ListView widgets...
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(getBaseContext());
@ -112,8 +103,7 @@ public class ManageRepo extends ListActivity {
}
tv_lastCheck.setText(getString(R.string.last_update_check,s_lastUpdateCheck));
reposToRemove = new ArrayList<String>();
reposToDisable = new ArrayList<String>();
*/
/* let's see if someone is trying to send us a new repo */
Intent intent = getIntent();
@ -126,6 +116,9 @@ public class ManageRepo extends ListActivity {
String host = uri.getHost().toLowerCase(Locale.ENGLISH);
if (scheme.equals("fdroidrepos") || scheme.equals("fdroidrepo")
|| scheme.equals("https") || scheme.equals("http")) {
isImportingRepo = true;
// QRCode are more efficient in all upper case, so some incoming
// URLs might be encoded in all upper case. Therefore, we allow
// the standard paths to be encoded all upper case, then they'll
@ -147,81 +140,95 @@ public class ManageRepo extends ListActivity {
@Override
protected void onResume() {
super.onResume();
redraw();
}
private void redraw() {
try {
DB db = DB.getDB();
repos = db.getRepos();
} finally {
DB.releaseDB();
}
List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
Map<String, Object> server_line;
for (DB.Repo repo : repos) {
server_line = new HashMap<String, Object>();
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);
}
if (repo.fingerprint != null) {
server_line.put("fingerprint", repo.fingerprint);
}
result.add(server_line);
}
SimpleAdapter show_out = new SimpleAdapter(this, result,
R.layout.repolisticons, new String[] { "address", "inuse",
"fingerprint" }, new int[] { R.id.uri, R.id.img,
R.id.fingerprint });
setListAdapter(show_out);
refreshList();
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
try {
DB db = DB.getDB();
String address = repos.get(position).address;
db.changeServerStatus(address);
// TODO: Disabling and re-enabling a repo will delete its apks too.
disableRepo(address);
} finally {
DB.releaseDB();
}
changed = true;
redraw();
DB.Repo repo = (DB.Repo)getListView().getItemAtPosition(position);
editRepo(repo);
}
private void refreshList() {
repoAdapter.refresh();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuItem item = 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);
MenuItemCompat.setShowAsAction(item,
MenuItemCompat.SHOW_AS_ACTION_IF_ROOM |
MenuItem updateItem = menu.add(Menu.NONE, UPDATE_REPOS, 1,
R.string.menu_update_repo).setIcon(R.drawable.ic_menu_refresh);
MenuItemCompat.setShowAsAction(updateItem,
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
MenuItem addItem = menu.add(Menu.NONE, ADD_REPO, 1, R.string.menu_add_repo).setIcon(
android.R.drawable.ic_menu_add);
MenuItemCompat.setShowAsAction(addItem,
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
return true;
}
protected void addRepo(String repoUri, String fingerprint) {
public static final int SHOW_REPO_DETAILS = 1;
public void editRepo(DB.Repo repo) {
Log.d("FDroid", "Showing details screen for repo: '" + repo + "'.");
Intent intent = new Intent(this, RepoDetailsActivity.class);
intent.putExtra(RepoDetailsFragment.ARG_REPO_ID, repo.id);
startActivityForResult(intent, SHOW_REPO_DETAILS);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == SHOW_REPO_DETAILS && resultCode == RESULT_OK) {
boolean wasDeleted = data.getBooleanExtra(RepoDetailsActivity.ACTION_IS_DELETED, false);
boolean wasEnabled = data.getBooleanExtra(RepoDetailsActivity.ACTION_IS_ENABLED, false);
boolean wasDisabled = data.getBooleanExtra(RepoDetailsActivity.ACTION_IS_DISABLED, false);
boolean wasChanged = data.getBooleanExtra(RepoDetailsActivity.ACTION_IS_CHANGED, false);
if (wasDeleted) {
int repoId = data.getIntExtra(RepoDetailsActivity.DATA_REPO_ID, 0);
remove(repoId);
} else if (wasEnabled || wasDisabled || wasChanged) {
changed = true;
}
}
}
private DB.Repo getRepoById(int repoId) {
for (int i = 0; i < getListAdapter().getCount(); i ++) {
DB.Repo repo = (DB.Repo)getListAdapter().getItem(i);
if (repo.id == repoId) {
return repo;
}
}
return null;
}
private void remove(int repoId) {
DB.Repo repo = getRepoById(repoId);
if (repo == null) {
return;
}
List<DB.Repo> reposToRemove = new ArrayList<DB.Repo>(1);
reposToRemove.add(repo);
try {
DB db = DB.getDB();
db.addRepo(repoUri, null, null, 0, 10, null, fingerprint, 0, true);
db.doDisableRepos(reposToRemove, true);
} finally {
DB.releaseDB();
}
refreshList();
}
protected List<Repo> getRepos() {
@ -253,16 +260,30 @@ public class ManageRepo extends ListActivity {
return super.onOptionsItemSelected(item);
}
private void updateRepos() {
UpdateService.updateNow(this).setListener(new ProgressListener() {
@Override
public void onProgress(Event event) {
// No need to prompt to update any more, we just did it!
changed = false;
refreshList();
}
});
}
private void showAddRepo() {
showAddRepo(getNewRepoUri(), null);
}
private void showAddRepo(String newAddress, String newFingerprint) {
LayoutInflater li = LayoutInflater.from(this);
View view = li.inflate(R.layout.addrepo, null);
Builder p = new AlertDialog.Builder(this).setView(view);
final AlertDialog alrt = p.create();
View view = getLayoutInflater().inflate(R.layout.addrepo, null);
final AlertDialog alrt = new AlertDialog.Builder(this).setView(view).create();
final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri);
final EditText fingerprintEditText = (EditText) view.findViewById(R.id.edit_fingerprint);
List<Repo> repos = getRepos();
final Repo repo = getRepoByAddress(newAddress, repos);
final Repo repo = newAddress != null && isImportingRepo ? getRepoByAddress(newAddress, repos) : null;
alrt.setIcon(android.R.drawable.ic_menu_add);
alrt.setTitle(getString(R.string.repo_add_title));
@ -271,15 +292,18 @@ public class ManageRepo extends ListActivity {
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String fp = fingerprintEditText.getText().toString();
// the DB uses null for no fingerprint but the above
// code returns "" rather than null if its blank
if (fp.equals(""))
fp = null;
if (positiveAction == PositiveAction.ADD_NEW)
addRepoPositiveAction(uriEditText.getText().toString(), fp, null);
createNewRepo(uriEditText.getText().toString(), fp);
else if (positiveAction == PositiveAction.ENABLE)
addRepoPositiveAction(null, null, repo);
createNewRepo(repo);
}
});
@ -290,6 +314,7 @@ public class ManageRepo extends ListActivity {
public void onClick(DialogInterface dialog, int which) {
setResult(Activity.RESULT_CANCELED);
finish();
return;
}
});
alrt.show();
@ -332,29 +357,58 @@ public class ManageRepo extends ListActivity {
}
}
if (newAddress != null)
uriEditText.setText(newAddress);
if (newFingerprint != null)
fingerprintEditText.setText(newFingerprint);
if (newAddress != null) {
// This trick of emptying text then appending,
// rather than just setting in the first place,
// is neccesary to move the cursor to the end of the input.
uriEditText.setText("");
uriEditText.append(newAddress);
}
}
private void addRepoPositiveAction(String address, String fingerprint, Repo repo) {
if (address != null) {
addRepo(address, fingerprint);
} else if (repo != null) {
// force-enable an existing repo
repo.inuse = true;
try {
DB db = DB.getDB();
db.updateRepoByAddress(repo);
} finally {
DB.releaseDB();
}
/**
* Adds a new repo to the database.
*/
private void createNewRepo(String address, String fingerprint) {
try {
DB db = DB.getDB();
db.addRepo(address, null, null, 0, 10, null, fingerprint, 0, true);
} finally {
DB.releaseDB();
}
finishedAddingRepo();
}
/**
* Seeing as this repo already exists, we will force it to be enabled again.
*/
private void createNewRepo(Repo repo) {
repo.inuse = true;
try {
DB db = DB.getDB();
db.updateRepoByAddress(repo);
} finally {
DB.releaseDB();
}
finishedAddingRepo();
}
/**
* If started by an intent that expects a result (e.g. QR codes) then we
* will set a result and finish. Otherwise, we'll refresh the list of
* repos to reflect the newly created repo.
*/
private void finishedAddingRepo() {
changed = true;
redraw();
setResult(Activity.RESULT_OK);
finish();
if (isImportingRepo) {
setResult(Activity.RESULT_OK);
finish();
} else {
refreshList();
}
}
@Override
@ -362,89 +416,77 @@ public class ManageRepo extends ListActivity {
super.onMenuItemSelected(featureId, item);
switch (item.getItemId()) {
case ADD_REPO:
showAddRepo(null, null);
if (item.getItemId() == ADD_REPO) {
showAddRepo();
return true;
case REM_REPO:
final List<String> rem_lst = new ArrayList<String>();
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() {
@Override
public void onClick(DialogInterface dialog,
int whichButton, boolean isChecked) {
if (isChecked) {
rem_lst.add(repos.get(whichButton).address);
} else {
rem_lst.remove(repos.get(whichButton).address);
}
}
});
builder.setPositiveButton(getString(R.string.ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int whichButton) {
try {
DB db = DB.getDB();
removeRepos(rem_lst);
} finally {
DB.releaseDB();
}
changed = true;
redraw();
}
});
builder.setNegativeButton(getString(R.string.cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int whichButton) {
}
});
AlertDialog alert = builder.create();
alert.show();
} else if (item.getItemId() == UPDATE_REPOS) {
updateRepos();
return true;
}
return true;
return false;
}
/**
* If there is text in the clipboard, and it looks like a URL, use that.
* Otherwise return "https://".
*/
private String getNewRepoUri() {
ClipboardCompat clipboard = ClipboardCompat.create(this);
String text = clipboard.getText();
if (text != null) {
try {
new URL(text);
} catch (MalformedURLException e) {
text = null;
}
}
if (text == null) {
text = DEFAULT_NEW_REPO_TEXT;
}
return text;
}
@Override
public void finish() {
if (!reposToRemove.isEmpty()) {
try {
DB db = DB.getDB();
db.doDisableRepos(reposToRemove, true);
} finally {
DB.releaseDB();
}
((FDroidApp) getApplication()).invalidateAllApps();
}
if (!reposToDisable.isEmpty()) {
try {
DB db = DB.getDB();
db.doDisableRepos(reposToDisable, false);
} finally {
DB.releaseDB();
}
((FDroidApp) getApplication()).invalidateAllApps();
}
Intent ret = new Intent();
if (changed)
ret.putExtra("update", true);
this.setResult(RESULT_OK, ret);
if (changed) {
Log.i("FDroid", "Repo details have changed, prompting for update.");
ret.putExtra(REQUEST_UPDATE, true);
}
setResult(RESULT_OK, ret);
super.finish();
}
/**
* NOTE: If somebody toggles a repo off then on again, it will have removed
* all apps from the index when it was toggled off, so when it is toggled on
* again, then it will require a refresh.
*
* Previously, I toyed with the idea of remembering whether they had
* toggled on or off, and then only actually performing the function when
* the activity stopped, but I think that will be problematic. What about
* when they press the home button, or edit a repos details? It will start
* to become somewhat-random as to when the actual enabling, disabling is
* performed.
*
* So now, it just does the disable as soon as the user clicks "Off" and
* then removes the apps. To compensate for the removal of apps from
* index, it notifies the user via a toast that the apps have been removed.
* Also, as before, it will still prompt the user to update the repos if
* you toggled on on.
*/
public void setRepoEnabled(DB.Repo repo, boolean enabled) {
FDroidApp app = (FDroidApp)getApplication();
if (enabled) {
repo.enable(app);
changed = true;
} else {
repo.disable(app);
String notification = getString(R.string.repo_disabled_notification, repo.toString());
Toast.makeText(this, notification, Toast.LENGTH_LONG).show();
}
}
}

View File

@ -1,6 +1,8 @@
package org.fdroid.fdroid;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
public interface ProgressListener {
@ -9,7 +11,7 @@ public interface ProgressListener {
// I went a bit overboard with the overloaded constructors, but they all
// seemed potentially useful and unambiguous, so I just put them in there
// while I'm here.
public static class Event {
public static class Event implements Parcelable {
public static final int NO_VALUE = Integer.MIN_VALUE;
@ -49,6 +51,30 @@ public interface ProgressListener {
this.total = total;
this.data = data == null ? new Bundle() : data;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(type);
dest.writeInt(progress);
dest.writeInt(total);
dest.writeBundle(data);
}
public static final Parcelable.Creator<Event> CREATOR = new Parcelable.Creator<Event>() {
public Event createFromParcel(Parcel in) {
return new Event(in.readInt(), in.readInt(), in.readInt(), in.readBundle());
}
public Event[] newArray(int size) {
return new Event[size];
}
};
}
}

View File

@ -19,38 +19,16 @@
package org.fdroid.fdroid;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.cert.Certificate;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.net.ssl.SSLHandshakeException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import android.os.Bundle;
import org.fdroid.fdroid.updater.RepoUpdater;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import android.os.Bundle;
import android.content.Context;
import android.util.Log;
import java.text.ParseException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class RepoXMLHandler extends DefaultHandler {
@ -64,10 +42,10 @@ public class RepoXMLHandler extends DefaultHandler {
private DB.Apk curapk = null;
private StringBuilder curchars = new StringBuilder();
// After processing the XML, these will be null if the index didn't specify
// After processing the XML, these will be -1 if the index didn't specify
// them - otherwise it will be the value specified.
private String version;
private String maxage;
private int version = -1;
private int maxage = -1;
// After processing the XML, this will be null if the index specified a
// public key - otherwise a public key. This is used for TOFU where an
@ -82,15 +60,6 @@ public class RepoXMLHandler extends DefaultHandler {
private int progressCounter = 0;
private ProgressListener progressListener;
public static final int PROGRESS_TYPE_DOWNLOAD = 1;
public static final int PROGRESS_TYPE_PROCESS_XML = 2;
public static final String PROGRESS_DATA_REPO = "repo";
// The date format used in the repo XML file.
private SimpleDateFormat mXMLDateFormat = new SimpleDateFormat("yyyy-MM-dd");
private static final SimpleDateFormat logDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private int totalAppCount;
public RepoXMLHandler(DB.Repo repo, List<DB.App> appsList, ProgressListener listener) {
@ -104,6 +73,18 @@ public class RepoXMLHandler extends DefaultHandler {
progressListener = listener;
}
public int getMaxAge() { return maxage; }
public int getVersion() { return version; }
public String getDescription() { return description; }
public String getName() { return name; }
public String getPubKey() {
return pubkey;
}
@Override
public void characters(char[] ch, int start, int length) {
curchars.append(ch, start, length);
@ -176,7 +157,7 @@ public class RepoXMLHandler extends DefaultHandler {
}
} else if (curel.equals("added")) {
try {
curapk.added = str.length() == 0 ? null : mXMLDateFormat
curapk.added = str.length() == 0 ? null : DB.dateFormat
.parse(str);
} catch (ParseException e) {
curapk.added = null;
@ -223,7 +204,7 @@ public class RepoXMLHandler extends DefaultHandler {
curapp.detail_trackerURL = str;
} else if (curel.equals("added")) {
try {
curapp.added = str.length() == 0 ? null : mXMLDateFormat
curapp.added = str.length() == 0 ? null : DB.dateFormat
.parse(str);
} catch (ParseException e) {
curapp.added = null;
@ -231,7 +212,7 @@ public class RepoXMLHandler extends DefaultHandler {
} else if (curel.equals("lastupdated")) {
try {
curapp.lastUpdated = str.length() == 0 ? null
: mXMLDateFormat.parse(str);
: DB.dateFormat.parse(str);
} catch (ParseException e) {
curapp.lastUpdated = null;
}
@ -250,15 +231,11 @@ public class RepoXMLHandler extends DefaultHandler {
} else if (curel.equals("requirements")) {
curapp.requirements = DB.CommaSeparatedList.make(str);
}
} else if (curel.equals("description")) {
description = str;
}
}
private static Bundle createProgressData(String repoAddress) {
Bundle data = new Bundle();
data.putString(PROGRESS_DATA_REPO, repoAddress);
return data;
}
@Override
public void startElement(String uri, String localName, String qName,
Attributes attributes) throws SAXException {
@ -268,8 +245,21 @@ public class RepoXMLHandler extends DefaultHandler {
String pk = attributes.getValue("", "pubkey");
if (pk != null)
pubkey = pk;
version = attributes.getValue("", "version");
maxage = attributes.getValue("", "maxage");
String maxAgeAttr = attributes.getValue("", "maxage");
if (maxAgeAttr != null) {
try {
maxage = Integer.parseInt(maxAgeAttr);
} catch (NumberFormatException nfe) {}
}
String versionAttr = attributes.getValue("", "version");
if (versionAttr != null) {
try {
version = Integer.parseInt(versionAttr);
} catch (NumberFormatException nfe) {}
}
String nm = attributes.getValue("", "name");
if (nm != null)
name = nm;
@ -281,11 +271,11 @@ public class RepoXMLHandler extends DefaultHandler {
curapp = new DB.App();
curapp.detail_Populated = true;
curapp.id = attributes.getValue("", "id");
Bundle progressData = createProgressData(repo.address);
Bundle progressData = RepoUpdater.createProgressData(repo.address);
progressCounter ++;
progressListener.onProgress(
new ProgressListener.Event(
RepoXMLHandler.PROGRESS_TYPE_PROCESS_XML, progressCounter,
RepoUpdater.PROGRESS_TYPE_PROCESS_XML, progressCounter,
totalAppCount, progressData));
} else if (localName.equals("package") && curapp != null && curapk == null) {
@ -300,243 +290,6 @@ public class RepoXMLHandler extends DefaultHandler {
curchars.setLength(0);
}
// Get a remote file. Returns the HTTP response code.
// If 'etag' is not null, it's passed to the server as an If-None-Match
// header, in which case expect a 304 response if nothing changed.
// In the event of a 200 response ONLY, 'retag' (which should be passed
// empty) may contain an etag value for the response, or it may be left
// empty if none was available.
private static int getRemoteFile(Context ctx, String url, String dest,
String etag, StringBuilder retag,
ProgressListener progressListener,
ProgressListener.Event progressEvent) throws MalformedURLException,
IOException {
long startTime = System.currentTimeMillis();
URL u = new URL(url);
HttpURLConnection connection = (HttpURLConnection) u.openConnection();
if (etag != null)
connection.setRequestProperty("If-None-Match", etag);
int code = connection.getResponseCode();
if (code == 200) {
// Testing in the emulator for me, showed that figuring out the filesize took about 1 to 1.5 seconds.
// To put this in context, downloading a repo of:
// - 400k takes ~6 seconds
// - 5k takes ~3 seconds
// on my connection. I think the 1/1.5 seconds is worth it, because as the repo grows, the tradeoff will
// become more worth it.
progressEvent.total = connection.getContentLength();
Log.d("FDroid", "Downloading " + progressEvent.total + " bytes from " + url);
InputStream input = null;
OutputStream output = null;
try {
input = connection.getInputStream();
output = ctx.openFileOutput(dest, Context.MODE_PRIVATE);
Utils.copy(input, output, progressListener, progressEvent);
} finally {
Utils.closeQuietly(output);
Utils.closeQuietly(input);
}
String et = connection.getHeaderField("ETag");
if (et != null)
retag.append(et);
}
Log.d("FDroid", "Fetched " + url + " (" + progressEvent.total +
" bytes) in " + (System.currentTimeMillis() - startTime) +
"ms");
return code;
}
// Do an update from the given repo. All applications found, and their
// APKs, are added to 'apps'. (If 'apps' already contains an app, its
// APKs are merged into the existing one).
// Returns null if successful, otherwise an error message to be displayed
// to the user (if there is an interactive user!)
// 'newetag' should be passed empty. On success, it may contain an etag
// value for the index that was successfully processed, or it may contain
// null if none was available.
public static String doUpdate(Context ctx, DB.Repo repo,
List<DB.App> appsList, StringBuilder newetag, List<Integer> keeprepos,
ProgressListener progressListener) {
try {
int code = 0;
if (repo.pubkey != null) {
// This is a signed repo - we download the jar file,
// check the signature, and extract the index...
Log.d("FDroid", "Getting signed index from " + repo.address + " at " +
logDateFormat.format(new Date(System.currentTimeMillis())));
String address = repo.address + "/index.jar?client_version="
+ ctx.getString(R.string.version_name);
Bundle progressData = createProgressData(repo.address);
ProgressListener.Event event = new ProgressListener.Event(
RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD, progressData);
code = getRemoteFile(ctx, address, "tempindex.jar",
repo.lastetag, newetag, progressListener, event );
if (code == 200) {
String jarpath = ctx.getFilesDir() + "/tempindex.jar";
JarFile jar = null;
JarEntry je;
Certificate[] certs;
try {
jar = new JarFile(jarpath, true);
je = (JarEntry) jar.getEntry("index.xml");
File efile = new File(ctx.getFilesDir(),
"/tempindex.xml");
InputStream input = null;
OutputStream output = null;
try {
input = jar.getInputStream(je);
output = new FileOutputStream(efile);
Utils.copy(input, output);
} finally {
Utils.closeQuietly(output);
Utils.closeQuietly(input);
}
certs = je.getCertificates();
} catch (SecurityException e) {
Log.e("FDroid", "Invalid hash for index file");
return "Invalid hash for index file";
} finally {
if (jar != null) {
jar.close();
}
}
if (certs == null) {
Log.d("FDroid", "No signature found in index");
return "No signature found in index";
}
Log.d("FDroid", "Index has " + certs.length + " signature"
+ (certs.length > 1 ? "s." : "."));
boolean match = false;
for (Certificate cert : certs) {
String certdata = Hasher.hex(cert.getEncoded());
if (repo.pubkey.equals(certdata)) {
match = true;
break;
}
}
if (!match) {
Log.d("FDroid", "Index signature mismatch");
return "Index signature mismatch";
}
}
} else {
// It's an old-fashioned unsigned repo...
Log.d("FDroid", "Getting unsigned index from " + repo.address);
Bundle eventData = createProgressData(repo.address);
ProgressListener.Event event = new ProgressListener.Event(
RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD, eventData);
code = getRemoteFile(ctx, repo.address + "/index.xml",
"tempindex.xml", repo.lastetag, newetag,
progressListener, event);
}
if (code == 200) {
// Process the index...
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser sp = spf.newSAXParser();
XMLReader xr = sp.getXMLReader();
RepoXMLHandler handler = new RepoXMLHandler(repo, appsList, progressListener);
xr.setContentHandler(handler);
File tempIndex = new File(ctx.getFilesDir() + "/tempindex.xml");
BufferedReader r = new BufferedReader(new FileReader(tempIndex));
// A bit of a hack, this might return false positives if an apps description
// or some other part of the XML file contains this, but it is a pretty good
// estimate and makes the progress counter more informative.
// As with asking the server about the size of the index before downloading,
// this also has a time tradeoff. It takes about three seconds to iterate
// through the file and count 600 apps on a slow emulator (v17), but if it is
// taking two minutes to update, the three second wait may be worth it.
final String APPLICATION = "<application";
handler.setTotalAppCount(Utils.countSubstringOccurrence(tempIndex, APPLICATION));
InputSource is = new InputSource(r);
xr.parse(is);
if (handler.pubkey != null && repo.pubkey == null) {
// We read an unsigned index, but that indicates that
// a signed version is now available...
Log.d("FDroid",
"Public key found - switching to signed repo for future updates");
repo.pubkey = handler.pubkey;
try {
DB db = DB.getDB();
db.updateRepoByAddress(repo);
} finally {
DB.releaseDB();
}
}
boolean updateRepo = false;
if (handler.version != null) {
int version = Integer.parseInt(handler.version);
if (version != repo.version) {
Log.d("FDroid", "Repo specified a new version: from "
+ repo.version + " to " + version);
repo.version = version;
updateRepo = true;
}
}
if (handler.maxage != null) {
int maxage = Integer.parseInt(handler.maxage);
if (maxage != repo.maxage) {
Log.d("FDroid",
"Repo specified a new maximum age - updated");
repo.maxage = maxage;
updateRepo = true;
}
}
if (updateRepo) {
try {
DB db = DB.getDB();
db.updateRepoByAddress(repo);
} finally {
DB.releaseDB();
}
}
} else if (code == 304) {
// The index is unchanged since we last read it. We just mark
// everything that came from this repo as being updated.
Log.d("FDroid", "Repo index for " + repo.address
+ " is up to date (by etag)");
keeprepos.add(repo.id);
// Make sure we give back the same etag. (The 200 route will
// have supplied a new one.
newetag.append(repo.lastetag);
} else {
return "Failed to read index - HTTP response "
+ Integer.toString(code);
}
} catch (SSLHandshakeException sslex) {
Log.e("FDroid", "SSLHandShakeException updating from "
+ repo.address + ":\n" + Log.getStackTraceString(sslex));
return "A problem occurred while establishing an SSL connection. If this problem persists, AND you have a very old device, you could try using http instead of https for the repo URL.";
} catch (Exception e) {
Log.e("FDroid", "Exception updating from " + repo.address + ":\n"
+ Log.getStackTraceString(e));
return "Failed to update - " + e.getMessage();
} finally {
ctx.deleteFile("tempindex.xml");
ctx.deleteFile("tempindex.jar");
}
return null;
}
public void setTotalAppCount(int totalAppCount) {
this.totalAppCount = totalAppCount;
}

View File

@ -21,33 +21,31 @@ package org.fdroid.fdroid;
import java.util.ArrayList;
import java.util.List;
import android.app.AlarmManager;
import android.app.IntentService;
import android.app.PendingIntent;
import android.app.NotificationManager;
import android.app.*;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.os.*;
import android.preference.PreferenceManager;
import android.util.Log;
import org.fdroid.fdroid.updater.RepoUpdater;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.widget.Toast;
public class UpdateService extends IntentService implements ProgressListener {
public static final String RESULT_MESSAGE = "msg";
public static final int STATUS_CHANGES = 0;
public static final int STATUS_SAME = 1;
public static final int STATUS_ERROR = 2;
public static final int STATUS_INFO = 3;
public static final String RESULT_EVENT = "event";
public static final int STATUS_COMPLETE_WITH_CHANGES = 0;
public static final int STATUS_COMPLETE_AND_SAME = 1;
public static final int STATUS_ERROR = 2;
public static final int STATUS_INFO = 3;
private ResultReceiver receiver = null;
@ -55,6 +53,76 @@ public class UpdateService extends IntentService implements ProgressListener {
super("UpdateService");
}
// For receiving results from the UpdateService when we've told it to
// update in response to a user request.
public static class UpdateReceiver extends ResultReceiver {
private Context context;
private ProgressDialog dialog;
private ProgressListener listener;
public UpdateReceiver(Handler handler) {
super(handler);
}
public UpdateReceiver setContext(Context context) {
this.context = context;
return this;
}
public UpdateReceiver setDialog(ProgressDialog dialog) {
this.dialog = dialog;
return this;
}
public UpdateReceiver setListener(ProgressListener listener) {
this.listener = listener;
return this;
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
String message = resultData.getString(UpdateService.RESULT_MESSAGE);
boolean finished = false;
if (resultCode == UpdateService.STATUS_ERROR) {
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
finished = true;
} else if (resultCode == UpdateService.STATUS_COMPLETE_WITH_CHANGES
|| resultCode == UpdateService.STATUS_COMPLETE_AND_SAME) {
finished = true;
} else if (resultCode == UpdateService.STATUS_INFO) {
dialog.setMessage(message);
}
// Forward the progress event on to anybody else who'd like to know.
if (listener != null) {
Parcelable event = resultData.getParcelable(UpdateService.RESULT_EVENT);
if (event != null && event instanceof Event) {
listener.onProgress((Event)event);
}
}
if (finished && dialog.isShowing())
dialog.dismiss();
}
}
public static UpdateReceiver updateNow(Context context) {
String title = context.getString(R.string.process_wait_title);
String message = context.getString(R.string.process_update_msg);
ProgressDialog dialog = ProgressDialog.show(context, title, message, true, true);
dialog.setIcon(android.R.drawable.ic_dialog_info);
dialog.setCanceledOnTouchOutside(false);
Intent intent = new Intent(context, UpdateService.class);
UpdateReceiver receiver = new UpdateReceiver(new Handler());
receiver.setContext(context).setDialog(dialog);
intent.putExtra("receiver", receiver);
context.startService(intent);
return receiver;
}
// Schedule (or cancel schedule for) this service, according to the
// current preferences. Should be called a) at boot, b) if the preference
// is changed, or c) on startup, in case we get upgraded.
@ -87,10 +155,17 @@ public class UpdateService extends IntentService implements ProgressListener {
}
protected void sendStatus(int statusCode, String message) {
sendStatus(statusCode, message, null);
}
protected void sendStatus(int statusCode, String message, Event event) {
if (receiver != null) {
Bundle resultData = new Bundle();
if (message != null && message.length() > 0)
resultData.putString(RESULT_MESSAGE, message);
if (event == null)
event = new Event(statusCode);
resultData.putParcelable(RESULT_EVENT, event);
receiver.send(statusCode, resultData);
}
}
@ -163,56 +238,50 @@ public class UpdateService extends IntentService implements ProgressListener {
// database while we do all the downloading, etc...
int updates = 0;
List<DB.Repo> repos;
List<DB.App> apps;
try {
DB db = DB.getDB();
repos = db.getRepos();
apps = db.getApps(false);
} finally {
DB.releaseDB();
}
// Process each repo...
List<DB.App> apps;
List<DB.App> updatingApps = new ArrayList<DB.App>();
List<Integer> keeprepos = new ArrayList<Integer>();
boolean success = true;
boolean changes = false;
for (DB.Repo repo : repos) {
if (repo.inuse) {
sendStatus(
STATUS_INFO,
getString(R.string.status_connecting_to_repo,
repo.address));
StringBuilder newetag = new StringBuilder();
String err = RepoXMLHandler.doUpdate(getBaseContext(),
repo, updatingApps, newetag, keeprepos, this);
if (err == null) {
String nt = newetag.toString();
if (!nt.equals(repo.lastetag)) {
repo.lastetag = newetag.toString();
changes = true;
}
if (!repo.inuse) {
continue;
}
sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address));
RepoUpdater updater = RepoUpdater.createUpdaterFor(getBaseContext(), repo);
updater.setProgressListener(this);
try {
updater.update();
if (updater.hasChanged()) {
updatingApps.addAll(updater.getApps());
changes = true;
} else {
success = false;
err = "Update failed for " + repo.address + " - " + err;
Log.d("FDroid", err);
if (errmsg.length() == 0)
errmsg = err;
else
errmsg += "\n" + err;
keeprepos.add(repo.id);
}
} catch (RepoUpdater.UpdateException e) {
errmsg += (errmsg.length() == 0 ? "" : "\n") + e.getMessage();
Log.e("FDroid", "Error updating repository " + repo.address + ": " + e.getMessage());
Log.e("FDroid", Log.getStackTraceString(e));
}
}
if (!changes && success) {
Log.d("FDroid",
"Not checking app details or compatibility, because all repos were up to date.");
"Not checking app details or compatibility, " +
"because all repos were up to date.");
} else if (changes && success) {
sendStatus(STATUS_INFO,
getString(R.string.status_checking_compatibility));
apps = ((FDroidApp) getApplication()).getApps();
DB db = DB.getDB();
try {
@ -231,7 +300,7 @@ public class UpdateService extends IntentService implements ProgressListener {
}
if (keepapp) {
DB.App app_k = null;
for (DB.App app2 : updatingApps) {
for (DB.App app2 : apps) {
if (app2.id.equals(app.id)) {
app_k = app2;
break;
@ -319,9 +388,9 @@ public class UpdateService extends IntentService implements ProgressListener {
e.putLong("lastUpdateCheck", System.currentTimeMillis());
e.commit();
if (changes) {
sendStatus(STATUS_CHANGES);
sendStatus(STATUS_COMPLETE_WITH_CHANGES);
} else {
sendStatus(STATUS_SAME);
sendStatus(STATUS_COMPLETE_AND_SAME);
}
}
@ -347,23 +416,17 @@ public class UpdateService extends IntentService implements ProgressListener {
*/
@Override
public void onProgress(ProgressListener.Event event) {
String message = "";
if (event.type == RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD) {
String repoAddress = event.data
.getString(RepoXMLHandler.PROGRESS_DATA_REPO);
String downloadedSize = Utils.getFriendlySize(event.progress);
String totalSize = Utils.getFriendlySize(event.total);
int percent = (int) ((double) event.progress / event.total * 100);
message = getString(R.string.status_download, repoAddress,
downloadedSize, totalSize, percent);
} else if (event.type == RepoXMLHandler.PROGRESS_TYPE_PROCESS_XML) {
String repoAddress = event.data
.getString(RepoXMLHandler.PROGRESS_DATA_REPO);
message = getString(R.string.status_processing_xml, repoAddress,
event.progress, event.total);
if (event.type == RepoUpdater.PROGRESS_TYPE_DOWNLOAD) {
String repoAddress = event.data.getString(RepoUpdater.PROGRESS_DATA_REPO);
String downloadedSize = Utils.getFriendlySize( event.progress );
String totalSize = Utils.getFriendlySize( event.total );
int percent = (int)((double)event.progress/event.total * 100);
message = getString(R.string.status_download, repoAddress, downloadedSize, totalSize, percent);
} else if (event.type == RepoUpdater.PROGRESS_TYPE_PROCESS_XML) {
String repoAddress = event.data.getString(RepoUpdater.PROGRESS_DATA_REPO);
message = getString(R.string.status_processing_xml, repoAddress, event.progress, event.total);
}
sendStatus(STATUS_INFO, message);
}
}

View File

@ -18,6 +18,9 @@
package org.fdroid.fdroid;
import android.os.Build;
import android.util.Log;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
@ -25,6 +28,10 @@ import java.io.FileReader;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.security.MessageDigest;
import java.util.Formatter;
import java.util.Locale;
import android.content.Context;
@ -37,6 +44,10 @@ public final class Utils {
private static final String[] FRIENDLY_SIZE_FORMAT = {
"%.0f B", "%.0f KiB", "%.1f MiB", "%.2f GiB" };
public static final SimpleDateFormat LOG_DATE_FORMAT =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
public static void copy(InputStream input, OutputStream output)
throws IOException {
@ -148,6 +159,36 @@ public final class Utils {
return count;
}
public static String formatFingerprint(DB.Repo repo) {
return formatFingerprint(repo.pubkey);
}
public static String formatFingerprint(String key) {
String fingerprintString;
if (key == null) {
return "";
}
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update(Hasher.unhex(key));
byte[] fingerprint = digest.digest();
Formatter formatter = new Formatter(new StringBuilder());
formatter.format("%02X", fingerprint[0]);
for (int i = 1; i < fingerprint.length; i++) {
formatter.format(i % 5 == 0 ? " %02X" : ":%02X",
fingerprint[i]);
}
fingerprintString = formatter.toString();
formatter.close();
} catch (Exception e) {
Log.w("FDroid", "Unable to get certificate fingerprint.\n"
+ Log.getStackTraceString(e));
fingerprintString = "";
}
return fingerprintString;
}
public static File getApkCacheDir(Context context) {
File apkCacheDir = new File(
StorageUtils.getCacheDirectory(context, true), "apks");

View File

@ -0,0 +1,55 @@
package org.fdroid.fdroid.compat;
import android.annotation.TargetApi;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.widget.CompoundButton;
import android.widget.Switch;
import android.widget.ToggleButton;
import org.fdroid.fdroid.ManageRepo;
public abstract class ClipboardCompat extends Compatibility {
public abstract String getText();
public static ClipboardCompat create(Context context) {
if (hasApi(11)) {
return new HoneycombClipboard(context);
} else {
return new OldClipboard();
}
}
}
@TargetApi(11)
class HoneycombClipboard extends ClipboardCompat {
private final ClipboardManager manager;
protected HoneycombClipboard(Context context) {
this.manager =
(ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
}
@Override
public String getText() {
CharSequence text = null;
if (manager.hasPrimaryClip()) {
ClipData data = manager.getPrimaryClip();
if (data.getItemCount() > 0) {
text = data.getItemAt(0).getText();
}
}
return text != null ? text.toString() : null;
}
}
class OldClipboard extends ClipboardCompat {
@Override
public String getText() {
return null;
}
}

View File

@ -0,0 +1,30 @@
package org.fdroid.fdroid.compat;
import java.util.HashSet;
import android.annotation.TargetApi;
import android.util.Log;
import android.os.Build;
public class SupportedArchitectures extends Compatibility {
private static HashSet<String> getOneAbi() {
HashSet<String> abis = new HashSet<String>(1);
abis.add(Build.CPU_ABI);
return abis;
}
@TargetApi(8)
private static HashSet<String> getTwoAbis() {
HashSet<String> abis = new HashSet<String>(2);
abis.add(Build.CPU_ABI);
abis.add(Build.CPU_ABI2);
return abis;
}
public static HashSet<String> getAbis() {
if (hasApi(8)) return getTwoAbis();
return getOneAbi();
}
}

View File

@ -0,0 +1,52 @@
package org.fdroid.fdroid.compat;
import android.annotation.TargetApi;
import android.widget.CompoundButton;
import android.widget.Switch;
import android.widget.ToggleButton;
import org.fdroid.fdroid.ManageRepo;
public abstract class SwitchCompat extends Compatibility {
protected final ManageRepo activity;
protected SwitchCompat(ManageRepo activity) {
this.activity = activity;
}
public abstract CompoundButton createSwitch();
public static SwitchCompat create(ManageRepo activity) {
if (hasApi(14)) {
return new IceCreamSwitch(activity);
} else {
return new OldSwitch(activity);
}
}
}
@TargetApi(14)
class IceCreamSwitch extends SwitchCompat {
protected IceCreamSwitch(ManageRepo activity) {
super(activity);
}
@Override
public CompoundButton createSwitch() {
return new Switch(activity);
}
}
class OldSwitch extends SwitchCompat {
protected OldSwitch(ManageRepo activity) {
super(activity);
}
@Override
public CompoundButton createSwitch() {
return new ToggleButton(activity);
}
}

View File

@ -0,0 +1,251 @@
package org.fdroid.fdroid.data;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import org.fdroid.fdroid.DB;
import org.fdroid.fdroid.R;
import java.util.ArrayList;
import java.util.List;
public class DBHelper extends SQLiteOpenHelper {
public static final String DATABASE_NAME = "fdroid";
private static final String CREATE_TABLE_REPO = "create table "
+ DB.TABLE_REPO + " (id integer primary key, address text not null, "
+ "name text, description text, inuse integer not null, "
+ "priority integer not null, pubkey text, fingerprint text, "
+ "maxage integer not null default 0, version integer not null default 0,"
+ "lastetag text, lastUpdated string);";
private static final String CREATE_TABLE_APK = "create table " + DB.TABLE_APK
+ " ( " + "id text not null, " + "version text not null, "
+ "repo integer not null, " + "hash text not null, "
+ "vercode int not null," + "apkName text not null, "
+ "size int not null," + "sig string," + "srcname string,"
+ "minSdkVersion integer," + "permissions string,"
+ "features string," + "nativecode string,"
+ "hashType string," + "added string,"
+ "compatible int not null," + "primary key(id,vercode));";
private static final String CREATE_TABLE_APP = "create table " + DB.TABLE_APP
+ " ( " + "id text not null, " + "name text not null, "
+ "summary text not null, " + "icon text, "
+ "description text not null, " + "license text not null, "
+ "webURL text, " + "trackerURL text, " + "sourceURL text, "
+ "curVersion text," + "curVercode integer,"
+ "antiFeatures string," + "donateURL string,"
+ "bitcoinAddr string," + "litecoinAddr string,"
+ "dogecoinAddr string,"
+ "flattrID string," + "requirements string,"
+ "categories string," + "added string,"
+ "lastUpdated string," + "compatible int not null,"
+ "ignoreAllUpdates int not null,"
+ "ignoreThisUpdate int not null,"
+ "primary key(id));";
private static final int DB_VERSION = 35;
private Context context;
public DBHelper(Context context) {
super(context, DATABASE_NAME, null, DB_VERSION);
this.context = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
createAppApk(db);
db.execSQL(CREATE_TABLE_REPO);
ContentValues values = new ContentValues();
values.put("address",
context.getString(R.string.default_repo_address));
values.put("name",
context.getString(R.string.default_repo_name));
values.put("description",
context.getString(R.string.default_repo_description));
values.put("version", 0);
String pubkey = context.getString(R.string.default_repo_pubkey);
String fingerprint = DB.calcFingerprint(pubkey);
values.put("pubkey", pubkey);
values.put("fingerprint", fingerprint);
values.put("maxage", 0);
values.put("inuse", 1);
values.put("priority", 10);
values.put("lastetag", (String) null);
db.insert(DB.TABLE_REPO, null, values);
values = new ContentValues();
values.put("address",
context.getString(R.string.default_repo_address2));
values.put("name",
context.getString(R.string.default_repo_name2));
values.put("description",
context.getString(R.string.default_repo_description2));
values.put("version", 0);
// default #2 is /archive which has the same key as /repo
values.put("pubkey", pubkey);
values.put("fingerprint", fingerprint);
values.put("maxage", 0);
values.put("inuse", 0);
values.put("priority", 20);
values.put("lastetag", (String) null);
db.insert(DB.TABLE_REPO, null, values);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.i("FDroid", "Upgrading database from v" + oldVersion + " v"
+ newVersion);
migradeRepoTable(db, oldVersion);
// The other tables are transient and can just be reset. Do this after
// the repo table changes though, because it also clears the lastetag
// fields which didn't always exist.
resetTransient(db);
addNameAndDescriptionToRepo(db, oldVersion);
addFingerprintToRepo(db, oldVersion);
addMaxAgeToRepo(db, oldVersion);
addVersionToRepo(db, oldVersion);
addLastUpdatedToRepo(db, oldVersion);
}
/**
* Migrate repo list to new structure. (No way to change primary
* key in sqlite - table must be recreated).
*/
private void migradeRepoTable(SQLiteDatabase db, int oldVersion) {
if (oldVersion < 20) {
List<DB.Repo> oldrepos = new ArrayList<DB.Repo>();
Cursor c = db.query(DB.TABLE_REPO,
new String[] { "address", "inuse", "pubkey" },
null, null, null, null, null);
c.moveToFirst();
while (!c.isAfterLast()) {
DB.Repo repo = new DB.Repo();
repo.address = c.getString(0);
repo.inuse = (c.getInt(1) == 1);
repo.pubkey = c.getString(2);
oldrepos.add(repo);
c.moveToNext();
}
c.close();
db.execSQL("drop table " + DB.TABLE_REPO);
db.execSQL(CREATE_TABLE_REPO);
for (DB.Repo repo : oldrepos) {
ContentValues values = new ContentValues();
values.put("address", repo.address);
values.put("inuse", repo.inuse);
values.put("priority", 10);
values.put("pubkey", repo.pubkey);
values.put("lastetag", (String) null);
db.insert(DB.TABLE_REPO, null, values);
}
}
}
/**
* Add a name and description to the repo table, and updates the two
* default repos with values from strings.xml.
*/
private void addNameAndDescriptionToRepo(SQLiteDatabase db, int oldVersion) {
if (oldVersion < 21) {
if (!columnExists(db, DB.TABLE_REPO, "name"))
db.execSQL("alter table " + DB.TABLE_REPO + " add column name text");
if (!columnExists(db, DB.TABLE_REPO, "description"))
db.execSQL("alter table " + DB.TABLE_REPO + " add column description text");
ContentValues values = new ContentValues();
values.put("name", context.getString(R.string.default_repo_name));
values.put("description", context.getString(R.string.default_repo_description));
db.update(DB.TABLE_REPO, values, "address = ?", new String[]{
context.getString(R.string.default_repo_address)});
values.clear();
values.put("name", context.getString(R.string.default_repo_name2));
values.put("description", context.getString(R.string.default_repo_description2));
db.update(DB.TABLE_REPO, values, "address = ?", new String[] {
context.getString(R.string.default_repo_address2) });
}
}
/**
* Add a fingerprint field to repos. For any field with a public key,
* calculate its fingerprint and save it to the database.
*/
private void addFingerprintToRepo(SQLiteDatabase db, int oldVersion) {
if (oldVersion < 29) {
if (!columnExists(db, DB.TABLE_REPO, "fingerprint"))
db.execSQL("alter table " + DB.TABLE_REPO + " add column fingerprint text");
List<DB.Repo> oldrepos = new ArrayList<DB.Repo>();
Cursor c = db.query(DB.TABLE_REPO,
new String[] { "address", "pubkey" },
null, null, null, null, null);
c.moveToFirst();
while (!c.isAfterLast()) {
DB.Repo repo = new DB.Repo();
repo.address = c.getString(0);
repo.pubkey = c.getString(1);
oldrepos.add(repo);
c.moveToNext();
}
c.close();
for (DB.Repo repo : oldrepos) {
ContentValues values = new ContentValues();
values.put("fingerprint", DB.calcFingerprint(repo.pubkey));
db.update(DB.TABLE_REPO, values, "address = ?", new String[] { repo.address });
}
}
}
private void addMaxAgeToRepo(SQLiteDatabase db, int oldVersion) {
if (oldVersion < 30) {
db.execSQL("alter table " + DB.TABLE_REPO + " add column maxage integer not null default 0");
}
}
private void addVersionToRepo(SQLiteDatabase db, int oldVersion) {
if (oldVersion < 33 && !columnExists(db, DB.TABLE_REPO, "version")) {
db.execSQL("alter table " + DB.TABLE_REPO + " add column version integer not null default 0");
}
}
private void addLastUpdatedToRepo(SQLiteDatabase db, int oldVersion) {
if (oldVersion < 35 && !columnExists(db, DB.TABLE_REPO, "lastUpdated")) {
db.execSQL("Alter table " + DB.TABLE_REPO + " add column lastUpdated string");
}
}
private void resetTransient(SQLiteDatabase db) {
context.getSharedPreferences("FDroid", Context.MODE_PRIVATE).edit()
.putBoolean("triedEmptyUpdate", false).commit();
db.execSQL("drop table " + DB.TABLE_APP);
db.execSQL("drop table " + DB.TABLE_APK);
db.execSQL("update " + DB.TABLE_REPO + " set lastetag = NULL");
createAppApk(db);
}
private static void createAppApk(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_APP);
db.execSQL("create index app_id on " + DB.TABLE_APP + " (id);");
db.execSQL(CREATE_TABLE_APK);
db.execSQL("create index apk_vercode on " + DB.TABLE_APK + " (vercode);");
db.execSQL("create index apk_id on " + DB.TABLE_APK + " (id);");
}
private static boolean columnExists(SQLiteDatabase db,
String table, String column) {
return (db.rawQuery( "select * from " + table + " limit 0,1", null )
.getColumnIndex(column) != -1);
}
}

View File

@ -0,0 +1,142 @@
package org.fdroid.fdroid.net;
import java.io.*;
import java.net.*;
import android.content.*;
import org.fdroid.fdroid.*;
public class Downloader {
private static final String HEADER_IF_NONE_MATCH = "If-None-Match";
private static final String HEADER_FIELD_ETAG = "ETag";
private URL sourceUrl;
private OutputStream outputStream;
private ProgressListener progressListener = null;
private ProgressListener.Event progressEvent = null;
private String eTag = null;
private final File outputFile;
private HttpURLConnection connection;
private int statusCode = -1;
// The context is required for opening the file to write to.
public Downloader(String source, String destFile, Context ctx)
throws FileNotFoundException, MalformedURLException {
sourceUrl = new URL(source);
outputStream = ctx.openFileOutput(destFile, Context.MODE_PRIVATE);
outputFile = new File(ctx.getFilesDir() + File.separator + destFile);
}
/**
* Downloads to a temporary file, which *you must delete yourself when
* you are done*.
* @see org.fdroid.fdroid.net.Downloader#getFile()
*/
public Downloader(String source, Context ctx) throws IOException {
// http://developer.android.com/guide/topics/data/data-storage.html#InternalCache
outputFile = File.createTempFile("dl-", "", ctx.getCacheDir());
outputStream = new FileOutputStream(outputFile);
sourceUrl = new URL(source);
}
public Downloader(String source, OutputStream output)
throws MalformedURLException {
sourceUrl = new URL(source);
outputStream = output;
outputFile = null;
}
public void setProgressListener(ProgressListener progressListener,
ProgressListener.Event progressEvent) {
this.progressListener = progressListener;
this.progressEvent = progressEvent;
}
/**
* Only available if you passed a context object into the constructor
* (rather than an outputStream, which may or may not be associated with
* a file).
*/
public File getFile() {
return outputFile;
}
/**
* Only available after downloading a file.
*/
public int getStatusCode() {
return statusCode;
}
/**
* If you ask for the eTag before calling download(), you will get the
* same one you passed in (if any). If you call it after download(), you
* will get the new eTag from the server, or null if there was none.
*/
public String getETag() {
return eTag;
}
/**
* If this eTag matches that returned by the server, then no download will
* take place, and a status code of 304 will be returned by download().
*/
public void setETag(String eTag) {
this.eTag = eTag;
}
// Get a remote file. Returns the HTTP response code.
// If 'etag' is not null, it's passed to the server as an If-None-Match
// header, in which case expect a 304 response if nothing changed.
// In the event of a 200 response ONLY, 'retag' (which should be passed
// empty) may contain an etag value for the response, or it may be left
// empty if none was available.
public int download() throws IOException {
connection = (HttpURLConnection)sourceUrl.openConnection();
setupCacheCheck();
statusCode = connection.getResponseCode();
if (statusCode == 200) {
setupProgressListener();
InputStream input = null;
try {
input = connection.getInputStream();
Utils.copy(input, outputStream,
progressListener, progressEvent);
} finally {
Utils.closeQuietly(outputStream);
Utils.closeQuietly(input);
}
updateCacheCheck();
}
return statusCode;
}
protected void setupCacheCheck() {
if (eTag != null) {
connection.setRequestProperty(HEADER_IF_NONE_MATCH, eTag);
}
}
protected void updateCacheCheck() {
eTag = connection.getHeaderField(HEADER_FIELD_ETAG);
}
protected void setupProgressListener() {
if (progressListener != null && progressEvent != null) {
// Testing in the emulator for me, showed that figuring out the
// filesize took about 1 to 1.5 seconds.
// To put this in context, downloading a repo of:
// - 400k takes ~6 seconds
// - 5k takes ~3 seconds
// on my connection. I think the 1/1.5 seconds is worth it,
// because as the repo grows, the tradeoff will
// become more worth it.
progressEvent.total = connection.getContentLength();
}
}
public boolean hasChanged() {
return this.statusCode == 200;
}
}

View File

@ -0,0 +1,280 @@
package org.fdroid.fdroid.updater;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import org.fdroid.fdroid.DB;
import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.RepoXMLHandler;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.net.Downloader;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import javax.net.ssl.SSLHandshakeException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
abstract public class RepoUpdater {
public static final int PROGRESS_TYPE_DOWNLOAD = 1;
public static final int PROGRESS_TYPE_PROCESS_XML = 2;
public static final String PROGRESS_DATA_REPO = "repo";
public static RepoUpdater createUpdaterFor(Context ctx, DB.Repo repo) {
if (repo.pubkey != null) {
return new SignedRepoUpdater(ctx, repo);
} else {
return new UnsignedRepoUpdater(ctx, repo);
}
}
protected final Context context;
protected final DB.Repo repo;
protected final List<DB.App> apps = new ArrayList<DB.App>();
protected boolean hasChanged = false;
protected ProgressListener progressListener;
public RepoUpdater(Context ctx, DB.Repo repo) {
this.context = ctx;
this.repo = repo;
}
public void setProgressListener(ProgressListener progressListener) {
this.progressListener = progressListener;
}
public boolean hasChanged() {
return hasChanged;
}
public List<DB.App> getApps() {
return apps;
}
public boolean isInteractive() {
return progressListener != null;
}
/**
* For example, you may want to unzip a jar file to get the index inside,
* or if the file is not compressed, you can just return a reference to
* the downloaded file.
*
* @throws UpdateException All error states will come from here.
*/
protected abstract File getIndexFromFile(File downloadedFile) throws
UpdateException;
protected abstract String getIndexAddress();
protected Downloader downloadIndex() throws UpdateException {
Bundle progressData = createProgressData(repo.address);
Downloader downloader = null;
try {
downloader = new Downloader(getIndexAddress(), context);
downloader.setETag(repo.lastetag);
if (isInteractive()) {
ProgressListener.Event event =
new ProgressListener.Event(
RepoUpdater.PROGRESS_TYPE_DOWNLOAD, progressData);
downloader.setProgressListener(progressListener, event);
}
int status = downloader.download();
if (status == 304) {
// The index is unchanged since we last read it. We just mark
// everything that came from this repo as being updated.
Log.d("FDroid", "Repo index for " + repo.address
+ " is up to date (by etag)");
} else if (status == 200) {
// Nothing needed to be done here...
} else {
// Is there any code other than 200 which still returns
// content? Just in case, lets try to clean up.
if (downloader.getFile() != null) {
downloader.getFile().delete();
}
throw new UpdateException(
repo,
"Failed to update repo " + repo.address +
" - HTTP response " + status);
}
} catch (SSLHandshakeException e) {
throw new UpdateException(
repo,
"A problem occurred while establishing an SSL " +
"connection. If this problem persists, AND you have a " +
"very old device, you could try using http instead of " +
"https for the repo URL.",
e );
} catch (IOException e) {
if (downloader != null && downloader.getFile() != null) {
downloader.getFile().delete();
}
throw new UpdateException(
repo,
"Error getting index file from " + repo.address,
e);
}
return downloader;
}
public static Bundle createProgressData(String repoAddress) {
Bundle data = new Bundle();
data.putString(PROGRESS_DATA_REPO, repoAddress);
return data;
}
private int estimateAppCount(File indexFile) {
int count = -1;
try {
// A bit of a hack, this might return false positives if an apps description
// or some other part of the XML file contains this, but it is a pretty good
// estimate and makes the progress counter more informative.
// As with asking the server about the size of the index before downloading,
// this also has a time tradeoff. It takes about three seconds to iterate
// through the file and count 600 apps on a slow emulator (v17), but if it is
// taking two minutes to update, the three second wait may be worth it.
final String APPLICATION = "<application";
count = Utils.countSubstringOccurrence(indexFile, APPLICATION);
} catch (IOException e) {
// Do nothing. Leave count at default -1 value.
}
return count;
}
public void update() throws UpdateException {
File downloadedFile = null;
File indexFile = null;
try {
Downloader downloader = downloadIndex();
hasChanged = downloader.hasChanged();
if (hasChanged) {
downloadedFile = downloader.getFile();
repo.lastetag = downloader.getETag();
indexFile = getIndexFromFile(downloadedFile);
// Process the index...
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
XMLReader reader = parser.getXMLReader();
RepoXMLHandler handler = new RepoXMLHandler(repo, apps, progressListener);
if (isInteractive()) {
// Only bother spending the time to count the expected apps
// if we can show that to the user...
handler.setTotalAppCount(estimateAppCount(indexFile));
}
reader.setContentHandler(handler);
InputSource is = new InputSource(
new BufferedReader(new FileReader(indexFile)));
reader.parse(is);
updateRepo(handler);
}
} catch (SAXException e) {
throw new UpdateException(
repo, "Error parsing index for repo " + repo.address, e);
} catch (FileNotFoundException e) {
throw new UpdateException(
repo, "Error parsing index for repo " + repo.address, e);
} catch (ParserConfigurationException e) {
throw new UpdateException(
repo, "Error parsing index for repo " + repo.address, e);
} catch (IOException e) {
throw new UpdateException(
repo, "Error parsing index for repo " + repo.address, e);
} finally {
if (downloadedFile != null &&
downloadedFile != indexFile && downloadedFile.exists()) {
downloadedFile.delete();
}
if (indexFile != null && indexFile.exists()) {
indexFile.delete();
}
}
}
private void updateRepo(RepoXMLHandler handler) {
boolean repoChanged = false;
// We read an unsigned index, but that indicates that
// a signed version is now available...
if (handler.getPubKey() != null && repo.pubkey == null) {
// TODO: Spend the time *now* going to get the etag of the signed
// repo, so that we can prevent downloading it next time. Otherwise
// next time we update, we have to download the signed index
// in its entirety, regardless of if it contains the same
// information as the unsigned one does not...
Log.d("FDroid",
"Public key found - switching to signed repo for future updates");
repo.pubkey = handler.getPubKey();
repoChanged = true;
}
if (handler.getVersion() != -1 && handler.getVersion() != repo.version) {
Log.d("FDroid", "Repo specified a new version: from "
+ repo.version + " to " + handler.getVersion());
repo.version = handler.getVersion();
repoChanged = true;
}
if (handler.getMaxAge() != -1 && handler.getMaxAge() != repo.maxage) {
Log.d("FDroid",
"Repo specified a new maximum age - updated");
repo.maxage = handler.getMaxAge();
repoChanged = true;
}
if (handler.getDescription() != null && !handler.getDescription().equals(repo.description)) {
repo.description = handler.getDescription();
repoChanged = true;
}
if (handler.getName() != null && !handler.getName().equals(repo.name)) {
repo.name = handler.getName();
repoChanged = true;
}
if (repoChanged) {
try {
DB db = DB.getDB();
db.updateRepoByAddress(repo);
} finally {
DB.releaseDB();
}
}
}
public static class UpdateException extends Exception {
public final DB.Repo repo;
public UpdateException(DB.Repo repo, String message) {
super(message);
this.repo = repo;
}
public UpdateException(DB.Repo repo, String message, Exception cause) {
super(message, cause);
this.repo = repo;
}
}
}

View File

@ -0,0 +1,110 @@
package org.fdroid.fdroid.updater;
import android.content.Context;
import android.util.Log;
import org.fdroid.fdroid.DB;
import org.fdroid.fdroid.Hasher;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.net.Downloader;
import java.io.*;
import java.security.cert.Certificate;
import java.util.Date;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
public class SignedRepoUpdater extends RepoUpdater {
public SignedRepoUpdater(Context ctx, DB.Repo repo) {
super(ctx, repo);
}
private boolean verifyCerts(JarEntry item) throws UpdateException {
Certificate[] certs = item.getCertificates();
if (certs == null || certs.length == 0) {
throw new UpdateException(repo, "No signature found in index");
}
Log.d("FDroid", "Index has " + certs.length + " signature(s)");
boolean match = false;
for (Certificate cert : certs) {
String certdata = Hasher.hex(cert);
if (repo.pubkey.equals(certdata)) {
match = true;
break;
}
}
return match;
}
protected File extractIndexFromJar(File indexJar) throws UpdateException {
File indexFile = null;
JarFile jarFile = null;
try {
jarFile = new JarFile(indexJar, true);
JarEntry indexEntry = (JarEntry)jarFile.getEntry("index.xml");
indexFile = File.createTempFile("index-", ".xml", context.getFilesDir());
InputStream input = null;
OutputStream output = null;
try {
input = jarFile.getInputStream(indexEntry);
output = new FileOutputStream(indexFile);
Utils.copy(input, output);
} finally {
Utils.closeQuietly(output);
Utils.closeQuietly(input);
}
// Can only read certificates from jar after it has been read
// completely, so we put it after the copy above...
if (!verifyCerts(indexEntry)) {
indexFile.delete();
throw new UpdateException(repo, "Index signature mismatch");
}
} catch (IOException e) {
if (indexFile != null) {
indexFile.delete();
}
throw new UpdateException(
repo, "Error opening signed index", e);
} finally {
if (jarFile != null) {
try {
jarFile.close();
} catch (IOException ioe) {
// ignore
}
}
}
return indexFile;
}
protected String getIndexAddress() {
return repo.address + "/index.jar?client_version=" + context.getString(R.string.version_name);
}
/**
* As this is a signed repo - we download the jar file,
* check the signature, and extract the index file
*/
@Override
protected File getIndexFromFile(File downloadedFile) throws
UpdateException {
Date updateTime = new Date(System.currentTimeMillis());
Log.d("FDroid", "Getting signed index from " + repo.address + " at " +
Utils.LOG_DATE_FORMAT.format(updateTime));
File indexJar = downloadedFile;
File indexXml = null;
// Don't worry about checking the status code for 200. If it was a
// successful download, then we will have a file ready to use:
if (indexJar != null && indexJar.exists()) {
indexXml = extractIndexFromJar(indexJar);
}
return indexXml;
}
}

View File

@ -0,0 +1,26 @@
package org.fdroid.fdroid.updater;
import android.content.Context;
import android.util.Log;
import org.fdroid.fdroid.DB;
import org.fdroid.fdroid.net.Downloader;
import java.io.File;
public class UnsignedRepoUpdater extends RepoUpdater {
public UnsignedRepoUpdater(Context ctx, DB.Repo repo) {
super(ctx, repo);
}
@Override
protected File getIndexFromFile(File file) throws UpdateException {
Log.d("FDroid", "Getting unsigned index from " + getIndexAddress());
return file;
}
@Override
protected String getIndexAddress() {
return repo.address + "/index.xml";
}
}

View File

@ -79,7 +79,7 @@ abstract public class AppListAdapter extends BaseAdapter {
return position;
}
static class ViewHolder {
private static class ViewHolder {
TextView name;
TextView summary;
TextView status;

View File

@ -0,0 +1,117 @@
package org.fdroid.fdroid.views;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;
import org.fdroid.fdroid.DB;
import org.fdroid.fdroid.ManageRepo;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.compat.SwitchCompat;
import java.util.List;
public class RepoAdapter extends BaseAdapter {
private List<DB.Repo> repositories;
private final ManageRepo activity;
public RepoAdapter(ManageRepo activity) {
this.activity = activity;
refresh();
}
public void refresh() {
try {
DB db = DB.getDB();
repositories = db.getRepos();
} finally {
DB.releaseDB();
}
notifyDataSetChanged();
}
public boolean hasStableIds() {
return true;
}
@Override
public int getCount() {
return repositories.size();
}
@Override
public Object getItem(int position) {
return repositories.get(position);
}
@Override
public long getItemId(int position) {
return getItem(position).hashCode();
}
private static final int SWITCH_ID = 10000;
@Override
public View getView(int position, View view, ViewGroup parent) {
final DB.Repo repository = repositories.get(position);
CompoundButton switchView;
if (view == null) {
view = activity.getLayoutInflater().inflate(R.layout.repo_item,null);
switchView = addSwitchToView(view);
} else {
switchView = (CompoundButton)view.findViewById(SWITCH_ID);
// Remove old listener (because we are reusing this view, we don't want
// to invoke the listener for the last repo to use it - particularly
// because we are potentially about to change the checked status
// which would in turn invoke this listener....
switchView.setOnCheckedChangeListener(null);
}
switchView.setChecked(repository.inuse);
// Add this listener *after* setting the checked status, so we don't
// invoke the listener while setting up the view...
switchView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
activity.setRepoEnabled(repository, isChecked);
}
});
TextView nameView = (TextView)view.findViewById(R.id.repo_name);
nameView.setText(repository.getName());
RelativeLayout.LayoutParams nameViewLayout =
(RelativeLayout.LayoutParams)nameView.getLayoutParams();
nameViewLayout.addRule(RelativeLayout.LEFT_OF, switchView.getId());
// If we set the signed view to GONE instead of INVISIBLE, then the
// height of each list item varies.
View signedView = view.findViewById(R.id.repo_unsigned);
if (repository.isSigned()) {
signedView.setVisibility(View.INVISIBLE);
} else {
signedView.setVisibility(View.VISIBLE);
}
return view;
}
private CompoundButton addSwitchToView(View parent) {
SwitchCompat switchBuilder = SwitchCompat.create(activity);
CompoundButton switchView = switchBuilder.createSwitch();
switchView.setId(SWITCH_ID);
RelativeLayout.LayoutParams layout = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT
);
layout.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
layout.addRule(RelativeLayout.CENTER_VERTICAL);
switchView.setLayoutParams(layout);
((RelativeLayout)parent).addView(switchView);
return switchView;
}
}

View File

@ -0,0 +1,69 @@
package org.fdroid.fdroid.views;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import org.fdroid.fdroid.DB;
import org.fdroid.fdroid.compat.ActionBarCompat;
import org.fdroid.fdroid.views.fragments.RepoDetailsFragment;
public class RepoDetailsActivity extends FragmentActivity implements RepoDetailsFragment.OnRepoChangeListener {
public static final String ACTION_IS_DELETED = "isDeleted";
public static final String ACTION_IS_ENABLED = "isEnabled";
public static final String ACTION_IS_DISABLED = "isDisabled";
public static final String ACTION_IS_CHANGED = "isChanged";
public static final String DATA_REPO_ID = "repoId";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
RepoDetailsFragment fragment = new RepoDetailsFragment();
fragment.setRepoChangeListener(this);
fragment.setArguments(getIntent().getExtras());
getSupportFragmentManager()
.beginTransaction()
.add(android.R.id.content, fragment)
.commit();
}
ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true);
}
private void finishWithAction(String actionName) {
Intent data = new Intent();
data.putExtra(actionName, true);
data.putExtra(DATA_REPO_ID, getIntent().getIntExtra(RepoDetailsFragment.ARG_REPO_ID, -1));
setResult(RESULT_OK, data);
finish();
}
@Override
public void onDeleteRepo(DB.Repo repo) {
finishWithAction(ACTION_IS_DELETED);
}
@Override
public void onRepoDetailsChanged(DB.Repo repo) {
// Do nothing...
}
@Override
public void onEnableRepo(DB.Repo repo) {
finishWithAction(ACTION_IS_ENABLED);
}
@Override
public void onDisableRepo(DB.Repo repo) {
finishWithAction(ACTION_IS_DISABLED);
}
@Override
public void onUpdatePerformed(DB.Repo repo) {
// do nothing - the actual update is done by the repo fragment...
}
}

View File

@ -0,0 +1,344 @@
package org.fdroid.fdroid.views.fragments;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.view.MenuItemCompat;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.*;
import android.widget.*;
import org.fdroid.fdroid.*;
import java.util.List;
public class RepoDetailsFragment extends Fragment {
public static final String ARG_REPO_ID = "repo_id";
/**
* If the repo has been updated at least once, then we will show
* all of this info, otherwise they will be hidden.
*/
private static final int[] SHOW_IF_EXISTS = {
R.id.label_repo_name,
R.id.text_repo_name,
R.id.label_description,
R.id.text_description,
R.id.label_num_apps,
R.id.text_num_apps,
R.id.label_last_update,
R.id.text_last_update,
R.id.label_signature,
R.id.text_signature,
R.id.text_signature_description
};
/**
* If the repo has <em>not</em> been updated yet, then we only show
* these, otherwise they are hidden.
*/
private static final int[] HIDE_IF_EXISTS = {
R.id.text_not_yet_updated,
R.id.btn_update
};
private static final int DELETE = 0;
private static final int UPDATE = 1;
public void setRepoChangeListener(OnRepoChangeListener listener) {
repoChangeListener = listener;
}
private OnRepoChangeListener repoChangeListener;
public static interface OnRepoChangeListener {
/**
* This fragment is responsible for getting confirmation from the
* user, so you should presume that the user has already consented
* and confirmed to the deletion.
*/
public void onDeleteRepo(DB.Repo repo);
public void onRepoDetailsChanged(DB.Repo repo);
public void onEnableRepo(DB.Repo repo);
public void onDisableRepo(DB.Repo repo);
public void onUpdatePerformed(DB.Repo repo);
}
// TODO: Currently initialised in onCreateView. Not sure if that is the
// best way to go about this...
private DB.Repo repo;
public void onAttach(Activity activity) {
super.onAttach(activity);
}
/**
* After, for example, a repo update, the details will have changed in the
* database. However, or local reference to the DB.Repo object will not
* have been updated. The safest way to deal with this is to reload the
* repo object directly from the database.
*/
private void reloadRepoDetails() {
try {
DB db = DB.getDB();
repo = db.getRepo(repo.id);
} finally {
DB.releaseDB();
}
}
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
int repoId = getArguments().getInt(ARG_REPO_ID);
DB db = DB.getDB();
repo = db.getRepo(repoId);
DB.releaseDB();
if (repo == null) {
Log.e("FDroid", "Error showing details for repo '" + repoId + "'");
return new LinearLayout(container.getContext());
}
ViewGroup repoView = (ViewGroup)inflater.inflate(R.layout.repodetails, null);
updateView(repoView);
// Setup listeners here, rather than in updateView(...),
// because otherwise we will end up adding multiple listeners with
// subsequent calls to updateView().
EditText inputUrl = (EditText)repoView.findViewById(R.id.input_repo_url);
inputUrl.addTextChangedListener(new UrlWatcher());
Button update = (Button)repoView.findViewById(R.id.btn_update);
update.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
performUpdate();
}
});
return repoView;
}
/**
* Populates relevant views with properties from the current repository.
* Decides which views to show and hide depending on the state of the
* repository.
*/
private void updateView(ViewGroup repoView) {
EditText inputUrl = (EditText)repoView.findViewById(R.id.input_repo_url);
inputUrl.setText(repo.address);
if (repo.hasBeenUpdated()) {
updateViewForExistingRepo(repoView);
} else {
updateViewForNewRepo(repoView);
}
}
/**
* Help function to make switching between two view states easier.
* Perhaps there is a better way to do this. I recall that using Adobe
* Flex, there was a thing called "ViewStates" for exactly this. Wonder if
* that exists in Android?
*/
private static void setMultipleViewVisibility(ViewGroup parent,
int[] viewIds,
int visibility) {
for (int viewId : viewIds) {
parent.findViewById(viewId).setVisibility(visibility);
}
}
private void updateViewForNewRepo(ViewGroup repoView) {
setMultipleViewVisibility(repoView, HIDE_IF_EXISTS, View.VISIBLE);
setMultipleViewVisibility(repoView, SHOW_IF_EXISTS, View.GONE);
}
private void updateViewForExistingRepo(ViewGroup repoView) {
setMultipleViewVisibility(repoView, SHOW_IF_EXISTS, View.VISIBLE);
setMultipleViewVisibility(repoView, HIDE_IF_EXISTS, View.GONE);
TextView name = (TextView)repoView.findViewById(R.id.text_repo_name);
TextView numApps = (TextView)repoView.findViewById(R.id.text_num_apps);
TextView lastUpdated = (TextView)repoView.findViewById(R.id.text_last_update);
name.setText(repo.getName());
numApps.setText(Integer.toString(repo.getNumberOfApps()));
setupDescription(repoView, repo);
setupSignature(repoView, repo);
// Repos that existed before this feature was supported will have an
// "Unknown" last update until next time they update...
String lastUpdate = repo.lastUpdated != null
? repo.lastUpdated.toString() : getString(R.string.unknown);
lastUpdated.setText(lastUpdate);
}
private void setupDescription(ViewGroup parent, DB.Repo repo) {
TextView descriptionLabel = (TextView)parent.findViewById(R.id.label_description);
TextView description = (TextView)parent.findViewById(R.id.text_description);
if (repo.description == null || repo.description.length() == 0) {
descriptionLabel.setVisibility(View.GONE);
description.setVisibility(View.GONE);
} else {
descriptionLabel.setVisibility(View.VISIBLE);
description.setVisibility(View.VISIBLE);
}
String descriptionText = repo.description == null
? "" : repo.description.replaceAll("\n", " ");
description.setText(descriptionText);
}
/**
* When an update is performed, notify the listener so that the repo
* list can be updated. We will perform the update ourselves though.
*/
private void performUpdate() {
repo.enable((FDroidApp)getActivity().getApplication());
UpdateService.updateNow(getActivity()).setListener(new ProgressListener() {
@Override
public void onProgress(Event event) {
if (event.type == UpdateService.STATUS_COMPLETE_AND_SAME ||
event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES) {
reloadRepoDetails();
updateView((ViewGroup)getView());
}
}
});
if (repoChangeListener != null) {
repoChangeListener.onUpdatePerformed(repo);
}
}
/**
* When the URL is changed, notify the repoChangeListener.
*/
class UrlWatcher implements TextWatcher {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void afterTextChanged(Editable s) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (!repo.address.equals(s.toString())) {
repo.address = s.toString();
try {
DB db = DB.getDB();
db.updateRepo(repo);
} finally {
DB.releaseDB();
}
if (repoChangeListener != null) {
repoChangeListener.onRepoDetailsChanged(repo);
}
}
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
menu.clear();
MenuItem update = menu.add(Menu.NONE, UPDATE, 0, R.string.repo_update);
update.setIcon(R.drawable.ic_menu_refresh);
MenuItemCompat.setShowAsAction(update,
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT );
MenuItem delete = menu.add(Menu.NONE, DELETE, 0, R.string.delete);
delete.setIcon(android.R.drawable.ic_menu_delete);
MenuItemCompat.setShowAsAction(delete,
MenuItemCompat.SHOW_AS_ACTION_IF_ROOM |
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == DELETE) {
promptForDelete();
return true;
} else if (item.getItemId() == UPDATE) {
performUpdate();
return true;
}
return false;
}
private void promptForDelete() {
new AlertDialog.Builder(getActivity())
.setTitle(R.string.repo_confirm_delete_title)
.setIcon(android.R.drawable.ic_menu_delete)
.setMessage(R.string.repo_confirm_delete_body)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (repoChangeListener != null) {
DB.Repo repo = RepoDetailsFragment.this.repo;
repoChangeListener.onDeleteRepo(repo);
}
}
}).setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Do nothing...
}
}
).show();
}
private void setupSignature(ViewGroup parent, DB.Repo repo) {
TextView signatureView = (TextView)parent.findViewById(R.id.text_signature);
TextView signatureDescView = (TextView)parent.findViewById(R.id.text_signature_description);
String signature;
int signatureColour;
if (repo.pubkey != null && repo.pubkey.length() > 0) {
signature = Utils.formatFingerprint(repo.pubkey);
signatureColour = getResources().getColor(R.color.signed);
signatureDescView.setVisibility(View.GONE);
} else {
signature = getResources().getString(R.string.unsigned);
signatureColour = getResources().getColor(R.color.unsigned);
signatureDescView.setVisibility(View.VISIBLE);
signatureDescView.setText(getResources().getString(R.string.unsigned_description));
}
signatureView.setText(signature);
signatureView.setTextColor(signatureColour);
}
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
}

View File

@ -0,0 +1,8 @@
/*___Generated_by_IDEA___*/
/** Automatically generated file. DO NOT MODIFY */
package org.fdroid.fdroid.tests;
public final class BuildConfig {
public final static boolean DEBUG = true;
}

View File

@ -0,0 +1,7 @@
/*___Generated_by_IDEA___*/
package org.fdroid.fdroid.tests;
/* This stub is for using by IDE only. It is NOT the Manifest class actually packed into APK */
public final class Manifest {
}

View File

@ -0,0 +1,7 @@
/*___Generated_by_IDEA___*/
package org.fdroid.fdroid.tests;
/* This stub is for using by IDE only. It is NOT the R class actually packed into APK */
public final class R {
}

10
tests/local.properties Normal file
View File

@ -0,0 +1,10 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
# location of the SDK. This is only used by Ant
# For customization when using a Version Control System, please read the
# header note.
sdk.dir=/opt/android-sdk

14
tests/project.properties Normal file
View File

@ -0,0 +1,14 @@
# 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 edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-4

View File

@ -1,4 +1,4 @@
#!/bin/sh -x
#!/bin/bash -x
## For changing the package name so that your app can be installed alongside
## F-Droid. This script also changes the app name, but DOESN'T change the
@ -13,7 +13,6 @@ FDROID_NAME=${2:-Your FDroid}
FDROID_PATH=${FDROID_PACKAGE//./\/}
mkdir -p "src/${FDROID_PATH}"
perl -pi -e"s|org/fdroid/fdroid/R.java|${FDROID_PATH}/R.java|g" build.xml
find src/org/fdroid/ res/ -type f |xargs -n 1 perl -pi -e"s/org.fdroid.fdroid(?=\W)/${FDROID_PACKAGE}/g"
perl -pi -e"s|org.fdroid.fdroid|${FDROID_PACKAGE}|g" AndroidManifest.xml
@ -21,6 +20,5 @@ perl -pi -e"s|org.fdroid.fdroid|${FDROID_PACKAGE}|g" AndroidManifest.xml
mv src/org/fdroid/fdroid/* src/${FDROID_PATH}/
rm -rf src/org/fdroid/fdroid/
perl -pi -e"s|FDroid|${FDROID_NAME}|g" build.xml
find res/ -type f -print0 | xargs -0 sed -i "s/F-Droid/${FDROID_NAME}/g"

5
tools/fix-ellipsis.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash -x
# Fix TypographyEllipsis programmatically
find res -name strings.xml -type f | xargs -n 1 sed -i 's/\.\.\./…/g'