Merge branch 'development'
This commit is contained in:
commit
7c472c8e18
10
.gitmodules
vendored
10
.gitmodules
vendored
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
6
ant-prepare.sh
Executable 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
1
ant.properties
Normal file
@ -0,0 +1 @@
|
||||
java.encoding=UTF-8
|
@ -38,4 +38,8 @@ android {
|
||||
proguardFile getDefaultProguardFile('proguard-android.txt')
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Compile) {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
}
|
||||
|
1
extern/AndroidPinning
vendored
Submodule
1
extern/AndroidPinning
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 526654e1b9997b32e513d58d9094d4c1102a6cb3
|
1
extern/MemorizingTrustManager
vendored
Submodule
1
extern/MemorizingTrustManager
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 49452f67a760dfef77ddaa7e0b7d88c713c4a195
|
8
lint.xml
Normal file
8
lint.xml
Normal 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>
|
@ -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 |
@ -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
38
res/layout/repo_item.xml
Normal 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
131
res/layout/repodetails.xml
Normal 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>
|
@ -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>
|
||||
<!--
|
||||
|
15
res/menu/manage_repo_context.xml
Normal file
15
res/menu/manage_repo_context.xml
Normal 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>
|
@ -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">Списъкът на хранилищата е променен.
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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é.
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -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">사용된 저장소의 목록이 변경되었습니다.
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
5
res/values/colors.xml
Normal 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
6
res/values/dimens.xml
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
|
56
src/org/fdroid/fdroid/FDroidCertPins.java
Normal file
56
src/org/fdroid/fdroid/FDroidCertPins.java
Normal 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()]);
|
||||
}
|
||||
}
|
@ -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++) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
|
55
src/org/fdroid/fdroid/compat/ClipboardCompat.java
Normal file
55
src/org/fdroid/fdroid/compat/ClipboardCompat.java
Normal 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;
|
||||
}
|
||||
}
|
30
src/org/fdroid/fdroid/compat/SupportedArchitectures.java
Normal file
30
src/org/fdroid/fdroid/compat/SupportedArchitectures.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
52
src/org/fdroid/fdroid/compat/SwitchCompat.java
Normal file
52
src/org/fdroid/fdroid/compat/SwitchCompat.java
Normal 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);
|
||||
}
|
||||
}
|
251
src/org/fdroid/fdroid/data/DBHelper.java
Normal file
251
src/org/fdroid/fdroid/data/DBHelper.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
142
src/org/fdroid/fdroid/net/Downloader.java
Normal file
142
src/org/fdroid/fdroid/net/Downloader.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
280
src/org/fdroid/fdroid/updater/RepoUpdater.java
Normal file
280
src/org/fdroid/fdroid/updater/RepoUpdater.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
110
src/org/fdroid/fdroid/updater/SignedRepoUpdater.java
Normal file
110
src/org/fdroid/fdroid/updater/SignedRepoUpdater.java
Normal 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;
|
||||
}
|
||||
}
|
26
src/org/fdroid/fdroid/updater/UnsignedRepoUpdater.java
Normal file
26
src/org/fdroid/fdroid/updater/UnsignedRepoUpdater.java
Normal 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";
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
117
src/org/fdroid/fdroid/views/RepoAdapter.java
Normal file
117
src/org/fdroid/fdroid/views/RepoAdapter.java
Normal 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;
|
||||
}
|
||||
}
|
69
src/org/fdroid/fdroid/views/RepoDetailsActivity.java
Normal file
69
src/org/fdroid/fdroid/views/RepoDetailsActivity.java
Normal 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...
|
||||
}
|
||||
|
||||
}
|
344
src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java
Normal file
344
src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
8
tests/gen/org/fdroid/fdroid/tests/BuildConfig.java
Normal file
8
tests/gen/org/fdroid/fdroid/tests/BuildConfig.java
Normal 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;
|
||||
}
|
7
tests/gen/org/fdroid/fdroid/tests/Manifest.java
Normal file
7
tests/gen/org/fdroid/fdroid/tests/Manifest.java
Normal 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 {
|
||||
}
|
7
tests/gen/org/fdroid/fdroid/tests/R.java
Normal file
7
tests/gen/org/fdroid/fdroid/tests/R.java
Normal 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
10
tests/local.properties
Normal 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
14
tests/project.properties
Normal 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
|
@ -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
5
tools/fix-ellipsis.sh
Executable 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'
|
Loading…
x
Reference in New Issue
Block a user