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"]
|
[submodule "extern/Universal-Image-Loader"]
|
||||||
path = extern/Universal-Image-Loader
|
path = extern/Universal-Image-Loader
|
||||||
url = https://github.com/nostra13/Android-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>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".views.RepoDetailsActivity"
|
||||||
|
android:label="@string/menu_manage" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".AppDetails"
|
android:name=".AppDetails"
|
||||||
android:label="@string/app_details"
|
android:label="@string/app_details"
|
||||||
@ -196,6 +200,9 @@
|
|||||||
android:name="android.app.searchable"
|
android:name="android.app.searchable"
|
||||||
android:resource="@xml/searchable" />
|
android:resource="@xml/searchable" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!--Used for SSL TOFU, supported by extern/MemorizingTrustManager lib -->
|
||||||
|
<activity android:name="de.duenndns.ssl.MemorizingActivity" />
|
||||||
|
|
||||||
<receiver android:name=".StartupReceiver" >
|
<receiver android:name=".StartupReceiver" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
@ -12,8 +12,7 @@ The only required tools are the Android SDK and Apache Ant.
|
|||||||
|
|
||||||
```
|
```
|
||||||
git submodule update --init
|
git submodule update --init
|
||||||
android update project -p .
|
./ant-prepare.sh # This runs 'android update' on the libs and the main project
|
||||||
android update project -p extern/Universal-Image-Loader/library
|
|
||||||
ant clean release
|
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')
|
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
|
target=android-19
|
||||||
|
|
||||||
android.library.reference.1=extern/Universal-Image-Loader/library
|
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"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical" >
|
android:orientation="vertical"
|
||||||
|
android:padding="6dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -15,7 +16,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="textUri"
|
android:inputType="textUri"
|
||||||
android:maxLines="2"
|
android:maxLines="2"
|
||||||
android:text="https://" />
|
android:text="@string/https" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/vw1"
|
android:id="@+id/vw1"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<ImageView android:id="@+id/img"
|
<ImageView android:id="@+id/img"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView android:id="@+id/uri"
|
<TextView android:id="@+id/uri"
|
||||||
android:textSize="21sp"
|
android:textSize="21sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="fill_parent"/>
|
android:layout_height="fill_parent"/>
|
||||||
|
|
||||||
<TextView android:id="@+id/fingerprint"
|
<TextView android:id="@+id/fingerprint"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:typeface="monospace"
|
android:typeface="monospace"
|
||||||
android:singleLine="false"
|
android:singleLine="false"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="fill_parent"/>
|
android:layout_height="fill_parent"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</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="many_updates_available">%d налични актуализации.</string>
|
||||||
<string name="fdroid_updates_available">Актуализации на F-Droid са налични</string>
|
<string name="fdroid_updates_available">Актуализации на F-Droid са налични</string>
|
||||||
<string name="process_wait_title">Моля изчакай</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="download_server">Взимане на приложението от</string>
|
||||||
<string name="repo_add_url">Адрес на хранилището</string>
|
<string name="repo_add_url">Адрес на хранилището</string>
|
||||||
<string name="repo_alrt">Списъкът на хранилищата е променен.
|
<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="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="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_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="download_server">S\'està obtenint l\'aplicació des de</string>
|
||||||
<string name="repo_add_url">Adreça del dipòsit</string>
|
<string name="repo_add_url">Adreça del dipòsit</string>
|
||||||
<string name="repo_alrt">La llista de dipòsits ha canviat.
|
<string name="repo_alrt">La llista de dipòsits ha canviat.
|
||||||
@ -98,7 +98,7 @@ La voleu actualitzar?</string>
|
|||||||
%1$s</string>
|
%1$s</string>
|
||||||
<string name="status_connecting_to_repo">S\'està connectant a
|
<string name="status_connecting_to_repo">S\'està connectant a
|
||||||
%1$s</string>
|
%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="no_permissions">No es fa servir cap permís.</string>
|
||||||
<string name="permissions_for_long">Permisos de la versió %s</string>
|
<string name="permissions_for_long">Permisos de la versió %s</string>
|
||||||
<string name="showPermissions">Mostra els permisos</string>
|
<string name="showPermissions">Mostra els permisos</string>
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
<string name="many_updates_available">%d διαθέσιμες ενημερώσεις.</string>
|
<string name="many_updates_available">%d διαθέσιμες ενημερώσεις.</string>
|
||||||
<string name="fdroid_updates_available">Διαθέσιμες ενημερώσεις για το F-Droid</string>
|
<string name="fdroid_updates_available">Διαθέσιμες ενημερώσεις για το F-Droid</string>
|
||||||
<string name="process_wait_title">Παρακαλώ περιμένετε</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="download_server">Λήψη εφαρμογών από</string>
|
||||||
<string name="repo_add_url">Διεύθυνση αποθετηρίου</string>
|
<string name="repo_add_url">Διεύθυνση αποθετηρίου</string>
|
||||||
<string name="repo_alrt">Η λίστα με τα χρησιμοποιούμενα αποθετήρια έχει αλλάξει.
|
<string name="repo_alrt">Η λίστα με τα χρησιμοποιούμενα αποθετήρια έχει αλλάξει.
|
||||||
@ -98,7 +98,7 @@
|
|||||||
%1$s</string>
|
%1$s</string>
|
||||||
<string name="status_connecting_to_repo">Σύνδεση με
|
<string name="status_connecting_to_repo">Σύνδεση με
|
||||||
%1$s</string>
|
%1$s</string>
|
||||||
<string name="status_checking_compatibility">Έλεγχος συμβατότητας εφαρμογών με τη συσκευή σας...</string>
|
<string name="status_checking_compatibility">Έλεγχος συμβατότητας εφαρμογών με τη συσκευή σας…</string>
|
||||||
<string name="no_permissions">Δεν χρησιμοποιείται καμία άδεια.</string>
|
<string name="no_permissions">Δεν χρησιμοποιείται καμία άδεια.</string>
|
||||||
<string name="permissions_for_long">Άδειες για την έκδοση %s</string>
|
<string name="permissions_for_long">Άδειες για την έκδοση %s</string>
|
||||||
<string name="showPermissions">Εμφάνιση αδειών</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="many_updates_available">%d actualizaciones disponibles.</string>
|
||||||
<string name="fdroid_updates_available">Actualizaciones de F-Droid 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_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="download_server">Obteniendo la aplicación de</string>
|
||||||
<string name="repo_add_url">Dirección del repositorio</string>
|
<string name="repo_add_url">Dirección del repositorio</string>
|
||||||
<string name="repo_alrt">La lista de repositorios usada ha cambiado.
|
<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>
|
%1$s</string>
|
||||||
<string name="status_connecting_to_repo">Conectando a
|
<string name="status_connecting_to_repo">Conectando a
|
||||||
%1$s</string>
|
%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="no_permissions">No se usan permisos.</string>
|
||||||
<string name="permissions_for_long">Permisos para la versión %s</string>
|
<string name="permissions_for_long">Permisos para la versión %s</string>
|
||||||
<string name="showPermissions">Mostrar permisos</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="many_updates_available">%d eguneraketa eskuragarri.</string>
|
||||||
<string name="fdroid_updates_available">F-Droid eguneraketak eskuragarri</string>
|
<string name="fdroid_updates_available">F-Droid eguneraketak eskuragarri</string>
|
||||||
<string name="process_wait_title">Mesedez itxaron</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="download_server">Aplikazioa eskuratzen hemendik</string>
|
||||||
<string name="repo_add_url">Biltegiaren helbidea</string>
|
<string name="repo_add_url">Biltegiaren helbidea</string>
|
||||||
<string name="repo_alrt">Erabilitako biltegien zerrenda aldatu egin da.
|
<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="category_recentlyupdated">Azkenaldian eguneratua</string>
|
||||||
<string name="status_connecting_to_repo">%1$s(e)ra
|
<string name="status_connecting_to_repo">%1$s(e)ra
|
||||||
konektatzen</string>
|
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="no_permissions">Ez da baimenik erabiltzen.</string>
|
||||||
<string name="permissions_for_long">%s bertsioarentzako baimenak</string>
|
<string name="permissions_for_long">%s bertsioarentzako baimenak</string>
|
||||||
<string name="showPermissions">Erakutsi baimenak</string>
|
<string name="showPermissions">Erakutsi baimenak</string>
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
<string name="many_updates_available">%d بهروزرسانی موجود است.</string>
|
<string name="many_updates_available">%d بهروزرسانی موجود است.</string>
|
||||||
<string name="fdroid_updates_available">بهروزرسانیهای F-Droid موجود هستند</string>
|
<string name="fdroid_updates_available">بهروزرسانیهای F-Droid موجود هستند</string>
|
||||||
<string name="process_wait_title">لطفاً صبر کنید</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="download_server">گرفتن برنامه از</string>
|
||||||
<string name="repo_add_url">نشانی مخزن</string>
|
<string name="repo_add_url">نشانی مخزن</string>
|
||||||
<string name="repo_alrt">فهرست مخزنها تغییر یافتهاست.
|
<string name="repo_alrt">فهرست مخزنها تغییر یافتهاست.
|
||||||
@ -98,7 +98,7 @@
|
|||||||
%1$s</string>
|
%1$s</string>
|
||||||
<string name="status_connecting_to_repo">اتصال به
|
<string name="status_connecting_to_repo">اتصال به
|
||||||
%1$s</string>
|
%1$s</string>
|
||||||
<string name="status_checking_compatibility">بررسی سازگاری برنامهها با دستگاه شما...</string>
|
<string name="status_checking_compatibility">بررسی سازگاری برنامهها با دستگاه شما…</string>
|
||||||
<string name="no_permissions">دسترسیای استفاده نشدهاست.</string>
|
<string name="no_permissions">دسترسیای استفاده نشدهاست.</string>
|
||||||
<string name="permissions_for_long">دسترسیهای نسخهٔ %s</string>
|
<string name="permissions_for_long">دسترسیهای نسخهٔ %s</string>
|
||||||
<string name="showPermissions">نمایش دسترسیها</string>
|
<string name="showPermissions">نمایش دسترسیها</string>
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
<string name="many_updates_available">%d päivitystä saatavilla.</string>
|
<string name="many_updates_available">%d päivitystä saatavilla.</string>
|
||||||
<string name="fdroid_updates_available">F-Droid: Päivityksiä saatavilla</string>
|
<string name="fdroid_updates_available">F-Droid: Päivityksiä saatavilla</string>
|
||||||
<string name="process_wait_title">Odota hetki</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="download_server">Haetaan sovellusta lähteestä</string>
|
||||||
<string name="repo_add_url">Sovelluslähteen osoite</string>
|
<string name="repo_add_url">Sovelluslähteen osoite</string>
|
||||||
<string name="repo_alrt">Lista käytetyistä sovelluslähteistä on muuttumut.
|
<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_all">Kaikki</string>
|
||||||
<string name="category_whatsnew">Uutta</string>
|
<string name="category_whatsnew">Uutta</string>
|
||||||
<string name="category_recentlyupdated">Viimeaikoina päivitetty</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>
|
<string name="theme">Teema</string>
|
||||||
</resources>
|
</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="many_updates_available">%d mises à jour sont disponibles.</string>
|
||||||
<string name="fdroid_updates_available">Des mises à jour F-Droid 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_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="download_server">Réception d\'application de</string>
|
||||||
<string name="repo_add_url">Adresse du dépôt</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é.
|
<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="many_updates_available">%d actualizacións dispoñíbeis</string>
|
||||||
<string name="fdroid_updates_available">Actualizacións de F-Droid 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_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="download_server">Obtención do aplicativo desde</string>
|
||||||
<string name="repo_add_url">Enderezo do repositorio</string>
|
<string name="repo_add_url">Enderezo do repositorio</string>
|
||||||
<string name="repo_alrt">Cambiou a lista de repositorios usados.
|
<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="many_updates_available">%d aggiornamenti disponibili.</string>
|
||||||
<string name="fdroid_updates_available">Aggiornamenti per F-Droid Disponibili</string>
|
<string name="fdroid_updates_available">Aggiornamenti per F-Droid Disponibili</string>
|
||||||
<string name="process_wait_title">Attendere prego</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="download_server">Scaricamento applicazione da</string>
|
||||||
<string name="repo_add_url">Indirizzo repository</string>
|
<string name="repo_add_url">Indirizzo repository</string>
|
||||||
<string name="repo_alrt">L\'elenco dei repository in uso è cambiato.
|
<string name="repo_alrt">L\'elenco dei repository in uso è cambiato.
|
||||||
@ -98,7 +98,7 @@ Vuoi aggiornarlo?</string>
|
|||||||
%1$s</string>
|
%1$s</string>
|
||||||
<string name="status_connecting_to_repo">Connessione a
|
<string name="status_connecting_to_repo">Connessione a
|
||||||
%1$s</string>
|
%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="no_permissions">Non viene usata alcuna autorizzazione.</string>
|
||||||
<string name="permissions_for_long">Autorizzazioni per la versione %s</string>
|
<string name="permissions_for_long">Autorizzazioni per la versione %s</string>
|
||||||
<string name="showPermissions">Mostra autorizzazioni</string>
|
<string name="showPermissions">Mostra autorizzazioni</string>
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
<string name="many_updates_available">%d개의 업데이트를 사용할 수 있습니다.</string>
|
<string name="many_updates_available">%d개의 업데이트를 사용할 수 있습니다.</string>
|
||||||
<string name="fdroid_updates_available">F-Droid 업데이트를 사용할 수 있습니다.</string>
|
<string name="fdroid_updates_available">F-Droid 업데이트를 사용할 수 있습니다.</string>
|
||||||
<string name="process_wait_title">잠시만 기다려주세요</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="download_server">에서 응용프로그램 가져오기</string>
|
||||||
<string name="repo_add_url">저장소 주소</string>
|
<string name="repo_add_url">저장소 주소</string>
|
||||||
<string name="repo_alrt">사용된 저장소의 목록이 변경되었습니다.
|
<string name="repo_alrt">사용된 저장소의 목록이 변경되었습니다.
|
||||||
|
@ -43,7 +43,7 @@ Lisensiert GNU GPLv3.</string>
|
|||||||
<string name="many_updates_available">%d oppdateringer tilgjengelig.</string>
|
<string name="many_updates_available">%d oppdateringer tilgjengelig.</string>
|
||||||
<string name="fdroid_updates_available">F-Droid: Oppdateringer tilgjengelig</string>
|
<string name="fdroid_updates_available">F-Droid: Oppdateringer tilgjengelig</string>
|
||||||
<string name="process_wait_title">Vennligst vent</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="download_server">Henter program fra</string>
|
||||||
<string name="repo_add_url">Registeradresse</string>
|
<string name="repo_add_url">Registeradresse</string>
|
||||||
<string name="repo_alrt">Listen over brukte register har endret seg. Vil du oppdatere dem?</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>
|
%1$s</string>
|
||||||
<string name="status_connecting_to_repo">Kobler til
|
<string name="status_connecting_to_repo">Kobler til
|
||||||
%1$s</string>
|
%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="no_permissions">Krever ingen tillatelser.</string>
|
||||||
<string name="permissions_for_long">Tillatelser for versjon %s</string>
|
<string name="permissions_for_long">Tillatelser for versjon %s</string>
|
||||||
<string name="showPermissions">Vis tillatelser</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="many_updates_available">%d vernieuwingen zijn beschikbaar</string>
|
||||||
<string name="fdroid_updates_available">F-Droid Vernieuwingen Beschikbaar</string>
|
<string name="fdroid_updates_available">F-Droid Vernieuwingen Beschikbaar</string>
|
||||||
<string name="process_wait_title">Even geduld aub</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="download_server">downloaden applicatie van</string>
|
||||||
<string name="repo_add_url">Bron-adres</string>
|
<string name="repo_add_url">Bron-adres</string>
|
||||||
<string name="repo_alrt">De lijst van gebruikte bronnen is veranderd.
|
<string name="repo_alrt">De lijst van gebruikte bronnen is veranderd.
|
||||||
@ -101,7 +101,7 @@ Wilt u ze vernieuwen?</string>
|
|||||||
%1$s</string>
|
%1$s</string>
|
||||||
<string name="status_connecting_to_repo">Connecteren naar
|
<string name="status_connecting_to_repo">Connecteren naar
|
||||||
%1$s</string>
|
%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="no_permissions">Geen permissies worden gebruikt</string>
|
||||||
<string name="permissions_for_long">Permissies voor versie %s</string>
|
<string name="permissions_for_long">Permissies voor versie %s</string>
|
||||||
<string name="showPermissions">Laat permissies zien</string>
|
<string name="showPermissions">Laat permissies zien</string>
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
<string name="tab_noninstalled">Dostępne</string>
|
<string name="tab_noninstalled">Dostępne</string>
|
||||||
<string name="tab_updates">Aktualizacje</string>
|
<string name="tab_updates">Aktualizacje</string>
|
||||||
<string name="process_wait_title">Proszę czekać</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="download_server">Pobieranie aplikacji z</string>
|
||||||
<string name="repo_add_url">Adres repozytorium</string>
|
<string name="repo_add_url">Adres repozytorium</string>
|
||||||
<string name="repo_alrt">Lista wykorzystywanych repozytoriów uległa zmianie.
|
<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="many_updates_available">%d atualizações disponíveis.</string>
|
||||||
<string name="fdroid_updates_available">Atualizações do F-Droid 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_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="download_server">Baixando aplicativo de</string>
|
||||||
<string name="repo_add_url">Endereço do repositório</string>
|
<string name="repo_add_url">Endereço do repositório</string>
|
||||||
<string name="repo_alrt">A lista de repositórios usados mudou.
|
<string name="repo_alrt">A lista de repositórios usados mudou.
|
||||||
@ -98,7 +98,7 @@ Você deseja atualizá-los?</string>
|
|||||||
%1$s</string>
|
%1$s</string>
|
||||||
<string name="status_connecting_to_repo">Conectando-se a
|
<string name="status_connecting_to_repo">Conectando-se a
|
||||||
%1$s</string>
|
%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="no_permissions">Nenhuma permissão utilizada.</string>
|
||||||
<string name="permissions_for_long">Permissões para a versão %s</string>
|
<string name="permissions_for_long">Permissões para a versão %s</string>
|
||||||
<string name="showPermissions">Mostrar permissões</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="repo_update_title">Actualizare depozit aplicatii</string>
|
||||||
<string name="tab_noninstalled">Disponibil</string>
|
<string name="tab_noninstalled">Disponibil</string>
|
||||||
<string name="tab_updates">Actualizare</string>
|
<string name="tab_updates">Actualizare</string>
|
||||||
<string name="process_wait_title">Asteptati ...</string>
|
<string name="process_wait_title">Asteptati …</string>
|
||||||
<string name="process_update_msg">Se actualizeaza lista ...</string>
|
<string name="process_update_msg">Se actualizeaza lista …</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
<string name="one_update_available">Доступно 1 обновление.</string>
|
<string name="one_update_available">Доступно 1 обновление.</string>
|
||||||
<string name="many_updates_available">Обновлений доступно - %d.</string>
|
<string name="many_updates_available">Обновлений доступно - %d.</string>
|
||||||
<string name="process_wait_title">Подождите</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="download_server">Взять приложение из</string>
|
||||||
<string name="repo_add_url">Адрес репозитория</string>
|
<string name="repo_add_url">Адрес репозитория</string>
|
||||||
<string name="repo_alrt">Список репозиториев изменился.
|
<string name="repo_alrt">Список репозиториев изменился.
|
||||||
@ -82,7 +82,7 @@
|
|||||||
%1$s</string>
|
%1$s</string>
|
||||||
<string name="status_connecting_to_repo">Соединение с
|
<string name="status_connecting_to_repo">Соединение с
|
||||||
%1$s</string>
|
%1$s</string>
|
||||||
<string name="status_checking_compatibility">Проверка совместимости приложений с устройством...</string>
|
<string name="status_checking_compatibility">Проверка совместимости приложений с устройством…</string>
|
||||||
<string name="no_permissions">Разрешений не требуется.</string>
|
<string name="no_permissions">Разрешений не требуется.</string>
|
||||||
<string name="permissions_for_long">Разрешения для версии %s</string>
|
<string name="permissions_for_long">Разрешения для версии %s</string>
|
||||||
<string name="showPermissions">Показывать разрешения</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_noninstalled">Na razpolago</string>
|
||||||
<string name="tab_updates">Posodobitve</string>
|
<string name="tab_updates">Posodobitve</string>
|
||||||
<string name="process_wait_title">Počakajte prosim</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="download_server">Prejem aplikacije iz</string>
|
||||||
<string name="repo_add_url">Naslov skladišča</string>
|
<string name="repo_add_url">Naslov skladišča</string>
|
||||||
<string name="repo_alrt">Spisek uporabljenih skladišč se je spremenil.
|
<string name="repo_alrt">Spisek uporabljenih skladišč se je spremenil.
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
<string name="many_updates_available">%d нове/нових верзија на располагању</string>
|
<string name="many_updates_available">%d нове/нових верзија на располагању</string>
|
||||||
<string name="fdroid_updates_available">Ажурирање Ф-Дроида на располагању.</string>
|
<string name="fdroid_updates_available">Ажурирање Ф-Дроида на располагању.</string>
|
||||||
<string name="process_wait_title">Сачекајте</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="download_server">Скида се апликација са</string>
|
||||||
<string name="repo_add_url">Адреса ризнице</string>
|
<string name="repo_add_url">Адреса ризнице</string>
|
||||||
<string name="repo_alrt">Промењена је листа ризница у употреби.
|
<string name="repo_alrt">Промењена је листа ризница у употреби.
|
||||||
@ -96,7 +96,7 @@
|
|||||||
%1$s</string>
|
%1$s</string>
|
||||||
<string name="status_connecting_to_repo">Повезивање са
|
<string name="status_connecting_to_repo">Повезивање са
|
||||||
%1$s</string>
|
%1$s</string>
|
||||||
<string name="status_checking_compatibility">Проверава се да ли је апликација компатибилна са вашим уређајем...</string>
|
<string name="status_checking_compatibility">Проверава се да ли је апликација компатибилна са вашим уређајем…</string>
|
||||||
<string name="no_permissions">Не захтевају се никакве дозволе.</string>
|
<string name="no_permissions">Не захтевају се никакве дозволе.</string>
|
||||||
<string name="permissions_for_long">Дозволе за верзију %s</string>
|
<string name="permissions_for_long">Дозволе за верзију %s</string>
|
||||||
<string name="showPermissions">Прикажи дозволе</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="many_updates_available">%d uppdateringar finns tillgängliga.</string>
|
||||||
<string name="fdroid_updates_available">Uppdateringar för F-Droid 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_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="download_server">Hämtar program från</string>
|
||||||
<string name="repo_add_url">Förrådsadress</string>
|
<string name="repo_add_url">Förrådsadress</string>
|
||||||
<string name="repo_alrt">Listan över förråd har ändrats.
|
<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="many_updates_available">%d güncelleme bulunmaktadır.</string>
|
||||||
<string name="fdroid_updates_available">F-Droid güncellemeleri 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_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="download_server">Uygulama buradan alınıyor:</string>
|
||||||
<string name="repo_add_url">Depo adresi</string>
|
<string name="repo_add_url">Depo adresi</string>
|
||||||
<string name="repo_alrt">Kullanılan depoların listesi değişti.
|
<string name="repo_alrt">Kullanılan depoların listesi değişti.
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
<string name="tab_noninstalled">Наявне</string>
|
<string name="tab_noninstalled">Наявне</string>
|
||||||
<string name="tab_updates">Оновлення</string>
|
<string name="tab_updates">Оновлення</string>
|
||||||
<string name="process_wait_title">Зачекайте</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="download_server">Звантажую програму</string>
|
||||||
<string name="repo_add_url">Адреса репозиторію</string>
|
<string name="repo_add_url">Адреса репозиторію</string>
|
||||||
<string name="repo_alrt">Список репозиторіїв змінено.
|
<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_dogecoin">Dogecoin</string>
|
||||||
<string name="menu_flattr">Flattr</string>
|
<string name="menu_flattr">Flattr</string>
|
||||||
|
|
||||||
|
<string name="https">https://</string>
|
||||||
|
|
||||||
<string-array name="updateIntervalValues">
|
<string-array name="updateIntervalValues">
|
||||||
<item>0</item>
|
<item>0</item>
|
||||||
<item>1</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="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="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="version">Version</string>
|
||||||
|
<string name="edit">Edit</string>
|
||||||
|
<string name="delete">Delete</string>
|
||||||
<string name="cache_downloaded">App cache</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_on">Keep downloaded apk files on SD card</string>
|
||||||
<string name="cache_downloaded_off">Do not keep any apk files</string>
|
<string name="cache_downloaded_off">Do not keep any apk files</string>
|
||||||
|
|
||||||
<string name="updates">Updates</string>
|
<string name="updates">Updates</string>
|
||||||
<string name="other">Other</string>
|
<string name="other">Other</string>
|
||||||
<string name="last_update_check">Last repo scan: %s</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="many_updates_available">%d updates are available.</string>
|
||||||
<string name="fdroid_updates_available">F-Droid Updates Available</string>
|
<string name="fdroid_updates_available">F-Droid Updates Available</string>
|
||||||
<string name="process_wait_title">Please Wait</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="download_server">Getting application from</string>
|
||||||
|
|
||||||
<string name="repo_add_url">Repository address</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">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_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>
|
<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>
|
want to update them?</string>
|
||||||
|
|
||||||
<string name="menu_update_repo">Update Repos</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_preferences">Preferences</string>
|
||||||
<string name="menu_about">About</string>
|
<string name="menu_about">About</string>
|
||||||
<string name="menu_search">Search</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_on">Show icons at a smaller size</string>
|
||||||
<string name="compactlayout_off">Show icons at regular size</string>
|
<string name="compactlayout_off">Show icons at regular size</string>
|
||||||
<string name="theme">Theme</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>
|
<string name="minsdk_or_later">Android %s or later</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -85,9 +85,20 @@ public class AppDetails extends ListActivity {
|
|||||||
private static final int REQUEST_INSTALL = 0;
|
private static final int REQUEST_INSTALL = 0;
|
||||||
private static final int REQUEST_UNINSTALL = 1;
|
private static final int REQUEST_UNINSTALL = 1;
|
||||||
|
|
||||||
|
private static class ViewHolder {
|
||||||
|
TextView version;
|
||||||
|
TextView status;
|
||||||
|
TextView size;
|
||||||
|
TextView api;
|
||||||
|
TextView buildtype;
|
||||||
|
TextView added;
|
||||||
|
TextView nativecode;
|
||||||
|
}
|
||||||
|
|
||||||
private class ApkListAdapter extends BaseAdapter {
|
private class ApkListAdapter extends BaseAdapter {
|
||||||
|
|
||||||
private List<DB.Apk> items;
|
private List<DB.Apk> items;
|
||||||
|
private LayoutInflater mInflater;
|
||||||
|
|
||||||
public ApkListAdapter(Context context, List<DB.Apk> items) {
|
public ApkListAdapter(Context context, List<DB.Apk> items) {
|
||||||
this.items = new ArrayList<DB.Apk>();
|
this.items = new ArrayList<DB.Apk>();
|
||||||
@ -96,6 +107,8 @@ public class AppDetails extends ListActivity {
|
|||||||
this.addItem(apk);
|
this.addItem(apk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mInflater = (LayoutInflater) mctx.getSystemService(
|
||||||
|
Context.LAYOUT_INFLATER_SERVICE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addItem(DB.Apk apk) {
|
public void addItem(DB.Apk apk) {
|
||||||
@ -128,72 +141,85 @@ public class AppDetails extends ListActivity {
|
|||||||
|
|
||||||
java.text.DateFormat df = DateFormat.getDateFormat(mctx);
|
java.text.DateFormat df = DateFormat.getDateFormat(mctx);
|
||||||
DB.Apk apk = items.get(position);
|
DB.Apk apk = items.get(position);
|
||||||
|
ViewHolder holder;
|
||||||
|
|
||||||
View v = convertView;
|
if (convertView == null) {
|
||||||
if (v == null) {
|
convertView = mInflater.inflate(R.layout.apklistitem, null);
|
||||||
LayoutInflater vi = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
||||||
v = vi.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);
|
holder.version.setText(getString(R.string.version)
|
||||||
tv.setText(getString(R.string.version) + " " + apk.version
|
+ " " + apk.version
|
||||||
+ (apk == app.curApk ? " ☆" : ""));
|
+ (apk == app.curApk ? " ☆" : ""));
|
||||||
tv.setEnabled(apk.compatible);
|
|
||||||
|
|
||||||
tv = (TextView) v.findViewById(R.id.status);
|
|
||||||
if (apk.vercode == app.installedVerCode
|
if (apk.vercode == app.installedVerCode
|
||||||
&& apk.sig.equals(mInstalledSigID)) {
|
&& apk.sig.equals(mInstalledSigID)) {
|
||||||
tv.setText(getString(R.string.inst));
|
holder.status.setText(getString(R.string.inst));
|
||||||
} else {
|
} else {
|
||||||
tv.setText(getString(R.string.not_inst));
|
holder.status.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tv = (TextView) v.findViewById(R.id.api);
|
if (apk.detail_size > 0) {
|
||||||
if (apk.minSdkVersion == 0) {
|
holder.size.setText(Utils.getFriendlySize(apk.detail_size));
|
||||||
tv.setText("");
|
|
||||||
} else {
|
} 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)));
|
Utils.getAndroidVersionName(apk.minSdkVersion)));
|
||||||
tv.setEnabled(apk.compatible);
|
} else {
|
||||||
|
holder.api.setText("");
|
||||||
}
|
}
|
||||||
|
|
||||||
tv = (TextView) v.findViewById(R.id.buildtype);
|
|
||||||
if (apk.srcname != null) {
|
if (apk.srcname != null) {
|
||||||
tv.setText("source");
|
holder.buildtype.setText("source");
|
||||||
} else {
|
} else {
|
||||||
tv.setText("bin");
|
holder.buildtype.setText("bin");
|
||||||
}
|
}
|
||||||
tv.setEnabled(apk.compatible);
|
|
||||||
|
|
||||||
tv = (TextView) v.findViewById(R.id.added);
|
|
||||||
if (apk.added != null) {
|
if (apk.added != null) {
|
||||||
tv.setVisibility(View.VISIBLE);
|
holder.added.setText(getString(R.string.added_on,
|
||||||
tv.setText(getString(R.string.added_on, df.format(apk.added)));
|
df.format(apk.added)));
|
||||||
tv.setEnabled(apk.compatible);
|
|
||||||
} else {
|
} else {
|
||||||
tv.setVisibility(View.GONE);
|
holder.added.setText("");
|
||||||
}
|
}
|
||||||
|
|
||||||
tv = (TextView) v.findViewById(R.id.nativecode);
|
|
||||||
if (pref_expert && apk.nativecode != null) {
|
if (pref_expert && apk.nativecode != null) {
|
||||||
tv.setVisibility(View.VISIBLE);
|
holder.nativecode.setText(apk.nativecode.toString().replaceAll(","," "));
|
||||||
tv.setText(apk.nativecode.toString().replaceAll(","," "));
|
|
||||||
tv.setEnabled(apk.compatible);
|
|
||||||
} else {
|
} 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;
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.text.ParseException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -31,6 +33,7 @@ import java.util.HashMap;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.Semaphore;
|
import java.util.concurrent.Semaphore;
|
||||||
|
|
||||||
@ -43,7 +46,6 @@ import android.content.pm.PackageInfo;
|
|||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.text.TextUtils.SimpleStringSplitter;
|
import android.text.TextUtils.SimpleStringSplitter;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
@ -51,6 +53,8 @@ import android.util.Log;
|
|||||||
|
|
||||||
import org.fdroid.fdroid.compat.Compatibility;
|
import org.fdroid.fdroid.compat.Compatibility;
|
||||||
import org.fdroid.fdroid.compat.ContextCompat;
|
import org.fdroid.fdroid.compat.ContextCompat;
|
||||||
|
import org.fdroid.fdroid.compat.SupportedArchitectures;
|
||||||
|
import org.fdroid.fdroid.data.DBHelper;
|
||||||
|
|
||||||
public class DB {
|
public class DB {
|
||||||
|
|
||||||
@ -65,7 +69,7 @@ public class DB {
|
|||||||
// Get access to the database. Must be called before any database activity,
|
// Get access to the database. Must be called before any database activity,
|
||||||
// and releaseDB must be called subsequently. Returns null in the event of
|
// and releaseDB must be called subsequently. Returns null in the event of
|
||||||
// failure.
|
// failure.
|
||||||
static DB getDB() {
|
public static DB getDB() {
|
||||||
try {
|
try {
|
||||||
dbSync.acquire();
|
dbSync.acquire();
|
||||||
return dbInstance;
|
return dbInstance;
|
||||||
@ -75,12 +79,10 @@ public class DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Release database access lock acquired via getDB().
|
// Release database access lock acquired via getDB().
|
||||||
static void releaseDB() {
|
public static void releaseDB() {
|
||||||
dbSync.release();
|
dbSync.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String DATABASE_NAME = "fdroid";
|
|
||||||
|
|
||||||
// Possible values of the SQLite flag "synchronous"
|
// Possible values of the SQLite flag "synchronous"
|
||||||
public static final int SYNC_OFF = 0;
|
public static final int SYNC_OFF = 0;
|
||||||
public static final int SYNC_NORMAL = 1;
|
public static final int SYNC_NORMAL = 1;
|
||||||
@ -88,24 +90,7 @@ public class DB {
|
|||||||
|
|
||||||
private SQLiteDatabase db;
|
private SQLiteDatabase db;
|
||||||
|
|
||||||
// The TABLE_APP table stores details of all the applications we know about.
|
public static final String TABLE_APP = "fdroid_app";
|
||||||
// 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 class App implements Comparable<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
|
// The TABLE_APK table stores details of all the application versions we
|
||||||
// know about. Each relates directly back to an entry in TABLE_APP.
|
// know about. Each relates directly back to an entry in TABLE_APP.
|
||||||
// This information is retrieved from the repositories.
|
// This information is retrieved from the repositories.
|
||||||
private static final String TABLE_APK = "fdroid_apk";
|
public 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 class Apk {
|
public static class Apk {
|
||||||
|
|
||||||
@ -347,10 +323,10 @@ public class DB {
|
|||||||
private static class CompatibilityChecker extends Compatibility {
|
private static class CompatibilityChecker extends Compatibility {
|
||||||
|
|
||||||
private HashSet<String> features;
|
private HashSet<String> features;
|
||||||
private List<String> cpuAbis;
|
private HashSet<String> cpuAbis;
|
||||||
|
private String cpuAbisDesc;
|
||||||
private boolean ignoreTouchscreen;
|
private boolean ignoreTouchscreen;
|
||||||
|
|
||||||
//@SuppressLint("NewApi")
|
|
||||||
public CompatibilityChecker(Context ctx) {
|
public CompatibilityChecker(Context ctx) {
|
||||||
|
|
||||||
SharedPreferences prefs = PreferenceManager
|
SharedPreferences prefs = PreferenceManager
|
||||||
@ -370,11 +346,17 @@ public class DB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cpuAbis = new ArrayList<String>(2);
|
cpuAbis = SupportedArchitectures.getAbis();
|
||||||
cpuAbis.add(android.os.Build.CPU_ABI);
|
|
||||||
if (hasApi(8)) {
|
StringBuilder builder = new StringBuilder();
|
||||||
cpuAbis.add(android.os.Build.CPU_ABI2);
|
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());
|
Log.d("FDroid", logMsg.toString());
|
||||||
}
|
}
|
||||||
@ -408,7 +390,7 @@ public class DB {
|
|||||||
if (!compatibleApi(apk.nativecode)) {
|
if (!compatibleApi(apk.nativecode)) {
|
||||||
Log.d("FDroid", apk.id + " vercode " + apk.vercode
|
Log.d("FDroid", apk.id + " vercode " + apk.vercode
|
||||||
+ " only supports " + CommaSeparatedList.str(apk.nativecode)
|
+ " only supports " + CommaSeparatedList.str(apk.nativecode)
|
||||||
+ " while your architecture is " + cpuAbis.get(0));
|
+ " while your architectures are " + cpuAbisDesc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -417,14 +399,7 @@ public class DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The TABLE_REPO table stores the details of the repositories in use.
|
// The TABLE_REPO table stores the details of the repositories in use.
|
||||||
private static final String TABLE_REPO = "fdroid_repo";
|
public 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 class Repo {
|
public static class Repo {
|
||||||
public int id;
|
public int id;
|
||||||
@ -438,16 +413,104 @@ public class DB {
|
|||||||
public String fingerprint; // always null for an unsigned repo
|
public String fingerprint; // always null for an unsigned repo
|
||||||
public int maxage; // maximum age of index that will be accepted - 0 for any
|
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 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 int countAppsForRepo(int id) {
|
||||||
|
String[] selection = { "COUNT(distinct id)" };
|
||||||
private static void createAppApk(SQLiteDatabase db) {
|
String[] selectionArgs = { Integer.toString(id) };
|
||||||
db.execSQL(CREATE_TABLE_APP);
|
Cursor result = db.query(
|
||||||
db.execSQL("create index app_id on " + TABLE_APP + " (id);");
|
TABLE_APK, selection, "repo = ?", selectionArgs, "repo", null, null);
|
||||||
db.execSQL(CREATE_TABLE_APK);
|
if (result.getCount() > 0) {
|
||||||
db.execSQL("create index apk_vercode on " + TABLE_APK + " (vercode);");
|
result.moveToFirst();
|
||||||
db.execSQL("create index apk_id on " + TABLE_APK + " (id);");
|
return result.getInt(0);
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String calcFingerprint(String pubkey) {
|
public static String calcFingerprint(String pubkey) {
|
||||||
@ -472,156 +535,12 @@ public class DB {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resetTransient(SQLiteDatabase db) {
|
/**
|
||||||
mContext.getSharedPreferences("FDroid", Context.MODE_PRIVATE).edit()
|
* Get the local storage (cache) path. This will also create it if
|
||||||
.putBoolean("triedEmptyUpdate", false).commit();
|
* it doesn't exist. It can return null if it's currently unavailable.
|
||||||
db.execSQL("drop table " + TABLE_APP);
|
*/
|
||||||
db.execSQL("drop table " + TABLE_APK);
|
public static File getDataPath(Context ctx) {
|
||||||
db.execSQL("update " + TABLE_REPO + " set lastetag = NULL");
|
return ContextCompat.create(ctx).getExternalCacheDir();
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
@ -629,7 +548,8 @@ public class DB {
|
|||||||
|
|
||||||
// The date format used for storing dates (e.g. lastupdated, added) in the
|
// The date format used for storing dates (e.g. lastupdated, added) in the
|
||||||
// database.
|
// database.
|
||||||
private SimpleDateFormat mDateFormat = new SimpleDateFormat("yyyy-MM-dd");
|
public static SimpleDateFormat dateFormat = new SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd", Locale.ENGLISH);
|
||||||
|
|
||||||
private DB(Context ctx) {
|
private DB(Context ctx) {
|
||||||
|
|
||||||
@ -656,16 +576,11 @@ public class DB {
|
|||||||
db = null;
|
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
|
// Delete the database, which should cause it to be re-created next time
|
||||||
// it's used.
|
// it's used.
|
||||||
public static void delete(Context ctx) {
|
public static void delete(Context ctx) {
|
||||||
try {
|
try {
|
||||||
ctx.deleteDatabase(DATABASE_NAME);
|
ctx.deleteDatabase(DBHelper.DATABASE_NAME);
|
||||||
// Also try and delete the old one, from versions 0.13 and earlier.
|
// Also try and delete the old one, from versions 0.13 and earlier.
|
||||||
ctx.deleteDatabase("fdroid_db");
|
ctx.deleteDatabase("fdroid_db");
|
||||||
} catch (Exception ex) {
|
} 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
|
// Return a list of apps matching the given criteria. Filtering is
|
||||||
// also done based on compatibility and anti-features according to
|
// also done based on compatibility and anti-features according to
|
||||||
// the user's current preferences.
|
// the user's current preferences.
|
||||||
@ -837,10 +741,10 @@ public class DB {
|
|||||||
app.curVercode = c.getInt(9);
|
app.curVercode = c.getInt(9);
|
||||||
String sAdded = c.getString(10);
|
String sAdded = c.getString(10);
|
||||||
app.added = (sAdded == null || sAdded.length() == 0) ? null
|
app.added = (sAdded == null || sAdded.length() == 0) ? null
|
||||||
: mDateFormat.parse(sAdded);
|
: dateFormat.parse(sAdded);
|
||||||
String sLastUpdated = c.getString(11);
|
String sLastUpdated = c.getString(11);
|
||||||
app.lastUpdated = (sLastUpdated == null || sLastUpdated
|
app.lastUpdated = (sLastUpdated == null || sLastUpdated
|
||||||
.length() == 0) ? null : mDateFormat
|
.length() == 0) ? null : dateFormat
|
||||||
.parse(sLastUpdated);
|
.parse(sLastUpdated);
|
||||||
app.compatible = c.getInt(12) == 1;
|
app.compatible = c.getInt(12) == 1;
|
||||||
app.ignoreAllUpdates = c.getInt(13) == 1;
|
app.ignoreAllUpdates = c.getInt(13) == 1;
|
||||||
@ -921,7 +825,7 @@ public class DB {
|
|||||||
apk.minSdkVersion = c.getInt(6);
|
apk.minSdkVersion = c.getInt(6);
|
||||||
String sApkAdded = c.getString(7);
|
String sApkAdded = c.getString(7);
|
||||||
apk.added = (sApkAdded == null || sApkAdded.length() == 0) ? null
|
apk.added = (sApkAdded == null || sApkAdded.length() == 0) ? null
|
||||||
: mDateFormat.parse(sApkAdded);
|
: dateFormat.parse(sApkAdded);
|
||||||
apk.features = CommaSeparatedList.make(c.getString(8));
|
apk.features = CommaSeparatedList.make(c.getString(8));
|
||||||
apk.nativecode = CommaSeparatedList.make(c.getString(9));
|
apk.nativecode = CommaSeparatedList.make(c.getString(9));
|
||||||
apk.compatible = compatible;
|
apk.compatible = compatible;
|
||||||
@ -1245,10 +1149,10 @@ public class DB {
|
|||||||
values.put("dogecoinAddr", upapp.detail_dogecoinAddr);
|
values.put("dogecoinAddr", upapp.detail_dogecoinAddr);
|
||||||
values.put("flattrID", upapp.detail_flattrID);
|
values.put("flattrID", upapp.detail_flattrID);
|
||||||
values.put("added",
|
values.put("added",
|
||||||
upapp.added == null ? "" : mDateFormat.format(upapp.added));
|
upapp.added == null ? "" : dateFormat.format(upapp.added));
|
||||||
values.put(
|
values.put(
|
||||||
"lastUpdated",
|
"lastUpdated",
|
||||||
upapp.added == null ? "" : mDateFormat
|
upapp.added == null ? "" : dateFormat
|
||||||
.format(upapp.lastUpdated));
|
.format(upapp.lastUpdated));
|
||||||
values.put("curVersion", upapp.curVersion);
|
values.put("curVersion", upapp.curVersion);
|
||||||
values.put("curVercode", upapp.curVercode);
|
values.put("curVercode", upapp.curVercode);
|
||||||
@ -1293,7 +1197,7 @@ public class DB {
|
|||||||
values.put("apkName", upapk.apkName);
|
values.put("apkName", upapk.apkName);
|
||||||
values.put("minSdkVersion", upapk.minSdkVersion);
|
values.put("minSdkVersion", upapk.minSdkVersion);
|
||||||
values.put("added",
|
values.put("added",
|
||||||
upapk.added == null ? "" : mDateFormat.format(upapk.added));
|
upapk.added == null ? "" : dateFormat.format(upapk.added));
|
||||||
values.put("permissions",
|
values.put("permissions",
|
||||||
CommaSeparatedList.str(upapk.detail_permissions));
|
CommaSeparatedList.str(upapk.detail_permissions));
|
||||||
values.put("features", CommaSeparatedList.str(upapk.features));
|
values.put("features", CommaSeparatedList.str(upapk.features));
|
||||||
@ -1315,7 +1219,7 @@ public class DB {
|
|||||||
try {
|
try {
|
||||||
c = db.query(TABLE_REPO, new String[] { "address", "name",
|
c = db.query(TABLE_REPO, new String[] { "address", "name",
|
||||||
"description", "version", "inuse", "priority", "pubkey",
|
"description", "version", "inuse", "priority", "pubkey",
|
||||||
"fingerprint", "maxage", "lastetag" },
|
"fingerprint", "maxage", "lastetag", "lastUpdated" },
|
||||||
"id = ?", new String[] { Integer.toString(id) }, null, null, null);
|
"id = ?", new String[] { Integer.toString(id) }, null, null, null);
|
||||||
if (!c.moveToFirst())
|
if (!c.moveToFirst())
|
||||||
return null;
|
return null;
|
||||||
@ -1331,6 +1235,13 @@ public class DB {
|
|||||||
repo.fingerprint = c.getString(7);
|
repo.fingerprint = c.getString(7);
|
||||||
repo.maxage = c.getInt(8);
|
repo.maxage = c.getInt(8);
|
||||||
repo.lastetag = c.getString(9);
|
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;
|
return repo;
|
||||||
} finally {
|
} finally {
|
||||||
if (c != null)
|
if (c != null)
|
||||||
@ -1373,6 +1284,27 @@ public class DB {
|
|||||||
return repos;
|
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) {
|
public void changeServerStatus(String address) {
|
||||||
db.execSQL("update " + TABLE_REPO
|
db.execSQL("update " + TABLE_REPO
|
||||||
+ " set inuse=1-inuse, lastetag=null where address = ?",
|
+ " set inuse=1-inuse, lastetag=null where address = ?",
|
||||||
@ -1387,8 +1319,17 @@ public class DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void updateRepoByAddress(Repo repo) {
|
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();
|
ContentValues values = new ContentValues();
|
||||||
values.put("name", repo.name);
|
values.put("name", repo.name);
|
||||||
|
values.put("address", repo.address);
|
||||||
values.put("description", repo.description);
|
values.put("description", repo.description);
|
||||||
values.put("version", repo.version);
|
values.put("version", repo.version);
|
||||||
values.put("inuse", repo.inuse);
|
values.put("inuse", repo.inuse);
|
||||||
@ -1402,13 +1343,24 @@ public class DB {
|
|||||||
}
|
}
|
||||||
values.put("maxage", repo.maxage);
|
values.put("maxage", repo.maxage);
|
||||||
values.put("lastetag", (String) null);
|
values.put("lastetag", (String) null);
|
||||||
db.update(TABLE_REPO, values, "address = ?",
|
db.update(TABLE_REPO, values, field + " = ?",
|
||||||
new String[] { repo.address });
|
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) {
|
public void writeLastEtag(Repo repo) {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put("lastetag", repo.lastetag);
|
values.put("lastetag", repo.lastetag);
|
||||||
|
values.put("lastUpdated", dateFormat.format(new Date()));
|
||||||
db.update(TABLE_REPO, values, "address = ?",
|
db.update(TABLE_REPO, values, "address = ?",
|
||||||
new String[] { repo.address });
|
new String[] { repo.address });
|
||||||
}
|
}
|
||||||
@ -1429,7 +1381,7 @@ public class DB {
|
|||||||
if (fingerprint == null) {
|
if (fingerprint == null) {
|
||||||
fingerprint = calcedFingerprint;
|
fingerprint = calcedFingerprint;
|
||||||
} else if (calcedFingerprint != null) {
|
} else if (calcedFingerprint != null) {
|
||||||
fingerprint = fingerprint.toUpperCase();
|
fingerprint = fingerprint.toUpperCase(Locale.ENGLISH);
|
||||||
if (!fingerprint.equals(calcedFingerprint)) {
|
if (!fingerprint.equals(calcedFingerprint)) {
|
||||||
throw new SecurityException("Given fingerprint does not match calculated one! ("
|
throw new SecurityException("Given fingerprint does not match calculated one! ("
|
||||||
+ fingerprint + " != " + calcedFingerprint);
|
+ fingerprint + " != " + calcedFingerprint);
|
||||||
@ -1441,18 +1393,23 @@ public class DB {
|
|||||||
db.insert(TABLE_REPO, null, values);
|
db.insert(TABLE_REPO, null, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void doDisableRepos(List<String> addresses, boolean remove) {
|
public void doDisableRepos(List<Repo> repos, boolean remove) {
|
||||||
if (addresses.isEmpty()) return;
|
if (repos.isEmpty()) return;
|
||||||
db.beginTransaction();
|
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
|
// Before removing the repo, remove any apks that are
|
||||||
// connected to it...
|
// connected to it...
|
||||||
Cursor c = null;
|
Cursor c = null;
|
||||||
try {
|
try {
|
||||||
c = db.query(TABLE_REPO, new String[] { "id" },
|
c = db.query(TABLE_REPO, new String[]{"id"},
|
||||||
"address = ?", new String[] { address },
|
"address = ?", new String[]{address},
|
||||||
null, null, null, null);
|
null, null, null, null);
|
||||||
c.moveToFirst();
|
c.moveToFirst();
|
||||||
if (!c.isAfterLast()) {
|
if (!c.isAfterLast()) {
|
||||||
@ -1467,6 +1424,13 @@ public class DB {
|
|||||||
if (remove)
|
if (remove)
|
||||||
db.delete(TABLE_REPO, "address = ?",
|
db.delete(TABLE_REPO, "address = ?",
|
||||||
new String[] { 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);
|
List<App> apps = getApps(false);
|
||||||
for (App app : apps) {
|
for (App app : apps) {
|
||||||
|
@ -19,22 +19,15 @@
|
|||||||
|
|
||||||
package org.fdroid.fdroid;
|
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;
|
||||||
import android.app.AlertDialog.Builder;
|
import android.app.AlertDialog.Builder;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.ProgressDialog;
|
import android.content.*;
|
||||||
import android.content.pm.PackageInfo;
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.ResultReceiver;
|
|
||||||
import android.support.v4.app.FragmentActivity;
|
|
||||||
import android.support.v4.view.ViewPager;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.ContextThemeWrapper;
|
import android.view.ContextThemeWrapper;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@ -42,6 +35,11 @@ import android.view.Menu;
|
|||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.*;
|
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.compat.TabManager;
|
||||||
import org.fdroid.fdroid.views.AppListFragmentPageAdapter;
|
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 ABOUT = Menu.FIRST + 3;
|
||||||
private static final int SEARCH = Menu.FIRST + 4;
|
private static final int SEARCH = Menu.FIRST + 4;
|
||||||
|
|
||||||
private ProgressDialog pd;
|
|
||||||
|
|
||||||
private ViewPager viewPager;
|
private ViewPager viewPager;
|
||||||
|
|
||||||
private AppListManager manager = null;
|
private AppListManager manager = null;
|
||||||
@ -131,8 +127,6 @@ public class FDroid extends FragmentActivity {
|
|||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
|
||||||
super.onCreateOptionsMenu(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(
|
menu.add(Menu.NONE, MANAGE_REPO, 2, R.string.menu_manage).setIcon(
|
||||||
android.R.drawable.ic_menu_agenda);
|
android.R.drawable.ic_menu_agenda);
|
||||||
MenuItem search = menu.add(Menu.NONE, SEARCH, 3, R.string.menu_search).setIcon(
|
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:
|
case REQUEST_APPDETAILS:
|
||||||
break;
|
break;
|
||||||
case REQUEST_MANAGEREPOS:
|
case REQUEST_MANAGEREPOS:
|
||||||
if (data.hasExtra("update")) {
|
if (data.hasExtra(ManageRepo.REQUEST_UPDATE)) {
|
||||||
AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this);
|
AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this);
|
||||||
ask_alrt.setTitle(getString(R.string.repo_update_title));
|
ask_alrt.setTitle(getString(R.string.repo_update_title));
|
||||||
ask_alrt.setIcon(android.R.drawable.ic_menu_rotate);
|
ask_alrt.setIcon(android.R.drawable.ic_menu_rotate);
|
||||||
@ -241,7 +235,7 @@ public class FDroid extends FragmentActivity {
|
|||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog,
|
public void onClick(DialogInterface dialog,
|
||||||
int whichButton) {
|
int whichButton) {
|
||||||
updateRepos();
|
updateRepos();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ask_alrt.setNegativeButton(getString(R.string.no),
|
ask_alrt.setNegativeButton(getString(R.string.no),
|
||||||
@ -249,6 +243,7 @@ public class FDroid extends FragmentActivity {
|
|||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog,
|
public void onClick(DialogInterface dialog,
|
||||||
int whichButton) {
|
int whichButton) {
|
||||||
|
// do nothing
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
AlertDialog alert = ask_alrt.create();
|
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.
|
* 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.
|
* 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
|
// is told to do the update, which will result in the database changing. The
|
||||||
// UpdateReceiver class should get told when this is finished.
|
// UpdateReceiver class should get told when this is finished.
|
||||||
public void updateRepos() {
|
public void updateRepos() {
|
||||||
|
UpdateService.updateNow(this).setListener(new ProgressListener() {
|
||||||
pd = ProgressDialog.show(this, getString(R.string.process_wait_title),
|
@Override
|
||||||
getString(R.string.process_update_msg), true, true);
|
public void onProgress(Event event) {
|
||||||
pd.setIcon(android.R.drawable.ic_dialog_info);
|
if (event.type == UpdateService.STATUS_COMPLETE_WITH_CHANGES){
|
||||||
pd.setCanceledOnTouchOutside(false);
|
repopulateViews();
|
||||||
|
}
|
||||||
Intent intent = new Intent(this, UpdateService.class);
|
}
|
||||||
UpdateReceiver mUpdateReceiver = new UpdateReceiver(new Handler());
|
});
|
||||||
intent.putExtra("receiver", mUpdateReceiver);
|
|
||||||
startService(intent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private TabManager getTabManager() {
|
private TabManager getTabManager() {
|
||||||
|
@ -19,26 +19,45 @@
|
|||||||
package org.fdroid.fdroid;
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
import java.io.File;
|
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.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Semaphore;
|
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.app.Activity;
|
||||||
import android.preference.PreferenceManager;
|
import android.app.Application;
|
||||||
import android.util.Log;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
import org.fdroid.fdroid.Utils;
|
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.LimitedAgeDiscCache;
|
||||||
|
import com.nostra13.universalimageloader.cache.disc.impl.UnlimitedDiscCache;
|
||||||
import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator;
|
import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator;
|
||||||
|
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
||||||
|
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
||||||
import com.nostra13.universalimageloader.utils.StorageUtils;
|
import com.nostra13.universalimageloader.utils.StorageUtils;
|
||||||
|
|
||||||
|
import de.duenndns.ssl.MemorizingTrustManager;
|
||||||
|
|
||||||
|
import org.thoughtcrime.ssl.pinning.PinningTrustManager;
|
||||||
|
import org.thoughtcrime.ssl.pinning.SystemKeyStore;
|
||||||
|
|
||||||
public class FDroidApp extends Application {
|
public class FDroidApp extends Application {
|
||||||
|
|
||||||
private static enum Theme {
|
private static enum Theme {
|
||||||
@ -117,6 +136,44 @@ public class FDroidApp extends Application {
|
|||||||
.threadPoolSize(Runtime.getRuntime().availableProcessors() * 2)
|
.threadPoolSize(Runtime.getRuntime().availableProcessors() * 2)
|
||||||
.build();
|
.build();
|
||||||
ImageLoader.getInstance().init(config);
|
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;
|
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.io.InputStream;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public class Hasher {
|
public class Hasher {
|
||||||
|
|
||||||
@ -86,7 +89,7 @@ public class Hasher {
|
|||||||
if (hashCache == null) getHash();
|
if (hashCache == null) getHash();
|
||||||
if (otherHash == null || hashCache.equals(""))
|
if (otherHash == null || hashCache.equals(""))
|
||||||
return false;
|
return false;
|
||||||
return hashCache.equals(otherHash.toLowerCase());
|
return hashCache.equals(otherHash.toLowerCase(Locale.ENGLISH));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reset() {
|
public void reset() {
|
||||||
@ -94,6 +97,16 @@ public class Hasher {
|
|||||||
digest.reset();
|
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) {
|
public static String hex(byte[] sig) {
|
||||||
byte[] csig = new byte[sig.length * 2];
|
byte[] csig = new byte[sig.length * 2];
|
||||||
for (int j = 0; j < sig.length; j++) {
|
for (int j = 0; j < sig.length; j++) {
|
||||||
|
@ -19,72 +19,59 @@
|
|||||||
|
|
||||||
package org.fdroid.fdroid;
|
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.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.app.AlertDialog.Builder;
|
|
||||||
import android.app.ListActivity;
|
import android.app.ListActivity;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.v4.app.NavUtils;
|
import android.support.v4.app.NavUtils;
|
||||||
import android.support.v4.view.MenuItemCompat;
|
import android.support.v4.view.MenuItemCompat;
|
||||||
import android.text.format.DateFormat;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.Button;
|
import android.widget.*;
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.SimpleAdapter;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import org.fdroid.fdroid.DB.Repo;
|
import org.fdroid.fdroid.DB.Repo;
|
||||||
import org.fdroid.fdroid.compat.ActionBarCompat;
|
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 {
|
public class ManageRepo extends ListActivity {
|
||||||
|
|
||||||
private final int ADD_REPO = 1;
|
private static final String DEFAULT_NEW_REPO_TEXT = "https://";
|
||||||
private final int REM_REPO = 2;
|
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 {
|
private enum PositiveAction {
|
||||||
ADD_NEW, ENABLE, IGNORE
|
ADD_NEW, ENABLE, IGNORE
|
||||||
}
|
}
|
||||||
private PositiveAction positiveAction;
|
private PositiveAction positiveAction;
|
||||||
|
|
||||||
private List<DB.Repo> repos;
|
private boolean changed = false;
|
||||||
|
|
||||||
private static List<String> reposToDisable;
|
private RepoAdapter repoAdapter;
|
||||||
private static List<String> reposToRemove;
|
|
||||||
|
|
||||||
public void disableRepo(String address) {
|
/**
|
||||||
if (reposToDisable.contains(address)) return;
|
* True if activity started with an intent such as from QR code. False if
|
||||||
reposToDisable.add(address);
|
* opened from, e.g. the main menu.
|
||||||
}
|
*/
|
||||||
|
private boolean isImportingRepo = false;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@ -92,11 +79,15 @@ public class ManageRepo extends ListActivity {
|
|||||||
((FDroidApp) getApplication()).applyTheme(this);
|
((FDroidApp) getApplication()).applyTheme(this);
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
ActionBarCompat abCompat = ActionBarCompat.create(this);
|
ActionBarCompat abCompat = ActionBarCompat.create(this);
|
||||||
abCompat.setDisplayHomeAsUpEnabled(true);
|
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
|
SharedPreferences prefs = PreferenceManager
|
||||||
.getDefaultSharedPreferences(getBaseContext());
|
.getDefaultSharedPreferences(getBaseContext());
|
||||||
|
|
||||||
@ -112,8 +103,7 @@ public class ManageRepo extends ListActivity {
|
|||||||
}
|
}
|
||||||
tv_lastCheck.setText(getString(R.string.last_update_check,s_lastUpdateCheck));
|
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 */
|
/* let's see if someone is trying to send us a new repo */
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
@ -126,6 +116,9 @@ public class ManageRepo extends ListActivity {
|
|||||||
String host = uri.getHost().toLowerCase(Locale.ENGLISH);
|
String host = uri.getHost().toLowerCase(Locale.ENGLISH);
|
||||||
if (scheme.equals("fdroidrepos") || scheme.equals("fdroidrepo")
|
if (scheme.equals("fdroidrepos") || scheme.equals("fdroidrepo")
|
||||||
|| scheme.equals("https") || scheme.equals("http")) {
|
|| scheme.equals("https") || scheme.equals("http")) {
|
||||||
|
|
||||||
|
isImportingRepo = true;
|
||||||
|
|
||||||
// QRCode are more efficient in all upper case, so some incoming
|
// QRCode are more efficient in all upper case, so some incoming
|
||||||
// URLs might be encoded in all upper case. Therefore, we allow
|
// URLs might be encoded in all upper case. Therefore, we allow
|
||||||
// the standard paths to be encoded all upper case, then they'll
|
// the standard paths to be encoded all upper case, then they'll
|
||||||
@ -147,81 +140,95 @@ public class ManageRepo extends ListActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
|
|
||||||
super.onResume();
|
super.onResume();
|
||||||
redraw();
|
refreshList();
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onListItemClick(ListView l, View v, int position, long id) {
|
protected void onListItemClick(ListView l, View v, int position, long id) {
|
||||||
|
|
||||||
super.onListItemClick(l, v, position, id);
|
super.onListItemClick(l, v, position, id);
|
||||||
try {
|
|
||||||
DB db = DB.getDB();
|
DB.Repo repo = (DB.Repo)getListView().getItemAtPosition(position);
|
||||||
String address = repos.get(position).address;
|
editRepo(repo);
|
||||||
db.changeServerStatus(address);
|
}
|
||||||
// TODO: Disabling and re-enabling a repo will delete its apks too.
|
|
||||||
disableRepo(address);
|
private void refreshList() {
|
||||||
} finally {
|
repoAdapter.refresh();
|
||||||
DB.releaseDB();
|
|
||||||
}
|
|
||||||
changed = true;
|
|
||||||
redraw();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
|
||||||
super.onCreateOptionsMenu(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);
|
MenuItem updateItem = menu.add(Menu.NONE, UPDATE_REPOS, 1,
|
||||||
menu.add(Menu.NONE, REM_REPO, 2, R.string.menu_rem_repo).setIcon(
|
R.string.menu_update_repo).setIcon(R.drawable.ic_menu_refresh);
|
||||||
android.R.drawable.ic_menu_close_clear_cancel);
|
MenuItemCompat.setShowAsAction(updateItem,
|
||||||
MenuItemCompat.setShowAsAction(item,
|
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
|
||||||
MenuItemCompat.SHOW_AS_ACTION_IF_ROOM |
|
|
||||||
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
|
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;
|
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 {
|
try {
|
||||||
DB db = DB.getDB();
|
DB db = DB.getDB();
|
||||||
db.addRepo(repoUri, null, null, 0, 10, null, fingerprint, 0, true);
|
db.doDisableRepos(reposToRemove, true);
|
||||||
} finally {
|
} finally {
|
||||||
DB.releaseDB();
|
DB.releaseDB();
|
||||||
}
|
}
|
||||||
|
refreshList();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected List<Repo> getRepos() {
|
protected List<Repo> getRepos() {
|
||||||
@ -253,16 +260,30 @@ public class ManageRepo extends ListActivity {
|
|||||||
return super.onOptionsItemSelected(item);
|
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) {
|
private void showAddRepo(String newAddress, String newFingerprint) {
|
||||||
LayoutInflater li = LayoutInflater.from(this);
|
|
||||||
View view = li.inflate(R.layout.addrepo, null);
|
View view = getLayoutInflater().inflate(R.layout.addrepo, null);
|
||||||
Builder p = new AlertDialog.Builder(this).setView(view);
|
final AlertDialog alrt = new AlertDialog.Builder(this).setView(view).create();
|
||||||
final AlertDialog alrt = p.create();
|
|
||||||
final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri);
|
final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri);
|
||||||
final EditText fingerprintEditText = (EditText) view.findViewById(R.id.edit_fingerprint);
|
final EditText fingerprintEditText = (EditText) view.findViewById(R.id.edit_fingerprint);
|
||||||
|
|
||||||
List<Repo> repos = getRepos();
|
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.setIcon(android.R.drawable.ic_menu_add);
|
||||||
alrt.setTitle(getString(R.string.repo_add_title));
|
alrt.setTitle(getString(R.string.repo_add_title));
|
||||||
@ -271,15 +292,18 @@ public class ManageRepo extends ListActivity {
|
|||||||
new DialogInterface.OnClickListener() {
|
new DialogInterface.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
|
||||||
String fp = fingerprintEditText.getText().toString();
|
String fp = fingerprintEditText.getText().toString();
|
||||||
|
|
||||||
// the DB uses null for no fingerprint but the above
|
// the DB uses null for no fingerprint but the above
|
||||||
// code returns "" rather than null if its blank
|
// code returns "" rather than null if its blank
|
||||||
if (fp.equals(""))
|
if (fp.equals(""))
|
||||||
fp = null;
|
fp = null;
|
||||||
|
|
||||||
if (positiveAction == PositiveAction.ADD_NEW)
|
if (positiveAction == PositiveAction.ADD_NEW)
|
||||||
addRepoPositiveAction(uriEditText.getText().toString(), fp, null);
|
createNewRepo(uriEditText.getText().toString(), fp);
|
||||||
else if (positiveAction == PositiveAction.ENABLE)
|
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) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
setResult(Activity.RESULT_CANCELED);
|
setResult(Activity.RESULT_CANCELED);
|
||||||
finish();
|
finish();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
alrt.show();
|
alrt.show();
|
||||||
@ -332,29 +357,58 @@ public class ManageRepo extends ListActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newAddress != null)
|
|
||||||
uriEditText.setText(newAddress);
|
|
||||||
if (newFingerprint != null)
|
if (newFingerprint != null)
|
||||||
fingerprintEditText.setText(newFingerprint);
|
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) {
|
* Adds a new repo to the database.
|
||||||
addRepo(address, fingerprint);
|
*/
|
||||||
} else if (repo != null) {
|
private void createNewRepo(String address, String fingerprint) {
|
||||||
// force-enable an existing repo
|
try {
|
||||||
repo.inuse = true;
|
DB db = DB.getDB();
|
||||||
try {
|
db.addRepo(address, null, null, 0, 10, null, fingerprint, 0, true);
|
||||||
DB db = DB.getDB();
|
} finally {
|
||||||
db.updateRepoByAddress(repo);
|
DB.releaseDB();
|
||||||
} 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;
|
changed = true;
|
||||||
redraw();
|
if (isImportingRepo) {
|
||||||
setResult(Activity.RESULT_OK);
|
setResult(Activity.RESULT_OK);
|
||||||
finish();
|
finish();
|
||||||
|
} else {
|
||||||
|
refreshList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -362,89 +416,77 @@ public class ManageRepo extends ListActivity {
|
|||||||
|
|
||||||
super.onMenuItemSelected(featureId, item);
|
super.onMenuItemSelected(featureId, item);
|
||||||
|
|
||||||
switch (item.getItemId()) {
|
if (item.getItemId() == ADD_REPO) {
|
||||||
case ADD_REPO:
|
showAddRepo();
|
||||||
showAddRepo(null, null);
|
|
||||||
return true;
|
return true;
|
||||||
|
} else if (item.getItemId() == UPDATE_REPOS) {
|
||||||
case REM_REPO:
|
updateRepos();
|
||||||
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();
|
|
||||||
return true;
|
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
|
@Override
|
||||||
public void finish() {
|
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();
|
Intent ret = new Intent();
|
||||||
if (changed)
|
if (changed) {
|
||||||
ret.putExtra("update", true);
|
Log.i("FDroid", "Repo details have changed, prompting for update.");
|
||||||
this.setResult(RESULT_OK, ret);
|
ret.putExtra(REQUEST_UPDATE, true);
|
||||||
|
}
|
||||||
|
setResult(RESULT_OK, ret);
|
||||||
super.finish();
|
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;
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
public interface ProgressListener {
|
public interface ProgressListener {
|
||||||
|
|
||||||
@ -9,7 +11,7 @@ public interface ProgressListener {
|
|||||||
// I went a bit overboard with the overloaded constructors, but they all
|
// I went a bit overboard with the overloaded constructors, but they all
|
||||||
// seemed potentially useful and unambiguous, so I just put them in there
|
// seemed potentially useful and unambiguous, so I just put them in there
|
||||||
// while I'm here.
|
// while I'm here.
|
||||||
public static class Event {
|
public static class Event implements Parcelable {
|
||||||
|
|
||||||
public static final int NO_VALUE = Integer.MIN_VALUE;
|
public static final int NO_VALUE = Integer.MIN_VALUE;
|
||||||
|
|
||||||
@ -49,6 +51,30 @@ public interface ProgressListener {
|
|||||||
this.total = total;
|
this.total = total;
|
||||||
this.data = data == null ? new Bundle() : data;
|
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;
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import android.os.Bundle;
|
||||||
import java.io.File;
|
import org.fdroid.fdroid.updater.RepoUpdater;
|
||||||
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 org.xml.sax.Attributes;
|
import org.xml.sax.Attributes;
|
||||||
import org.xml.sax.InputSource;
|
|
||||||
import org.xml.sax.SAXException;
|
import org.xml.sax.SAXException;
|
||||||
import org.xml.sax.XMLReader;
|
|
||||||
import org.xml.sax.helpers.DefaultHandler;
|
import org.xml.sax.helpers.DefaultHandler;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import java.text.ParseException;
|
||||||
import android.content.Context;
|
import java.util.HashMap;
|
||||||
import android.util.Log;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class RepoXMLHandler extends DefaultHandler {
|
public class RepoXMLHandler extends DefaultHandler {
|
||||||
|
|
||||||
@ -64,10 +42,10 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
private DB.Apk curapk = null;
|
private DB.Apk curapk = null;
|
||||||
private StringBuilder curchars = new StringBuilder();
|
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.
|
// them - otherwise it will be the value specified.
|
||||||
private String version;
|
private int version = -1;
|
||||||
private String maxage;
|
private int maxage = -1;
|
||||||
|
|
||||||
// After processing the XML, this will be null if the index specified a
|
// 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
|
// 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 int progressCounter = 0;
|
||||||
private ProgressListener progressListener;
|
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;
|
private int totalAppCount;
|
||||||
|
|
||||||
public RepoXMLHandler(DB.Repo repo, List<DB.App> appsList, ProgressListener listener) {
|
public RepoXMLHandler(DB.Repo repo, List<DB.App> appsList, ProgressListener listener) {
|
||||||
@ -104,6 +73,18 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
progressListener = listener;
|
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
|
@Override
|
||||||
public void characters(char[] ch, int start, int length) {
|
public void characters(char[] ch, int start, int length) {
|
||||||
curchars.append(ch, start, length);
|
curchars.append(ch, start, length);
|
||||||
@ -176,7 +157,7 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
}
|
}
|
||||||
} else if (curel.equals("added")) {
|
} else if (curel.equals("added")) {
|
||||||
try {
|
try {
|
||||||
curapk.added = str.length() == 0 ? null : mXMLDateFormat
|
curapk.added = str.length() == 0 ? null : DB.dateFormat
|
||||||
.parse(str);
|
.parse(str);
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
curapk.added = null;
|
curapk.added = null;
|
||||||
@ -223,7 +204,7 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
curapp.detail_trackerURL = str;
|
curapp.detail_trackerURL = str;
|
||||||
} else if (curel.equals("added")) {
|
} else if (curel.equals("added")) {
|
||||||
try {
|
try {
|
||||||
curapp.added = str.length() == 0 ? null : mXMLDateFormat
|
curapp.added = str.length() == 0 ? null : DB.dateFormat
|
||||||
.parse(str);
|
.parse(str);
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
curapp.added = null;
|
curapp.added = null;
|
||||||
@ -231,7 +212,7 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
} else if (curel.equals("lastupdated")) {
|
} else if (curel.equals("lastupdated")) {
|
||||||
try {
|
try {
|
||||||
curapp.lastUpdated = str.length() == 0 ? null
|
curapp.lastUpdated = str.length() == 0 ? null
|
||||||
: mXMLDateFormat.parse(str);
|
: DB.dateFormat.parse(str);
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
curapp.lastUpdated = null;
|
curapp.lastUpdated = null;
|
||||||
}
|
}
|
||||||
@ -250,15 +231,11 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
} else if (curel.equals("requirements")) {
|
} else if (curel.equals("requirements")) {
|
||||||
curapp.requirements = DB.CommaSeparatedList.make(str);
|
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
|
@Override
|
||||||
public void startElement(String uri, String localName, String qName,
|
public void startElement(String uri, String localName, String qName,
|
||||||
Attributes attributes) throws SAXException {
|
Attributes attributes) throws SAXException {
|
||||||
@ -268,8 +245,21 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
String pk = attributes.getValue("", "pubkey");
|
String pk = attributes.getValue("", "pubkey");
|
||||||
if (pk != null)
|
if (pk != null)
|
||||||
pubkey = pk;
|
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");
|
String nm = attributes.getValue("", "name");
|
||||||
if (nm != null)
|
if (nm != null)
|
||||||
name = nm;
|
name = nm;
|
||||||
@ -281,11 +271,11 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
curapp = new DB.App();
|
curapp = new DB.App();
|
||||||
curapp.detail_Populated = true;
|
curapp.detail_Populated = true;
|
||||||
curapp.id = attributes.getValue("", "id");
|
curapp.id = attributes.getValue("", "id");
|
||||||
Bundle progressData = createProgressData(repo.address);
|
Bundle progressData = RepoUpdater.createProgressData(repo.address);
|
||||||
progressCounter ++;
|
progressCounter ++;
|
||||||
progressListener.onProgress(
|
progressListener.onProgress(
|
||||||
new ProgressListener.Event(
|
new ProgressListener.Event(
|
||||||
RepoXMLHandler.PROGRESS_TYPE_PROCESS_XML, progressCounter,
|
RepoUpdater.PROGRESS_TYPE_PROCESS_XML, progressCounter,
|
||||||
totalAppCount, progressData));
|
totalAppCount, progressData));
|
||||||
|
|
||||||
} else if (localName.equals("package") && curapp != null && curapk == null) {
|
} else if (localName.equals("package") && curapp != null && curapk == null) {
|
||||||
@ -300,243 +290,6 @@ public class RepoXMLHandler extends DefaultHandler {
|
|||||||
curchars.setLength(0);
|
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) {
|
public void setTotalAppCount(int totalAppCount) {
|
||||||
this.totalAppCount = totalAppCount;
|
this.totalAppCount = totalAppCount;
|
||||||
}
|
}
|
||||||
|
@ -21,33 +21,31 @@ package org.fdroid.fdroid;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import android.app.AlarmManager;
|
import android.app.*;
|
||||||
import android.app.IntentService;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.SharedPreferences.Editor;
|
import android.content.SharedPreferences.Editor;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.NetworkInfo;
|
import android.net.NetworkInfo;
|
||||||
import android.os.Build;
|
import android.os.*;
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.ResultReceiver;
|
|
||||||
import android.os.SystemClock;
|
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import org.fdroid.fdroid.updater.RepoUpdater;
|
||||||
|
|
||||||
import android.support.v4.app.NotificationCompat;
|
import android.support.v4.app.NotificationCompat;
|
||||||
import android.support.v4.app.TaskStackBuilder;
|
import android.support.v4.app.TaskStackBuilder;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
public class UpdateService extends IntentService implements ProgressListener {
|
public class UpdateService extends IntentService implements ProgressListener {
|
||||||
|
|
||||||
public static final String RESULT_MESSAGE = "msg";
|
public static final String RESULT_MESSAGE = "msg";
|
||||||
public static final int STATUS_CHANGES = 0;
|
public static final String RESULT_EVENT = "event";
|
||||||
public static final int STATUS_SAME = 1;
|
|
||||||
public static final int STATUS_ERROR = 2;
|
public static final int STATUS_COMPLETE_WITH_CHANGES = 0;
|
||||||
public static final int STATUS_INFO = 3;
|
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;
|
private ResultReceiver receiver = null;
|
||||||
|
|
||||||
@ -55,6 +53,76 @@ public class UpdateService extends IntentService implements ProgressListener {
|
|||||||
super("UpdateService");
|
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
|
// Schedule (or cancel schedule for) this service, according to the
|
||||||
// current preferences. Should be called a) at boot, b) if the preference
|
// current preferences. Should be called a) at boot, b) if the preference
|
||||||
// is changed, or c) on startup, in case we get upgraded.
|
// 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) {
|
protected void sendStatus(int statusCode, String message) {
|
||||||
|
sendStatus(statusCode, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void sendStatus(int statusCode, String message, Event event) {
|
||||||
if (receiver != null) {
|
if (receiver != null) {
|
||||||
Bundle resultData = new Bundle();
|
Bundle resultData = new Bundle();
|
||||||
if (message != null && message.length() > 0)
|
if (message != null && message.length() > 0)
|
||||||
resultData.putString(RESULT_MESSAGE, message);
|
resultData.putString(RESULT_MESSAGE, message);
|
||||||
|
if (event == null)
|
||||||
|
event = new Event(statusCode);
|
||||||
|
resultData.putParcelable(RESULT_EVENT, event);
|
||||||
receiver.send(statusCode, resultData);
|
receiver.send(statusCode, resultData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,56 +238,50 @@ public class UpdateService extends IntentService implements ProgressListener {
|
|||||||
// database while we do all the downloading, etc...
|
// database while we do all the downloading, etc...
|
||||||
int updates = 0;
|
int updates = 0;
|
||||||
List<DB.Repo> repos;
|
List<DB.Repo> repos;
|
||||||
|
List<DB.App> apps;
|
||||||
try {
|
try {
|
||||||
DB db = DB.getDB();
|
DB db = DB.getDB();
|
||||||
repos = db.getRepos();
|
repos = db.getRepos();
|
||||||
|
apps = db.getApps(false);
|
||||||
} finally {
|
} finally {
|
||||||
DB.releaseDB();
|
DB.releaseDB();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each repo...
|
// Process each repo...
|
||||||
List<DB.App> apps;
|
|
||||||
List<DB.App> updatingApps = new ArrayList<DB.App>();
|
List<DB.App> updatingApps = new ArrayList<DB.App>();
|
||||||
List<Integer> keeprepos = new ArrayList<Integer>();
|
List<Integer> keeprepos = new ArrayList<Integer>();
|
||||||
boolean success = true;
|
boolean success = true;
|
||||||
boolean changes = false;
|
boolean changes = false;
|
||||||
for (DB.Repo repo : repos) {
|
for (DB.Repo repo : repos) {
|
||||||
if (repo.inuse) {
|
if (!repo.inuse) {
|
||||||
|
continue;
|
||||||
sendStatus(
|
}
|
||||||
STATUS_INFO,
|
sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address));
|
||||||
getString(R.string.status_connecting_to_repo,
|
RepoUpdater updater = RepoUpdater.createUpdaterFor(getBaseContext(), repo);
|
||||||
repo.address));
|
updater.setProgressListener(this);
|
||||||
|
try {
|
||||||
StringBuilder newetag = new StringBuilder();
|
updater.update();
|
||||||
String err = RepoXMLHandler.doUpdate(getBaseContext(),
|
if (updater.hasChanged()) {
|
||||||
repo, updatingApps, newetag, keeprepos, this);
|
updatingApps.addAll(updater.getApps());
|
||||||
if (err == null) {
|
changes = true;
|
||||||
String nt = newetag.toString();
|
|
||||||
if (!nt.equals(repo.lastetag)) {
|
|
||||||
repo.lastetag = newetag.toString();
|
|
||||||
changes = true;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
success = false;
|
keeprepos.add(repo.id);
|
||||||
err = "Update failed for " + repo.address + " - " + err;
|
|
||||||
Log.d("FDroid", err);
|
|
||||||
if (errmsg.length() == 0)
|
|
||||||
errmsg = err;
|
|
||||||
else
|
|
||||||
errmsg += "\n" + err;
|
|
||||||
}
|
}
|
||||||
|
} 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) {
|
if (!changes && success) {
|
||||||
Log.d("FDroid",
|
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) {
|
} else if (changes && success) {
|
||||||
|
|
||||||
sendStatus(STATUS_INFO,
|
sendStatus(STATUS_INFO,
|
||||||
getString(R.string.status_checking_compatibility));
|
getString(R.string.status_checking_compatibility));
|
||||||
apps = ((FDroidApp) getApplication()).getApps();
|
|
||||||
|
|
||||||
DB db = DB.getDB();
|
DB db = DB.getDB();
|
||||||
try {
|
try {
|
||||||
@ -231,7 +300,7 @@ public class UpdateService extends IntentService implements ProgressListener {
|
|||||||
}
|
}
|
||||||
if (keepapp) {
|
if (keepapp) {
|
||||||
DB.App app_k = null;
|
DB.App app_k = null;
|
||||||
for (DB.App app2 : updatingApps) {
|
for (DB.App app2 : apps) {
|
||||||
if (app2.id.equals(app.id)) {
|
if (app2.id.equals(app.id)) {
|
||||||
app_k = app2;
|
app_k = app2;
|
||||||
break;
|
break;
|
||||||
@ -319,9 +388,9 @@ public class UpdateService extends IntentService implements ProgressListener {
|
|||||||
e.putLong("lastUpdateCheck", System.currentTimeMillis());
|
e.putLong("lastUpdateCheck", System.currentTimeMillis());
|
||||||
e.commit();
|
e.commit();
|
||||||
if (changes) {
|
if (changes) {
|
||||||
sendStatus(STATUS_CHANGES);
|
sendStatus(STATUS_COMPLETE_WITH_CHANGES);
|
||||||
} else {
|
} else {
|
||||||
sendStatus(STATUS_SAME);
|
sendStatus(STATUS_COMPLETE_AND_SAME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,23 +416,17 @@ public class UpdateService extends IntentService implements ProgressListener {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onProgress(ProgressListener.Event event) {
|
public void onProgress(ProgressListener.Event event) {
|
||||||
|
|
||||||
String message = "";
|
String message = "";
|
||||||
if (event.type == RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD) {
|
if (event.type == RepoUpdater.PROGRESS_TYPE_DOWNLOAD) {
|
||||||
String repoAddress = event.data
|
String repoAddress = event.data.getString(RepoUpdater.PROGRESS_DATA_REPO);
|
||||||
.getString(RepoXMLHandler.PROGRESS_DATA_REPO);
|
String downloadedSize = Utils.getFriendlySize( event.progress );
|
||||||
String downloadedSize = Utils.getFriendlySize(event.progress);
|
String totalSize = Utils.getFriendlySize( event.total );
|
||||||
String totalSize = Utils.getFriendlySize(event.total);
|
int percent = (int)((double)event.progress/event.total * 100);
|
||||||
int percent = (int) ((double) event.progress / event.total * 100);
|
message = getString(R.string.status_download, repoAddress, downloadedSize, totalSize, percent);
|
||||||
message = getString(R.string.status_download, repoAddress,
|
} else if (event.type == RepoUpdater.PROGRESS_TYPE_PROCESS_XML) {
|
||||||
downloadedSize, totalSize, percent);
|
String repoAddress = event.data.getString(RepoUpdater.PROGRESS_DATA_REPO);
|
||||||
} else if (event.type == RepoXMLHandler.PROGRESS_TYPE_PROCESS_XML) {
|
message = getString(R.string.status_processing_xml, repoAddress, event.progress, event.total);
|
||||||
String repoAddress = event.data
|
|
||||||
.getString(RepoXMLHandler.PROGRESS_DATA_REPO);
|
|
||||||
message = getString(R.string.status_processing_xml, repoAddress,
|
|
||||||
event.progress, event.total);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sendStatus(STATUS_INFO, message);
|
sendStatus(STATUS_INFO, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,9 @@
|
|||||||
|
|
||||||
package org.fdroid.fdroid;
|
package org.fdroid.fdroid;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -25,6 +28,10 @@ import java.io.FileReader;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.util.Formatter;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
@ -37,6 +44,10 @@ public final class Utils {
|
|||||||
private static final String[] FRIENDLY_SIZE_FORMAT = {
|
private static final String[] FRIENDLY_SIZE_FORMAT = {
|
||||||
"%.0f B", "%.0f KiB", "%.1f MiB", "%.2f GiB" };
|
"%.0f B", "%.0f KiB", "%.1f MiB", "%.2f GiB" };
|
||||||
|
|
||||||
|
public static final SimpleDateFormat LOG_DATE_FORMAT =
|
||||||
|
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static void copy(InputStream input, OutputStream output)
|
public static void copy(InputStream input, OutputStream output)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
@ -148,6 +159,36 @@ public final class Utils {
|
|||||||
return count;
|
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) {
|
public static File getApkCacheDir(Context context) {
|
||||||
File apkCacheDir = new File(
|
File apkCacheDir = new File(
|
||||||
StorageUtils.getCacheDirectory(context, true), "apks");
|
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;
|
return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
static class ViewHolder {
|
private static class ViewHolder {
|
||||||
TextView name;
|
TextView name;
|
||||||
TextView summary;
|
TextView summary;
|
||||||
TextView status;
|
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
|
## 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
|
## 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//./\/}
|
FDROID_PATH=${FDROID_PACKAGE//./\/}
|
||||||
|
|
||||||
mkdir -p "src/${FDROID_PATH}"
|
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"
|
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
|
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}/
|
mv src/org/fdroid/fdroid/* src/${FDROID_PATH}/
|
||||||
rm -rf src/org/fdroid/fdroid/
|
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"
|
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