Merge master into super-fdroid

This commit is contained in:
Dominik Schürmann 2014-05-11 00:38:53 +02:00
commit 919f9c63b8
76 changed files with 3799 additions and 278 deletions

View File

@ -4,6 +4,7 @@
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="extern/nanohttpd/core/src/main/java"/>
<classpathentry kind="src" path="gen"/>
<classpathentry combineaccessrules="false" kind="src" path="/AndroidPinning"/>
<classpathentry combineaccessrules="false" kind="src" path="/MemorizingActivity"/>

4
.gitmodules vendored
View File

@ -10,6 +10,10 @@
path = extern/AndroidPinning
url = http://gitlab.doeg.gy/cpu/androidpinning.git
ignore = dirty
[submodule "extern/nanohttpd"]
path = extern/nanohttpd
url = https://github.com/eighthave/nanohttpd
ignore = dirty
[submodule "extern/libsuperuser"]
path = extern/libsuperuser
url = https://github.com/dschuermann/libsuperuser.git

View File

@ -3,8 +3,8 @@
xmlns:tools="http://schemas.android.com/tools"
package="org.fdroid.fdroid"
android:installLocation="auto"
android:versionCode="640"
android:versionName="0.64-test" >
android:versionCode="670"
android:versionName="0.67-test" >
<uses-sdk
android:minSdkVersion="5"
@ -34,6 +34,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@ -191,6 +192,29 @@
<activity
android:name=".NfcNotEnabledActivity"
android:noHistory="true" />
<activity android:name=".views.QrWizardDownloadActivity" />
<activity android:name=".views.QrWizardWifiNetworkActivity" />
<activity
android:name=".views.LocalRepoActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/local_repo"
android:launchMode="singleTop"
android:parentActivityName=".FDroid"
android:screenOrientation="portrait" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".views.SelectLocalAppsActivity"
android:label="@string/setup_repo"
android:parentActivityName=".views.LocalRepoActivity" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".views.LocalRepoActivity" />
</activity>
<activity
android:name=".views.RepoDetailsActivity"
android:label="@string/menu_manage"
@ -301,7 +325,7 @@
</receiver>
<receiver android:name=".PackageUpgradedReceiver" >
<intent-filter>
<action android:name="android.intent.action.PACKAGE_CHANGED" />
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data android:scheme="package" />
</intent-filter>
@ -313,8 +337,15 @@
<data android:scheme="package" />
</intent-filter>
</receiver>
<receiver android:name=".WifiStateChangeReceiver" >
<intent-filter>
<action android:name="android.net.wifi.STATE_CHANGE" />
</intent-filter>
</receiver>
<service android:name=".UpdateService" />
<service android:name=".net.WifiStateChangeService" />
<service android:name=".localrepo.LocalRepoService" />
</application>
</manifest>

View File

@ -1,4 +1,4 @@
### Upcoming release
### 0.66 (2014-05-01)
* Fix crash on startup for devices with more than 500 installed apps
@ -12,7 +12,12 @@
* Keep track of installed apps internally, rather than asking Android each time
* Fix some crashes
* Security fixes and updates for adding of repos
* Fix bug introduced in 0.63 which made F-Droid always omit density-specific
icons, making icons blurry on high-res devices
* Fix some other crashes
* Translation updates

View File

@ -31,10 +31,10 @@ Add the following lines to your repo manifest:
<remote name="fdroid" fetch="https://git.gitorious.org/f-droid" />
<remote name="github" fetch="https://github.com/" />
<project path="packages/apps/fdroidclient" name="fdroidclient.git" remote="fdroid" revision="0.62" />
<project path="packages/apps/fdroidclient" name="fdroidclient.git" remote="fdroid" revision="0.66" />
<project path="packages/apps/fdroidclient/extern/UniversalImageLoader" name="nostra13/Android-Universal-Image-Loader" remote="github" revision="b1b49e51f2c43b119edca44691daf9ab6c751158" />
<project path="packages/apps/fdroidclient/extern/AndroidPinning" name="binaryparadox/AndroidPinning" remote="github" revision="ce84a19e753bbcc3304525f763edb7d7f3b62429" />
<project path="packages/apps/fdroidclient/extern/UniversalImageLoader" name="nostra13/Android-Universal-Image-Loader" remote="github" revision="ee50fd1ce77d866a89374a5ff0886be6e179feb2" />
<project path="packages/apps/fdroidclient/extern/AndroidPinning" name="binaryparadox/AndroidPinning" remote="github" revision="a0d713c6162b7016a3c3f55bcaefcdca4acacebd" />
<project path="packages/apps/fdroidclient/extern/MemorizingTrustManager" name="ge0rg/MemorizingTrustManager" remote="github" revision="a705441ac53b9e1aba9f00f3f59aab81da6fbc9e" />
```

View File

@ -3,6 +3,7 @@
android update lib-project --path extern/UniversalImageLoader/library
android update lib-project --path extern/AndroidPinning
android update lib-project --path extern/MemorizingTrustManager
android update lib-project --path extern/nanohttpd
android update lib-project --path extern/libsuperuser/libsuperuser
android update project --path . --name F-Droid

View File

@ -0,0 +1,65 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>{{REPO_URL}} local FDroid repo</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
<meta http-equiv="content-type" content="text/html;charset=utf-8">
</head>
<body>
<style>
body {
padding: 0;
margin: 0;
font-family: "Trebuchet MS", Helvetica, sans-serif;
color: #444;
}
h1 {
margin: 0 auto;
padding-top: 10px;
text-align: center;
}
ol {
counter-reset:li;
margin-left:0;
padding-left:0;
}
ol > li {
position: relative;
padding-left: 35px;
padding-top: 20px;
border-bottom: solid 1px #333;
height: 4em;
box-sizing: border-box;
}
ol > li:first-child {
border-top: solid 1px #333;
}
ol > li:before {
content: counter(li);
counter-increment: li;
position: absolute;
left: 5px;
top: 10px;
font: bold 2em Sans-Serif;
}
</style>
<h1>Kerplapp Bootstrap</h1>
<ol>
<li><del>Find a Kerplapp Repo</del></li>
<li><a href="{{CLIENT_URL}}">Download F-Droid client</a></li>
<li>Install F-Droid client</li>
<li><a href="{{REPO_URL}}">Add Kerplapp Repo to F-Droid client</a></li>
<li>Kerplapp an App!</li>
</ol>
</body>
</html>

1
extern/nanohttpd vendored Submodule

@ -0,0 +1 @@
Subproject commit 73cb37f863c41b8f021019703d6453887b656dd6

BIN
libs/core-3.0.1.jar Normal file

Binary file not shown.

View File

@ -0,0 +1,17 @@
zxing
-----
ZXing ("zebra crossing") is an open-source, multi-format 1D/2D barcode image
processing library implemented in Java, with ports to other languages.
https://github.com/zxing/zxing
Building zxing from scratch is a massive pain, so we use the official jar.
The main source repo is SVN, so we couldn't do a git submodule anyway.
The releases should be signed by this key:
Sean Owen (ZXing) <srowen@gmail.com>
CE32 85F3 2068 5193 D11F EA01 F6CE 9695 C931 8406
http://central.maven.org/maven2/com/google/zxing/core/3.0.1/core-3.0.1.jar
http://central.maven.org/maven2/com/google/zxing/core/3.0.1/core-3.0.1.jar.asc

View File

@ -5,4 +5,5 @@ target=android-19
android.library.reference.1=extern/UniversalImageLoader/library
android.library.reference.2=extern/MemorizingTrustManager
android.library.reference.3=extern/AndroidPinning
android.library.reference.4=extern/libsuperuser/libsuperuser
android.library.reference.4=extern/nanohttpd
android.library.reference.5=extern/libsuperuser/libsuperuser

View File

@ -57,6 +57,7 @@
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:gravity="start"
android:textAlignment="viewStart"
android:layout_toLeftOf="@id/license"
android:layout_toStartOf="@id/license" />
@ -83,6 +84,7 @@
android:textSize="12sp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:gravity="start"
android:textAlignment="viewStart"
android:layout_toLeftOf="@id/categories"
android:layout_toStartOf="@id/categories"

View File

@ -43,6 +43,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="start"
android:textAlignment="viewStart"
/>
@ -56,6 +57,7 @@
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_gravity="center_vertical"
android:gravity="end"
android:textAlignment="viewEnd"
/>
@ -76,6 +78,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="start"
android:textAlignment="viewStart"
/>
@ -89,6 +92,7 @@
android:layout_marginLeft="6sp"
android:layout_marginStart="6sp"
android:layout_gravity="center_vertical"
android:gravity="end"
android:textAlignment="viewEnd"
/>

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<Button
android:id="@+id/enable_wifi"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/enable_wifi" />
<CheckBox
android:id="@+id/repoSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="15dp"
android:text="@string/touch_to_turn_on_local_repo" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/sharing_uri" />
<TextView
android:id="@+id/sharing_uri"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="15dp"
android:layout_marginStart="15dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold"
android:typeface="monospace" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/wifi_network" />
<TextView
android:id="@+id/wifi_network"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="15dp"
android:layout_marginStart="15dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold"
android:typeface="monospace" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/fingerprint" />
<TextView
android:id="@+id/fingerprint"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="15dp"
android:layout_marginStart="15dp"
android:typeface="monospace" />
</LinearLayout>
<TextView
android:id="@+id/instrucionsTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginStart="20dp"
android:layout_marginRight="20dp"
android:layout_marginEnd="20dp"
android:text="@string/same_wifi_instructions" />
<ImageView
android:id="@+id/repoQrCode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/qr_content_description" />
</LinearLayout>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/qrWizardInstructions"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/wifi_network" />
<TextView
android:id="@+id/qrWifiNetworkName"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="15dp"
android:layout_marginStart="15dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold"
android:typeface="monospace" />
</LinearLayout>
<ImageView
android:id="@+id/qrWizardImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/qr_code" />
<Button
android:id="@+id/qrNextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/next" />
</LinearLayout>

View File

@ -0,0 +1,12 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<fragment
android:id="@+id/fragment_app_list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
class="org.fdroid.fdroid.views.fragments.SelectLocalAppsFragment" />
</RelativeLayout>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2010 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/activatedBackgroundIndicator"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingBottom="2dip"
android:paddingTop="2dip"
tools:ignore="NewApi" >
<!-- TODO remove NewApi ignore when appcompat-v7 is in place -->
<ImageView
android:layout_width="48dip"
android:layout_height="48dip"
android:layout_marginLeft="?android:attr/listPreferredItemPaddingStart"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:layout_marginTop="6dip"
tools:ignore="ContentDescription" />
<TwoLineListItem
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:mode="twoLine" >
<TextView
android:id="@+id/application_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="?android:attr/listPreferredItemPaddingStart"
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
android:layout_marginTop="6dip"
android:textAppearance="?android:attr/textAppearanceListItem" />
<TextView
android:id="@+id/package_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignStart="@+id/application_label"
android:layout_below="@+id/application_label"
android:textAppearance="?android:attr/textAppearanceSmall" />
</TwoLineListItem>
</LinearLayout>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/menu_setup_repo"
android:icon="@android:drawable/ic_input_add"
android:showAsAction="ifRoom|withText"
android:title="@string/setup_repo"/>
<item
android:id="@+id/menu_send_fdroid_via_wifi"
android:icon="@android:drawable/arrow_up_float"
android:showAsAction="never"
android:title="@string/send_fdroid_via_wifi"/>
<item
android:id="@+id/menu_settings"
android:icon="@android:drawable/ic_menu_preferences"
android:showAsAction="never"
android:title="@string/menu_preferences"/>
</menu>

View File

@ -0,0 +1,14 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/action_search"
android:icon="@android:drawable/ic_menu_search"
android:showAsAction="ifRoom"
android:title="@string/menu_search"/>
<item
android:id="@+id/action_update_repo"
android:icon="@android:drawable/ic_input_add"
android:showAsAction="always|withText"
android:title="@string/update_repo"/>
</menu>

View File

@ -0,0 +1,15 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/action_search"
android:actionViewClass="android.widget.SearchView"
android:icon="@android:drawable/ic_menu_search"
android:showAsAction="collapseActionView|ifRoom"
android:title="@string/menu_search"/>
<item
android:id="@+id/action_settings"
android:icon="@android:drawable/ic_menu_preferences"
android:showAsAction="never"
android:title="@string/menu_preferences"/>
</menu>

View File

@ -10,7 +10,7 @@
<string name="edit">Éditer</string>
<string name="delete">Supprimer</string>
<string name="enable_nfc_send">Activer l\'envoi NFC…</string>
<string name="cache_downloaded">Stocker les applications téléchargées sur l\'appareil</string>
<string name="cache_downloaded">Stockage d\'applications téléchargées</string>
<string name="cache_downloaded_on">Garder les fichiers apk téléchargés sur la carte SD</string>
<string name="cache_downloaded_off">Ne pas garder les fichiers apk</string>
<string name="updates">Mises à jour</string>
@ -34,7 +34,7 @@
<string name="about_desc">Originellement basée sur Aptoide.
Publiée sous licence GNU GPL v3.</string>
<string name="about_site">Site Web :</string>
<string name="about_mail">E-Mail:</string>
<string name="about_mail">Courriel:</string>
<string name="about_version">Version:</string>
<string name="about_website">Site Web</string>
<string name="no_repo">Aucun dépôt n\'est configuré !
@ -42,8 +42,8 @@ Publiée sous licence GNU GPL v3.</string>
Un dépôt est une source d\'applications. Pour en ajouter un, appuyez maintenant sur le bouton MENU et entrez l\'adresse URL.
L\'URL d\'un dépôt ressemble à ceci : https://f-droid.org/repo</string>
<string name="inst">Installée</string>
<string name="not_inst">Pas installée</string>
<string name="inst">Installées</string>
<string name="not_inst">Non installée</string>
<string name="added_on">Ajouté le %s</string>
<string name="ok">OK</string>
<string name="yes">Oui</string>
@ -56,9 +56,9 @@ L\'URL d\'un dépôt ressemble à ceci : https://f-droid.org/repo</string>
<string name="overwrite">Écraser</string>
<string name="repo_delete_title">Choisissez le dépôt à supprimer</string>
<string name="repo_update_title">Mettre à jour les dépôts</string>
<string name="tab_noninstalled">Disponible</string>
<string name="tab_noninstalled">Disponibles</string>
<string name="tab_updates">Mises à jour</string>
<string name="one_update_available">1 mise à jour est disponible.</string>
<string name="one_update_available">Une mise à jour est disponible.</string>
<string name="many_updates_available">%d mises à jour sont disponibles.</string>
<string name="fdroid_updates_available">Des mises à jour F-Droid sont disponibles</string>
<string name="process_wait_title">Patientez</string>
@ -82,7 +82,7 @@ Voulez-vous les mettre à jour ?</string>
<string name="menu_manage">Gestion de dépôts</string>
<string name="menu_send_apk_bt">Bluetooth FDroid.apk…</string>
<string name="menu_preferences">Préférences</string>
<string name="menu_about">A propos</string>
<string name="menu_about">À propos</string>
<string name="menu_search">Rechercher</string>
<string name="menu_add_repo">Nouveau dépôt</string>
<string name="menu_rem_repo">Supprimer un dépôt</string>
@ -113,7 +113,7 @@ Voulez-vous les mettre à jour ?</string>
<string name="expert_on">Afficher plus d\'infos et activer des paramètres supplémentaires</string>
<string name="expert_off">Cacher des extras pour les utilisateurs avancés</string>
<string name="search_hint">Rechercher des applications</string>
<string name="appcompatibility">Compatibilité de l\'application</string>
<string name="appcompatibility">Compatibilité des applications</string>
<string name="show_incompat_versions">Versions incompatibles</string>
<string name="show_incompat_versions_on">Afficher les versions des applications incompatibles avec l\'appareil</string>
<string name="show_incompat_versions_off">Cacher les applications incompatibles avec l\'appareil</string>
@ -136,11 +136,11 @@ Voulez-vous les mettre à jour ?</string>
%1$s</string>
<string name="status_connecting_to_repo">Connexion à
%1$s</string>
<string name="status_checking_compatibility">Vérification de la compatibilité des applis avec votre appareil…</string>
<string name="status_checking_compatibility">Vérification de la compatibilité des applications avec votre appareil…</string>
<string name="status_inserting">Sauvegarder les détails de l\'application (%1$d%%)</string>
<string name="no_permissions">Aucune autorisation n\'est utilisée.</string>
<string name="permissions_for_long">Autorisations pour la version %s</string>
<string name="showPermissions">Afficher les autorisations</string>
<string name="permissions_for_long">Permissions pour la version %s</string>
<string name="showPermissions">Afficher les permissions</string>
<string name="showPermissions_on">Afficher la liste des permissions qu\'une app requiert</string>
<string name="showPermissions_off">Ne pas afficher les permissions avant le téléchargement</string>
<string name="no_handler_app">Vous n\'avez aucune application installée pour gérer %s</string>

View File

@ -9,6 +9,7 @@
<string name="version">Versie</string>
<string name="edit">Bewerken</string>
<string name="delete">Verwijderen</string>
<string name="enable_nfc_send">Versturen via NFC aanzetten…</string>
<string name="cache_downloaded">buffer gedownloade apps</string>
<string name="cache_downloaded_on">Gedownloade apk-files bewaren op SD card</string>
<string name="cache_downloaded_off">apk-files niet bewaren</string>
@ -26,7 +27,6 @@
<string name="about_desc">Gebaseerd op Aptoide.
Uitgegeven onder de GNU GPLv3 licentie</string>
<string name="about_site">Website:</string>
<string name="about_mail">Email:</string>
<string name="about_version">Versie:</string>
<string name="about_website">Website</string>
<string name="no_repo">U hebt geen bronnen geconfigureerd!
@ -57,11 +57,17 @@ Een bron-adres ziet er ongeveer
<string name="process_wait_title">Even geduld aub</string>
<string name="process_update_msg">Applicatie-lijst vernieuwen…</string>
<string name="download_server">downloaden applicatie van</string>
<string name="nfc_is_not_enabled">NFC is niet ingeschakeld!</string>
<string name="go_to_nfc_settings">Ga naar NFC instellingen…</string>
<string name="bluetooth_activity_not_found">Geen Bluetooth verzendmethode gevonden, kies er een!</string>
<string name="choose_bt_send">Kies Bluetooth verzendmethode</string>
<string name="repo_add_url">Bron-adres</string>
<string name="malformed_repo_uri">Misvormde bron-URI %s genegeerd</string>
<string name="repo_alrt">De lijst van gebruikte bronnen is veranderd.
Wilt u ze vernieuwen?</string>
<string name="menu_update_repo">Vernieuw Bronnen</string>
<string name="menu_manage">Beheer bronnen</string>
<string name="menu_send_apk_bt">Bluetooth FDroid.apk…</string>
<string name="menu_preferences">Voorkeuren</string>
<string name="menu_about">Over</string>
<string name="menu_search">Zoeken</string>
@ -99,6 +105,7 @@ Wilt u ze vernieuwen?</string>
<string name="category_whatsnew">Wat is nieuw</string>
<string name="category_recentlyupdated">Recentelijk vernieuwd</string>
<string name="local_repos_title">Lokale FDroid opslagplaatsen</string>
<string name="local_repos_scanning">Lokale FDroid-bronnen ontdekken…</string>
<string name="status_download">Downloaden
%2$s / %3$s (%4$d%%) van
%1$s</string>
@ -107,6 +114,7 @@ Wilt u ze vernieuwen?</string>
%1$s</string>
<string name="status_connecting_to_repo">Verbinden met %1$s</string>
<string name="status_checking_compatibility">Controleer app compatibiliteit met uw apparaat…</string>
<string name="status_inserting">Applicatiedetails opslaan (%1$d%%)</string>
<string name="no_permissions">Geen permissies worden gebruikt</string>
<string name="permissions_for_long">Permissies voor versie %s</string>
<string name="showPermissions">Laat permissies zien</string>
@ -115,16 +123,20 @@ Wilt u ze vernieuwen?</string>
<string name="theme">Thema</string>
<string name="repo_url">URL</string>
<string name="repo_num_apps">Aantal apps</string>
<string name="repo_fingerprint">Vingerafdruk van bronondertekensleutel (SHA-256)</string>
<string name="repo_description">Beschrijving</string>
<string name="repo_last_update">Meest recente update</string>
<string name="repo_name">Naam</string>
<string name="repo_not_yet_updated">Deze opslag is nog niet eerder gebruikt. Om de leverbare apps te zien moet u het updaten.
Zodra de update gedaan is ziet u hier de beschrijving en andere details.</string>
<string name="repo_delete_details">Weet u zeker dat u de opslag genaamd \"{0}\" met daarin \"{1}\" apps wilt verwijderen? Reeds geïnstalleerde apps blijven behouden, maar kunnen niet meer worden geüpdate via F-Droid.</string>
<string name="unknown">Onbekend</string>
<string name="repo_confirm_delete_body">Het verwijderen van een opslag betekent dat apps hierop niet meer beschikbaar zijn voor F-Droid.
Noot: Eerder geïnstalleerde apps blijven op uw apparaat.</string>
<string name="repo_disabled_notification">\"%1$s\" uitgeschakeld. U moet deze weer activeren mocht u apps vanuit deze opslag willen installeren.</string>
<string name="up_to_maxsdk">tot maximaal %s</string>
<string name="minsdk_up_to_maxsdk">%1$s tot %2$s</string>
<string name="not_on_same_wifi">Uw apparaat zit niet op hetzelfde WiFi-netwerk als de lokale bron die u zojuist heeft toegevoegd! Probeer om bij netwerk %s aan te melden</string>
<string name="requires_features">Benodigd: %1$s</string>
</resources>

View File

@ -2,4 +2,5 @@
<resources>
<color name="signed">#ffcccccc</color>
<color name="unsigned">#ffCC0000</color>
<color name="unverified">#ff999999</color>
</resources>

View File

@ -2,7 +2,7 @@
<resources>
<string name="app_name">F-Droid</string>
<string name="version_name">0.64-test</string>
<string name="version_name">0.67-test</string>
<string name="about_sitec">https://f-droid.org</string>
<string name="about_mailc">team@f-droid.org</string>

View File

@ -153,8 +153,36 @@
<string name="category_whatsnew">What\'s New</string>
<string name="category_recentlyupdated">Recently Updated</string>
<string name="local_repo">Local Repo</string>
<string name="local_repos_title">Local FDroid Repos</string>
<string name="local_repos_scanning">Discovering local FDroid repos&#8230;</string>
<string name="local_repo_running">Your local FDroid repo is accessible.</string>
<string name="setup_repo">Setup Local Repo</string>
<string name="touch_to_configure_local_repo">Touch to setup your local repo.</string>
<string name="touch_to_turn_on_local_repo">Touch to turn on your local repo.</string>
<string name="touch_to_turn_off_local_repo">Touch to turn off your local repo.</string>
<string name="updating">Updating&#8230;</string>
<string name="update_repo">Update Repo</string>
<string name="deleting_repo">Deleting current repo&#8230;</string>
<string name="adding_apks_format">Adding %s to repo&#8230;</string>
<string name="writing_index_xml">Writing raw index file (index.xml)&#8230;</string>
<string name="linking_apks">Linking APKs into the repo&#8230;</string>
<string name="copying_icons">Copying app icons into the repo&#8230;</string>
<string name="updated_local_repo">Finished updating local repo</string>
<string name="no_applications_found">No applications found</string>
<string name="icon">icon</string>
<string name="fingerprint">Fingerprint:</string>
<string name="wifi_network">WiFi Network:</string>
<string name="sharing_uri">Sharing URL:</string>
<string name="enable_wifi">Enable WiFi</string>
<string name="enabling_wifi">Enabling WiFi&#8230;</string>
<string name="same_wifi_instructions">To connect to other people\'s devices, make sure both devices are on the same WiFi network. Then either type the URL above into F-Droid, or scan this QR Code:</string>
<string name="qr_code">QR Code</string>
<string name="next">Next</string>
<string name="qr_content_description">QR Code of repo URL</string>
<string name="qr_wizard_wifi_network_instructions">Scan this QR Code to connect to the same WiFi network as this device.</string>
<string name="qr_wizard_download_instructions">Scan this QR Code to connect to the website for getting started.</string>
<string name="send_fdroid_via_wifi">Send FDroid via WiFi&#8230;</string>
<!--
status_download takes four parameters:
@ -179,6 +207,7 @@
<string name="compactlayout_off">Show icons at regular size</string>
<string name="theme">Theme</string>
<string name="unsigned">Unsigned</string>
<string name="unverified">Unverified</string>
<string name="repo_url">URL</string>
<string name="repo_num_apps">Number of apps</string>
<string name="repo_fingerprint">Fingerprint of Repo Signing Key (SHA-256)</string>

View File

@ -0,0 +1,114 @@
/*
* Copyright (C) 2008 ZXing authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.zxing.encode;
import android.provider.ContactsContract;
/**
* The set of constants to use when sending Barcode Scanner an Intent which requests a barcode
* to be encoded.
*
* @author dswitkin@google.com (Daniel Switkin)
*/
public final class Contents {
private Contents() {
}
public static final class Type {
/**
* Plain text. Use Intent.putExtra(DATA, string). This can be used for URLs too, but string
* must include "http://" or "https://".
*/
public static final String TEXT = "TEXT_TYPE";
/**
* An email type. Use Intent.putExtra(DATA, string) where string is the email address.
*/
public static final String EMAIL = "EMAIL_TYPE";
/**
* Use Intent.putExtra(DATA, string) where string is the phone number to call.
*/
public static final String PHONE = "PHONE_TYPE";
/**
* An SMS type. Use Intent.putExtra(DATA, string) where string is the number to SMS.
*/
public static final String SMS = "SMS_TYPE";
/**
* A contact. Send a request to encode it as follows:
* <p/>
* import android.provider.Contacts;
* <p/>
* Intent intent = new Intent(Intents.Encode.ACTION);
* intent.putExtra(Intents.Encode.TYPE, CONTACT);
* Bundle bundle = new Bundle();
* bundle.putString(Contacts.Intents.Insert.NAME, "Jenny");
* bundle.putString(Contacts.Intents.Insert.PHONE, "8675309");
* bundle.putString(Contacts.Intents.Insert.EMAIL, "jenny@the80s.com");
* bundle.putString(Contacts.Intents.Insert.POSTAL, "123 Fake St. San Francisco, CA 94102");
* intent.putExtra(Intents.Encode.DATA, bundle);
*/
public static final String CONTACT = "CONTACT_TYPE";
/**
* A geographic location. Use as follows:
* Bundle bundle = new Bundle();
* bundle.putFloat("LAT", latitude);
* bundle.putFloat("LONG", longitude);
* intent.putExtra(Intents.Encode.DATA, bundle);
*/
public static final String LOCATION = "LOCATION_TYPE";
private Type() {
}
}
public static final String URL_KEY = "URL_KEY";
public static final String NOTE_KEY = "NOTE_KEY";
/**
* When using Type.CONTACT, these arrays provide the keys for adding or retrieving multiple
* phone numbers and addresses.
*/
public static final String[] PHONE_KEYS = {
ContactsContract.Intents.Insert.PHONE,
ContactsContract.Intents.Insert.SECONDARY_PHONE,
ContactsContract.Intents.Insert.TERTIARY_PHONE
};
public static final String[] PHONE_TYPE_KEYS = {
ContactsContract.Intents.Insert.PHONE_TYPE,
ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE,
ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE
};
public static final String[] EMAIL_KEYS = {
ContactsContract.Intents.Insert.EMAIL,
ContactsContract.Intents.Insert.SECONDARY_EMAIL,
ContactsContract.Intents.Insert.TERTIARY_EMAIL
};
public static final String[] EMAIL_TYPE_KEYS = {
ContactsContract.Intents.Insert.EMAIL_TYPE,
ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE,
ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE
};
}

View File

@ -0,0 +1,248 @@
/*
* Copyright (C) 2008 ZXing authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// from https://stackoverflow.com/questions/4782543/integration-zxing-library-directly-into-my-android-application
package com.google.zxing.encode;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.telephony.PhoneNumberUtils;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
public final class QRCodeEncoder {
private static final int WHITE = 0xFFFFFFFF;
private static final int BLACK = 0xFF000000;
private int dimension = Integer.MIN_VALUE;
private String contents = null;
private String displayContents = null;
private String title = null;
private BarcodeFormat format = null;
private boolean encoded = false;
public QRCodeEncoder(String data, Bundle bundle, String type, String format, int dimension) {
this.dimension = dimension;
encoded = encodeContents(data, bundle, type, format);
}
public String getContents() {
return contents;
}
public String getDisplayContents() {
return displayContents;
}
public String getTitle() {
return title;
}
private boolean encodeContents(String data, Bundle bundle, String type, String formatString) {
// Default to QR_CODE if no format given.
format = null;
if (formatString != null) {
try {
format = BarcodeFormat.valueOf(formatString);
} catch (IllegalArgumentException iae) {
// Ignore it then
}
}
if (format == null || format == BarcodeFormat.QR_CODE) {
this.format = BarcodeFormat.QR_CODE;
encodeQRCodeContents(data, bundle, type);
} else if (data != null && data.length() > 0) {
contents = data;
displayContents = data;
title = "Text";
}
return contents != null && contents.length() > 0;
}
private void encodeQRCodeContents(String data, Bundle bundle, String type) {
if (type.equals(Contents.Type.TEXT)) {
if (data != null && data.length() > 0) {
contents = data;
displayContents = data;
title = "Text";
}
} else if (type.equals(Contents.Type.EMAIL)) {
data = trim(data);
if (data != null) {
contents = "mailto:" + data;
displayContents = data;
title = "E-Mail";
}
} else if (type.equals(Contents.Type.PHONE)) {
data = trim(data);
if (data != null) {
contents = "tel:" + data;
displayContents = PhoneNumberUtils.formatNumber(data);
title = "Phone";
}
} else if (type.equals(Contents.Type.SMS)) {
data = trim(data);
if (data != null) {
contents = "sms:" + data;
displayContents = PhoneNumberUtils.formatNumber(data);
title = "SMS";
}
} else if (type.equals(Contents.Type.CONTACT)) {
if (bundle != null) {
StringBuilder newContents = new StringBuilder(100);
StringBuilder newDisplayContents = new StringBuilder(100);
newContents.append("MECARD:");
String name = trim(bundle.getString(ContactsContract.Intents.Insert.NAME));
if (name != null) {
newContents.append("N:").append(escapeMECARD(name)).append(';');
newDisplayContents.append(name);
}
String address = trim(bundle.getString(ContactsContract.Intents.Insert.POSTAL));
if (address != null) {
newContents.append("ADR:").append(escapeMECARD(address)).append(';');
newDisplayContents.append('\n').append(address);
}
Collection<String> uniquePhones = new HashSet<String>(Contents.PHONE_KEYS.length);
for (int x = 0; x < Contents.PHONE_KEYS.length; x++) {
String phone = trim(bundle.getString(Contents.PHONE_KEYS[x]));
if (phone != null) {
uniquePhones.add(phone);
}
}
for (String phone : uniquePhones) {
newContents.append("TEL:").append(escapeMECARD(phone)).append(';');
newDisplayContents.append('\n').append(PhoneNumberUtils.formatNumber(phone));
}
Collection<String> uniqueEmails = new HashSet<String>(Contents.EMAIL_KEYS.length);
for (int x = 0; x < Contents.EMAIL_KEYS.length; x++) {
String email = trim(bundle.getString(Contents.EMAIL_KEYS[x]));
if (email != null) {
uniqueEmails.add(email);
}
}
for (String email : uniqueEmails) {
newContents.append("EMAIL:").append(escapeMECARD(email)).append(';');
newDisplayContents.append('\n').append(email);
}
String url = trim(bundle.getString(Contents.URL_KEY));
if (url != null) {
// escapeMECARD(url) -> wrong escape e.g. http\://zxing.google.com
newContents.append("URL:").append(url).append(';');
newDisplayContents.append('\n').append(url);
}
String note = trim(bundle.getString(Contents.NOTE_KEY));
if (note != null) {
newContents.append("NOTE:").append(escapeMECARD(note)).append(';');
newDisplayContents.append('\n').append(note);
}
// Make sure we've encoded at least one field.
if (newDisplayContents.length() > 0) {
newContents.append(';');
contents = newContents.toString();
displayContents = newDisplayContents.toString();
title = "Contact";
} else {
contents = null;
displayContents = null;
}
}
} else if (type.equals(Contents.Type.LOCATION)) {
if (bundle != null) {
// These must use Bundle.getFloat(), not getDouble(), it's part of the API.
float latitude = bundle.getFloat("LAT", Float.MAX_VALUE);
float longitude = bundle.getFloat("LONG", Float.MAX_VALUE);
if (latitude != Float.MAX_VALUE && longitude != Float.MAX_VALUE) {
contents = "geo:" + latitude + ',' + longitude;
displayContents = latitude + "," + longitude;
title = "Location";
}
}
}
}
public Bitmap encodeAsBitmap() throws WriterException {
if (!encoded) return null;
Map<EncodeHintType, Object> hints = null;
String encoding = guessAppropriateEncoding(contents);
if (encoding != null) {
hints = new EnumMap<EncodeHintType, Object>(EncodeHintType.class);
hints.put(EncodeHintType.CHARACTER_SET, encoding);
}
MultiFormatWriter writer = new MultiFormatWriter();
BitMatrix result = writer.encode(contents, format, dimension, dimension, hints);
int width = result.getWidth();
int height = result.getHeight();
int[] pixels = new int[width * height];
// All are 0, or black, by default
for (int y = 0; y < height; y++) {
int offset = y * width;
for (int x = 0; x < width; x++) {
pixels[offset + x] = result.get(x, y) ? BLACK : WHITE;
}
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
return bitmap;
}
private static String guessAppropriateEncoding(CharSequence contents) {
// Very crude at the moment
for (int i = 0; i < contents.length(); i++) {
if (contents.charAt(i) > 0xFF) { return "UTF-8"; }
}
return null;
}
private static String trim(String s) {
if (s == null) { return null; }
String result = s.trim();
return result.length() == 0 ? null : result;
}
private static String escapeMECARD(String input) {
if (input == null || (input.indexOf(':') < 0 && input.indexOf(';') < 0)) { return input; }
int length = input.length();
StringBuilder result = new StringBuilder(length);
for (int i = 0; i < length; i++) {
char c = input.charAt(i);
if (c == ':' || c == ';') {
result.append('\\');
}
result.append(c);
}
return result.toString();
}
}

View File

@ -19,20 +19,17 @@
package org.fdroid.fdroid;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.NotificationManager;
import android.bluetooth.BluetoothAdapter;
import android.content.*;
import android.content.pm.ApplicationInfo;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.database.ContentObserver;
import android.net.Uri;
import android.nfc.NfcAdapter;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
@ -49,6 +46,7 @@ import android.widget.TextView;
import org.fdroid.fdroid.compat.TabManager;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.views.AppListFragmentPageAdapter;
import org.fdroid.fdroid.views.LocalRepoActivity;
public class FDroid extends FragmentActivity {
@ -65,6 +63,7 @@ public class FDroid extends FragmentActivity {
private static final int ABOUT = Menu.FIRST + 3;
private static final int SEARCH = Menu.FIRST + 4;
private static final int BLUETOOTH_APK = Menu.FIRST + 5;
private static final int LOCAL_REPO = Menu.FIRST + 6;
private FDroidApp fdroidApp = null;
@ -135,6 +134,7 @@ public class FDroid extends FragmentActivity {
android.R.drawable.ic_menu_search);
if (fdroidApp.bluetoothAdapter != null) // ignore on devices without Bluetooth
menu.add(Menu.NONE, BLUETOOTH_APK, 3, R.string.menu_send_apk_bt);
menu.add(Menu.NONE, LOCAL_REPO, 4, R.string.local_repo);
menu.add(Menu.NONE, PREFERENCES, 4, R.string.menu_preferences).setIcon(
android.R.drawable.ic_menu_preferences);
menu.add(Menu.NONE, ABOUT, 5, R.string.menu_about).setIcon(
@ -162,6 +162,10 @@ public class FDroid extends FragmentActivity {
startActivityForResult(prefs, REQUEST_PREFS);
return true;
case LOCAL_REPO:
startActivity(new Intent(this, LocalRepoActivity.class));
return true;
case SEARCH:
onSearchRequested();
return true;

View File

@ -23,14 +23,22 @@ import android.app.Activity;
import android.app.Application;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;
@ -46,18 +54,40 @@ import de.duenndns.ssl.MemorizingTrustManager;
import org.fdroid.fdroid.compat.PRNGFixes;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.InstalledAppCacheUpdater;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.localrepo.LocalRepoManager;
import org.fdroid.fdroid.localrepo.LocalRepoService;
import org.fdroid.fdroid.net.WifiStateChangeService;
import org.thoughtcrime.ssl.pinning.PinningTrustManager;
import org.thoughtcrime.ssl.pinning.SystemKeyStore;
import javax.net.ssl.*;
import java.io.File;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.Set;
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;
public class FDroidApp extends Application {
// for the local repo on this device, all static since there is only one
public static int port = 8888;
public static String ipAddressString = null;
public static String ssid = "";
public static String bssid = "";
public static Repo repo = new Repo();
public static LocalRepoManager localRepo = null;
public static Set<String> selectedApps = null; // init in SelectLocalAppsFragment
private static Messenger localRepoServiceMessenger = null;
private static boolean localRepoServiceIsBound = false;
BluetoothAdapter bluetoothAdapter = null;
private static enum Theme {
@ -92,9 +122,11 @@ public class FDroidApp extends Application {
// it is more deterministic as to when this gets called...
Preferences.setup(this);
//Apply the Google PRNG fixes to properly seed SecureRandom
// Apply the Google PRNG fixes to properly seed SecureRandom
PRNGFixes.apply();
localRepo = new LocalRepoManager(getApplicationContext());
// Check that the installed app cache hasn't gotten out of sync somehow.
// e.g. if we crashed/ran out of battery half way through responding
// to a package installed intent. It doesn't really matter where
@ -153,7 +185,8 @@ public class FDroidApp extends Application {
// 30 days in secs: 30*24*60*60 = 2592000
2592000)
)
.threadPoolSize(Runtime.getRuntime().availableProcessors() * 2)
.threadPoolSize(4)
.threadPriority(Thread.NORM_PRIORITY - 2) // Default is NORM_PRIORITY - 1
.build();
ImageLoader.getInstance().init(config);
@ -194,6 +227,13 @@ public class FDroidApp extends Application {
} catch (KeyStoreException e) {
Log.e("FDroid", "Unable to set up trust manager chain. KeyStoreException");
}
// initialized the local repo information
WifiManager wifiManager = (WifiManager) getSystemService(WIFI_SERVICE);
int wifiState = wifiManager.getWifiState();
if (wifiState == WifiManager.WIFI_STATE_ENABLING
|| wifiState == WifiManager.WIFI_STATE_ENABLED)
startService(new Intent(this, WifiStateChangeService.class));
}
@TargetApi(18)
@ -247,4 +287,44 @@ public class FDroidApp extends Application {
activity.startActivity(sendBt);
}
}
private static ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
localRepoServiceMessenger = new Messenger(service);
}
@Override
public void onServiceDisconnected(ComponentName className) {
localRepoServiceMessenger = null;
}
};
public static void startLocalRepoService(Context context) {
if (!localRepoServiceIsBound) {
Context app = context.getApplicationContext();
app.bindService(new Intent(app, LocalRepoService.class),
serviceConnection, Context.BIND_AUTO_CREATE);
localRepoServiceIsBound = true;
}
}
public static void stopLocalRepoService(Context context) {
if (localRepoServiceIsBound) {
context.getApplicationContext().unbindService(serviceConnection);
localRepoServiceIsBound = false;
}
}
public static void restartLocalRepoService() {
if (localRepoServiceMessenger != null) {
try {
Message msg = Message.obtain(null,
LocalRepoService.RESTART, LocalRepoService.RESTART, 0);
localRepoServiceMessenger.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
}

View File

@ -22,8 +22,7 @@ import java.util.ArrayList;
import java.util.Arrays;
public class FDroidCertPins {
public static final String[] DEFAULT_PINS =
{
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
@ -31,22 +30,12 @@ public class FDroidCertPins {
* 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)
{
public static String[] getPinList() {
if (PINLIST == null) {
PINLIST = new ArrayList<String>();
PINLIST.addAll(Arrays.asList(DEFAULT_PINS));
}

View File

@ -20,17 +20,25 @@
package org.fdroid.fdroid;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.NavUtils;
import android.text.TextUtils;
import android.util.Log;
import android.view.MenuItem;
import android.widget.LinearLayout;
import android.widget.Toast;
import org.fdroid.fdroid.compat.ActionBarCompat;
import org.fdroid.fdroid.views.fragments.RepoListFragment;
import java.util.Locale;
public class ManageRepo extends FragmentActivity {
/**
@ -49,8 +57,8 @@ public class ManageRepo extends FragmentActivity {
((FDroidApp) getApplication()).applyTheme(this);
if (savedInstanceState == null) {
FragmentManager fm = getSupportFragmentManager();
if (fm.findFragmentById(android.R.id.content) == null) {
// Need to set a dummy view (which will get overridden by the fragment manager
// below) so that we can call setContentView(). This is a work around for
// a (bug?) thing in 3.0, 3.1 which requires setContentView to be invoked before
@ -59,8 +67,7 @@ public class ManageRepo extends FragmentActivity {
setContentView( new LinearLayout(this) );
listFragment = new RepoListFragment();
getSupportFragmentManager()
.beginTransaction()
fm.beginTransaction()
.add(android.R.id.content, listFragment)
.commit();
}
@ -68,6 +75,18 @@ public class ManageRepo extends FragmentActivity {
ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true);
}
@Override
protected void onResume() {
super.onResume();
/* let's see if someone is trying to send us a new repo */
addRepoFromIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
addRepoFromIntent(intent);
}
@Override
public void finish() {
Intent ret = new Intent();
@ -99,4 +118,74 @@ public class ManageRepo extends FragmentActivity {
}
return super.onOptionsItemSelected(item);
}
private void addRepoFromIntent(Intent intent) {
/* an URL from a click, NFC, QRCode scan, etc */
Uri uri = intent.getData();
if (uri != null) {
// scheme and host should only ever be pure ASCII aka Locale.ENGLISH
String scheme = intent.getScheme();
String host = uri.getHost();
if (scheme == null || host == null) {
String msg = String.format(getString(R.string.malformed_repo_uri), uri);
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
return;
}
if (scheme.equals("FDROIDREPO") || scheme.equals("FDROIDREPOS")) {
/*
* QRCodes are more efficient in all upper case, so QR URIs are
* encoded in all upper case, then forced to lower case.
* Checking if the special F-Droid scheme being all is upper
* case means it should be downcased.
*/
uri = Uri.parse(uri.toString().toLowerCase(Locale.ENGLISH));
} else if (uri.getPath().startsWith("/FDROID/REPO")) {
/*
* some QR scanners chop off the fdroidrepo:// and just try
* http://, then the incoming URI does not get downcased
* properly, and the query string is stripped off. So just
* downcase the path, and carry on to get something working.
*/
uri = Uri.parse(uri.toString().toLowerCase(Locale.ENGLISH));
}
// make scheme and host lowercase so they're readable in dialogs
scheme = scheme.toLowerCase(Locale.ENGLISH);
host = host.toLowerCase(Locale.ENGLISH);
String fingerprint = uri.getQueryParameter("fingerprint");
Log.i("RepoListFragment", "onCreate " + fingerprint);
if (scheme.equals("fdroidrepos") || scheme.equals("fdroidrepo")
|| scheme.equals("https") || scheme.equals("http")) {
/* sanitize and format for function and readability */
String uriString = uri.toString()
.replaceAll("\\?.*$", "") // remove the whole query
.replaceAll("/*$", "") // remove all trailing slashes
.replace(uri.getHost(), host) // downcase host name
.replace(intent.getScheme(), scheme) // downcase scheme
.replace("fdroidrepo", "http"); // proper repo address
listFragment.importRepo(uriString, fingerprint);
// if this is a local repo, check we're on the same wifi
String uriBssid = uri.getQueryParameter("bssid");
if (!TextUtils.isEmpty(uriBssid)) {
if (uri.getPort() != 8888
&& !host.matches("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+")) {
Log.i("ManageRepo", "URI is not local repo: " + uri);
return;
}
WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
String bssid = wifiInfo.getBSSID().toLowerCase(Locale.ENGLISH);
uriBssid = Uri.decode(uriBssid).toLowerCase(Locale.ENGLISH);
if (!bssid.equals(uriBssid)) {
String msg = String.format(getString(R.string.not_on_same_wifi),
uri.getQueryParameter("ssid"));
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
}
// TODO we should help the user to the right thing here,
// instead of just showing a message!
}
}
}
}
}

View File

@ -20,13 +20,26 @@ package org.fdroid.fdroid;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.util.Log;
import org.fdroid.fdroid.data.InstalledAppProvider;
public class PackageAddedReceiver extends PackageReceiver {
@Override
protected boolean toDiscard(Intent intent) {
if (intent.hasExtra(Intent.EXTRA_REPLACING)) {
Log.d("FDroid", "Discarding since this PACKAGE_ADDED is just a PACKAGE_REPLACED");
return true;
}
return false;
}
@Override
protected void handle(Context context, String appId) {
PackageInfo info = getPackageInfo(context, appId);
@ -34,10 +47,12 @@ public class PackageAddedReceiver extends PackageReceiver {
Log.d("FDroid", "Inserting installed app info for '" + appId + "' (v" + info.versionCode + ")");
Uri uri = InstalledAppProvider.getContentUri();
ContentValues values = new ContentValues(3);
ContentValues values = new ContentValues(4);
values.put(InstalledAppProvider.DataColumns.APP_ID, appId);
values.put(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode);
values.put(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName);
values.put(InstalledAppProvider.DataColumns.APPLICATION_LABEL,
InstalledAppProvider.getApplicationLabel(context, appId));
context.getContentResolver().insert(uri, values);
}

View File

@ -29,6 +29,7 @@ import org.fdroid.fdroid.data.AppProvider;
abstract class PackageReceiver extends BroadcastReceiver {
abstract protected boolean toDiscard(Intent intent);
abstract protected void handle(Context context, String appId);
protected PackageInfo getPackageInfo(Context context, String appId) {
@ -43,6 +44,9 @@ abstract class PackageReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("FDroid", "PackageReceiver received [action = '" + intent.getAction() + "', data = '" + intent.getData() + "']");
if (toDiscard(intent)) {
return;
}
String appId = intent.getData().getSchemeSpecificPart();
handle(context, appId);
context.getContentResolver().notifyChange(AppProvider.getContentUri(appId), null);

View File

@ -19,6 +19,7 @@
package org.fdroid.fdroid;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import org.fdroid.fdroid.data.AppProvider;
@ -26,6 +27,15 @@ import org.fdroid.fdroid.data.InstalledAppProvider;
public class PackageRemovedReceiver extends PackageReceiver {
@Override
protected boolean toDiscard(Intent intent) {
if (intent.hasExtra(Intent.EXTRA_REPLACING)) {
Log.d("FDroid", "Discarding since this PACKAGE_REMOVED is just a PACKAGE_REPLACED");
return true;
}
return false;
}
@Override
protected void handle(Context context, String appId) {

View File

@ -20,9 +20,14 @@ package org.fdroid.fdroid;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.util.Log;
import org.fdroid.fdroid.data.InstalledAppProvider;
/**
@ -33,6 +38,11 @@ import org.fdroid.fdroid.data.InstalledAppProvider;
*/
public class PackageUpgradedReceiver extends PackageReceiver {
@Override
protected boolean toDiscard(Intent intent) {
return false;
}
@Override
protected void handle(Context context, String appId) {
PackageInfo info = getPackageInfo(context, appId);
@ -40,10 +50,12 @@ public class PackageUpgradedReceiver extends PackageReceiver {
Log.d("FDroid", "Updating installed app info for '" + appId + "' to v" + info.versionCode + " (" + info.versionName + ")");
Uri uri = InstalledAppProvider.getContentUri();
ContentValues values = new ContentValues(1);
values.put(InstalledAppProvider.DataColumns.APP_ID, info.packageName);
ContentValues values = new ContentValues(4);
values.put(InstalledAppProvider.DataColumns.APP_ID, appId);
values.put(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode);
values.put(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName);
values.put(InstalledAppProvider.DataColumns.APPLICATION_LABEL,
InstalledAppProvider.getApplicationLabel(context, appId));
context.getContentResolver().insert(uri, values);
}

View File

@ -0,0 +1,75 @@
package org.fdroid.fdroid;
import android.annotation.TargetApi;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.os.AsyncTask;
import android.os.Build;
import android.util.Log;
import android.view.Display;
import android.widget.ImageView;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.encode.Contents;
import com.google.zxing.encode.QRCodeEncoder;
// zxing is android-8 and above
@TargetApi(8)
public class QrGenAsyncTask extends AsyncTask<String, Void, Void> {
private static final String TAG = "QrGenAsyncTask";
private Activity activity;
private int viewId;
private Bitmap qrBitmap;
public QrGenAsyncTask(Activity activity, int viewId) {
this.activity = activity;
this.viewId = viewId;
}
/*
* The method for getting screen dimens changed, so this uses both the
* deprecated one and the 13+ one, and supports all Android versions.
*/
@SuppressWarnings("deprecation")
@TargetApi(13)
@Override
protected Void doInBackground(String... s) {
String qrData = s[0];
Display display = activity.getWindowManager().getDefaultDisplay();
Point outSize = new Point();
int x, y, qrCodeDimension;
/* lame, got to use both the new and old APIs here */
if (Build.VERSION.SDK_INT >= 13) {
display.getSize(outSize);
x = outSize.x;
y = outSize.y;
} else {
x = display.getWidth();
y = display.getHeight();
}
if (x < y)
qrCodeDimension = x;
else
qrCodeDimension = y;
Log.i(TAG, "generating QRCode Bitmap of " + qrCodeDimension + "x" + qrCodeDimension);
QRCodeEncoder qrCodeEncoder = new QRCodeEncoder(qrData, null,
Contents.Type.TEXT, BarcodeFormat.QR_CODE.toString(), qrCodeDimension);
try {
qrBitmap = qrCodeEncoder.encodeAsBitmap();
} catch (WriterException e) {
Log.e(TAG, e.getMessage());
}
return (Void) null;
}
@Override
protected void onPostExecute(Void v) {
ImageView qrCodeImageView = (ImageView) activity.findViewById(viewId);
qrCodeImageView.setImageBitmap(qrBitmap);
}
}

View File

@ -18,8 +18,6 @@
package org.fdroid.fdroid;
import java.util.*;
import android.app.*;
import android.content.*;
import android.content.SharedPreferences.Editor;
@ -29,14 +27,15 @@ import android.net.NetworkInfo;
import android.net.Uri;
import android.os.*;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import org.fdroid.fdroid.data.*;
import org.fdroid.fdroid.updater.RepoUpdater;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.widget.Toast;
import java.util.*;
public class UpdateService extends IntentService implements ProgressListener {
@ -121,7 +120,13 @@ public class UpdateService extends IntentService implements ProgressListener {
}
if (finished && dialog.isShowing())
try {
dialog.dismiss();
} catch (IllegalArgumentException e) {
// sometimes dialog.isShowing() doesn't work :(
// https://stackoverflow.com/questions/19538282/view-not-attached-to-window-manager-dialog-dismiss
e.printStackTrace();
}
}
}
@ -305,7 +310,7 @@ public class UpdateService extends IntentService implements ProgressListener {
}
if (!changes) {
Log.d("FDroid", "Not checking app details or compatibility, ecause all repos were up to date.");
Log.d("FDroid", "Not checking app details or compatibility, because all repos were up to date.");
} else {
sendStatus(STATUS_INFO, getString(R.string.status_checking_compatibility));
@ -333,7 +338,7 @@ public class UpdateService extends IntentService implements ProgressListener {
notifyContentProviders();
if (prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, false)) {
if (prefs.getBoolean(Preferences.PREF_UPD_NOTIFY, true)) {
performUpdateNotification(appsToUpdate.values());
}
}
@ -388,30 +393,9 @@ public class UpdateService extends IntentService implements ProgressListener {
}
private void performUpdateNotification(Collection<App> apps) {
int updateCount = 0;
// This may be somewhat strange, because we usually would just trust
// App.canAndWantToUpdate(). The only problem is that the "appsToUpdate"
// list only contains data from the repo index, not our database.
// As such, it doesn't know if we want to ignore the apps or not. For that, we
// need to query the database manually and identify those which are to be ignored.
String[] projection = { AppProvider.DataColumns.APP_ID };
List<App> appsToIgnore = AppProvider.Helper.findIgnored(this, projection);
for (App app : apps) {
boolean ignored = false;
for(App appIgnored : appsToIgnore) {
if (appIgnored.id.equals(app.id)) {
ignored = true;
break;
}
}
if (!ignored && app.hasUpdates()) {
updateCount++;
}
}
if (updateCount > 0) {
showAppUpdatesNotification(updateCount);
int count = AppProvider.Helper.count(this, AppProvider.getCanUpdateUri());
if (count > 0) {
showAppUpdatesNotification(count);
}
}
@ -444,7 +428,9 @@ public class UpdateService extends IntentService implements ProgressListener {
private List<String> getKnownAppIds(List<App> apps) {
List<String> knownAppIds = new ArrayList<String>();
if (apps.size() > AppProvider.MAX_APPS_TO_QUERY) {
if (apps.size() == 0) {
// Do nothing
} else if (apps.size() > AppProvider.MAX_APPS_TO_QUERY) {
int middle = apps.size() / 2;
List<App> apps1 = apps.subList(0, middle);
List<App> apps2 = apps.subList(middle, apps.size());

View File

@ -19,23 +19,28 @@
package org.fdroid.fdroid;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.content.res.XmlResourceParser;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import com.nostra13.universalimageloader.utils.StorageUtils;
import java.io.Closeable;
import java.io.File;
import java.io.FileReader;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import org.fdroid.fdroid.data.Repo;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.*;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.text.SimpleDateFormat;
import java.security.MessageDigest;
import java.util.*;
public final class Utils {
@ -97,6 +102,61 @@ public final class Utils {
output.flush();
}
/**
* use symlinks if they are available, otherwise fall back to copying
*/
public static boolean symlinkOrCopyFile(File inFile, File outFile) {
if (new File("/system/bin/ln").exists()) {
return symlink(inFile, outFile);
} else {
return copy(inFile, outFile);
}
}
public static boolean symlink(File inFile, File outFile) {
int exitCode = -1;
try {
Process sh = Runtime.getRuntime().exec("sh");
OutputStream out = sh.getOutputStream();
String command = "/system/bin/ln -s " + inFile.getAbsolutePath() + " " + outFile
+ "\nexit\n";
out.write(command.getBytes("ASCII"));
final char buf[] = new char[40];
InputStreamReader reader = new InputStreamReader(sh.getInputStream());
while (reader.read(buf) != -1)
throw new IOException("stdout: " + new String(buf));
reader = new InputStreamReader(sh.getErrorStream());
while (reader.read(buf) != -1)
throw new IOException("stderr: " + new String(buf));
exitCode = sh.waitFor();
} catch (IOException e) {
e.printStackTrace();
return false;
} catch (InterruptedException e) {
e.printStackTrace();
return false;
}
return exitCode == 0;
}
public static boolean copy(File inFile, File outFile) {
InputStream input = null;
OutputStream output = null;
try {
input = new FileInputStream(inFile);
output = new FileOutputStream(outFile);
Utils.copy(input, output);
output.close();
input.close();
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
public static void closeQuietly(Closeable closeable) {
if (closeable == null) {
return;
@ -146,6 +206,34 @@ public final class Utils {
return androidVersionNames[sdkLevel];
}
/* PackageManager doesn't give us minSdkVersion, so we have to parse it */
public static int getMinSdkVersion(Context context, String packageName) {
try {
AssetManager am = context.createPackageContext(packageName, 0).getAssets();
XmlResourceParser xml = am.openXmlResourceParser("AndroidManifest.xml");
int eventType = xml.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG) {
if (xml.getName().equals("uses-sdk")) {
for (int j = 0; j < xml.getAttributeCount(); j++) {
if (xml.getAttributeName(j).equals("minSdkVersion")) {
return Integer.parseInt(xml.getAttributeValue(j));
}
}
}
}
eventType = xml.nextToken();
}
} catch (NameNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (XmlPullParserException e) {
e.printStackTrace();
}
return 8; // some kind of hopeful default
}
public static int countSubstringOccurrence(File file, String substring) throws IOException {
int count = 0;
FileReader input = null;
@ -178,7 +266,9 @@ public final class Utils {
// return a fingerprint formatted for display
public static String formatFingerprint(String fingerprint) {
if (fingerprint.length() != 62) // SHA-256 is 62 hex chars
if (TextUtils.isEmpty(fingerprint)
|| fingerprint.length() != 64 // SHA-256 is 64 hex chars
|| fingerprint.matches(".*[^0-9a-fA-F].*")) // its a hex string
return "BAD FINGERPRINT";
String displayFP = fingerprint.substring(0, 2);
for (int i = 2; i < fingerprint.length(); i = i + 2)
@ -186,6 +276,21 @@ public final class Utils {
return displayFP;
}
public static Uri getSharingUri(Context context, Repo repo) {
if (TextUtils.isEmpty(repo.address))
return Uri.parse("http://wifi-not-enabled");
Uri uri = Uri.parse(repo.address.replaceFirst("http", "fdroidrepo"));
Uri.Builder b = uri.buildUpon();
if (!TextUtils.isEmpty(repo.fingerprint))
b.appendQueryParameter("fingerprint", repo.fingerprint);
if (!TextUtils.isEmpty(FDroidApp.bssid)) {
b.appendQueryParameter("bssid", Uri.encode(FDroidApp.bssid));
if (!TextUtils.isEmpty(FDroidApp.ssid))
b.appendQueryParameter("ssid", Uri.encode(FDroidApp.ssid));
}
return b.build();
}
public static File getApkCacheDir(Context context) {
File apkCacheDir = new File(
StorageUtils.getCacheDirectory(context, true), "apks");
@ -196,9 +301,11 @@ public final class Utils {
}
public static String calcFingerprint(String keyHexString) {
if (TextUtils.isEmpty(keyHexString))
if (TextUtils.isEmpty(keyHexString)
|| keyHexString.matches(".*[^a-fA-F0-9].*")) {
Log.e("FDroid", "Signing key certificate was blank or contained a non-hex-digit!");
return null;
else
} else
return calcFingerprint(Hasher.unhex(keyHexString));
}
@ -212,13 +319,17 @@ public final class Utils {
public static String calcFingerprint(byte[] key) {
String ret = null;
if (key.length < 256) {
Log.e("FDroid", "key was shorter than 256 bytes (" + key.length + "), cannot be valid!");
return null;
}
try {
// keytool -list -v gives you the SHA-256 fingerprint
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.update(key);
byte[] fingerprint = digest.digest();
Formatter formatter = new Formatter(new StringBuilder());
for (int i = 1; i < fingerprint.length; i++) {
for (int i = 0; i < fingerprint.length; i++) {
formatter.format("%02X", fingerprint[i]);
}
ret = formatter.toString();
@ -288,4 +399,62 @@ public final class Utils {
}
}
// this is all new stuff being added
public static String hashBytes(byte[] input, String algo) {
try {
MessageDigest md = MessageDigest.getInstance(algo);
byte[] hashBytes = md.digest(input);
String hash = toHexString(hashBytes);
md.reset();
return hash;
} catch (NoSuchAlgorithmException e) {
Log.e("FDroid", "Device does not support " + algo + " MessageDisgest algorithm");
return null;
}
}
public static String getBinaryHash(File apk, String algo) {
FileInputStream fis = null;
BufferedInputStream bis = null;
try {
MessageDigest md = MessageDigest.getInstance(algo);
fis = new FileInputStream(apk);
bis = new BufferedInputStream(fis);
byte[] dataBytes = new byte[524288];
int nread = 0;
while ((nread = bis.read(dataBytes)) != -1)
md.update(dataBytes, 0, nread);
byte[] mdbytes = md.digest();
return toHexString(mdbytes);
} catch (IOException e) {
Log.e("FDroid", "Error reading \"" + apk.getAbsolutePath()
+ "\" to compute " + algo + " hash.");
return null;
} catch (NoSuchAlgorithmException e) {
Log.e("FDroid", "Device does not support " + algo + " MessageDisgest algorithm");
return null;
} finally {
closeQuietly(fis);
}
}
/**
* Computes the base 16 representation of the byte array argument.
*
* @param bytes an array of bytes.
* @return the bytes represented as a string of hexadecimal digits.
*/
public static String toHexString(byte[] bytes) {
BigInteger bi = new BigInteger(1, bytes);
return String.format("%0" + (bytes.length << 1) + "X", bi);
}
public static String getDefaultRepoName() {
return (Build.BRAND + " " + Build.MODEL).replaceAll(" ", "-");
}
}

View File

@ -0,0 +1,21 @@
package org.fdroid.fdroid;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.NetworkInfo;
import android.net.wifi.WifiManager;
import org.fdroid.fdroid.net.WifiStateChangeService;
public class WifiStateChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
NetworkInfo ni = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
if (ni.isConnected()) {
context.startService(new Intent(context, WifiStateChangeService.class));
}
}
}

View File

@ -4,7 +4,8 @@ import android.content.ContentValues;
import android.database.Cursor;
import org.fdroid.fdroid.Utils;
import java.util.*;
import java.io.File;
import java.util.Date;
public class Apk extends ValueObject implements Comparable<Apk> {
@ -31,7 +32,8 @@ public class Apk extends ValueObject implements Comparable<Apk> {
// True if compatible with the device.
public boolean compatible;
public String apkName;
public String apkName; // F-Droid style APK name
public File installedFile; // the .apk file on this device's filesystem
// If not null, this is the name of the source tarball for the
// application. Null indicates that it's a developer's binary

View File

@ -103,6 +103,9 @@ public class ApkProvider extends FDroidProvider {
*/
public static List<Apk> knownApks(Context context,
List<Apk> apks, String[] fields) {
if (apks.size() == 0) {
return new ArrayList<Apk>();
}
ContentResolver resolver = context.getContentResolver();
Uri uri = getContentUri(apks);
Cursor cursor = resolver.query(uri, fields, null, null, null);

View File

@ -1,20 +1,32 @@
package org.fdroid.fdroid.data;
import android.annotation.TargetApi;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.*;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import org.fdroid.fdroid.AppFilter;
import org.fdroid.fdroid.Utils;
import java.util.Date;
import java.util.Map;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
public class App extends ValueObject implements Comparable<App> {
// True if compatible with the device (i.e. if at least one apk is)
public boolean compatible;
public boolean includeInRepo = false;
public String id = "unknown";
public String name = "Unknown";
@ -83,6 +95,8 @@ public class App extends ValueObject implements Comparable<App> {
public int installedVersionCode;
public Apk installedApk; // might be null if not installed
@Override
public int compareTo(App app) {
return name.compareToIgnoreCase(app.name);
@ -160,6 +174,156 @@ public class App extends ValueObject implements Comparable<App> {
}
}
/**
* Instantiate from a locally installed package.
*/
@TargetApi(9)
public App(Context context, PackageManager pm, String packageName)
throws CertificateEncodingException, IOException, NameNotFoundException {
ApplicationInfo appInfo;
PackageInfo packageInfo;
appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES
| PackageManager.GET_PERMISSIONS);
String installerPackageName = pm.getInstallerPackageName(packageName);
CharSequence installerPackageLabel = null;
if (!TextUtils.isEmpty(installerPackageName)) {
try {
ApplicationInfo installerAppInfo = pm.getApplicationInfo(installerPackageName,
PackageManager.GET_META_DATA);
installerPackageLabel = installerAppInfo.loadLabel(pm);
} catch (NameNotFoundException e) {
Log.d(getClass().getCanonicalName(), e.getMessage());
}
}
if (TextUtils.isEmpty(installerPackageLabel))
installerPackageLabel = installerPackageName;
CharSequence appDescription = appInfo.loadDescription(pm);
if (TextUtils.isEmpty(appDescription))
this.summary = "(installed by " + installerPackageLabel + ")";
else
this.summary = (String) appDescription.subSequence(0, 40);
this.id = appInfo.packageName;
if (Build.VERSION.SDK_INT > 8) {
this.added = new Date(packageInfo.firstInstallTime);
this.lastUpdated = new Date(packageInfo.lastUpdateTime);
} else {
this.added = new Date(System.currentTimeMillis());
this.lastUpdated = this.added;
}
this.description = "<p>";
if (!TextUtils.isEmpty(appDescription))
this.description += appDescription + "\n";
this.description += "(installed by " + installerPackageLabel
+ ", first installed on " + this.added
+ ", last updated on " + this.lastUpdated + ")</p>";
this.name = (String) appInfo.loadLabel(pm);
File apkFile = new File(appInfo.publicSourceDir);
Apk apk = new Apk();
apk.version = packageInfo.versionName;
apk.vercode = packageInfo.versionCode;
apk.hashType = "sha256";
apk.hash = Utils.getBinaryHash(apkFile, apk.hashType);
apk.added = this.added;
apk.minSdkVersion = Utils.getMinSdkVersion(context, packageName);
apk.id = this.id;
apk.installedFile = apkFile;
if (packageInfo.requestedPermissions == null)
apk.permissions = null;
else
apk.permissions = Utils.CommaSeparatedList.make(
Arrays.asList(packageInfo.requestedPermissions));
apk.apkName = apk.id + "_" + apk.vercode + ".apk";
FeatureInfo[] features = packageInfo.reqFeatures;
if (features != null && features.length > 0) {
List<String> featureNames = new ArrayList<String>(features.length);
for (int i = 0; i < features.length; i++)
featureNames.add(features[i].name);
apk.features = Utils.CommaSeparatedList.make(featureNames);
}
// Signature[] sigs = pkgInfo.signatures;
byte[] rawCertBytes;
JarFile apkJar = new JarFile(apkFile);
JarEntry aSignedEntry = (JarEntry) apkJar.getEntry("AndroidManifest.xml");
if (aSignedEntry == null) {
apkJar.close();
throw new CertificateEncodingException("null signed entry!");
}
InputStream tmpIn = apkJar.getInputStream(aSignedEntry);
byte[] buff = new byte[2048];
while (tmpIn.read(buff, 0, buff.length) != -1) {
/*
* NOP - apparently have to READ from the JarEntry before you can
* call getCerficates() and have it return != null. Yay Java.
*/
}
tmpIn.close();
if (aSignedEntry.getCertificates() == null
|| aSignedEntry.getCertificates().length == 0) {
apkJar.close();
throw new CertificateEncodingException("No Certificates found!");
}
Certificate signer = aSignedEntry.getCertificates()[0];
rawCertBytes = signer.getEncoded();
apkJar.close();
/*
* I don't fully understand the loop used here. I've copied it verbatim
* from getsig.java bundled with FDroidServer. I *believe* it is taking
* the raw byte encoding of the certificate & converting it to a byte
* array of the hex representation of the original certificate byte
* array. This is then MD5 sum'd. It's a really bad way to be doing this
* if I'm right... If I'm not right, I really don't know! see lines
* 67->75 in getsig.java bundled with Fdroidserver
*/
byte[] fdroidSig = new byte[rawCertBytes.length * 2];
for (int j = 0; j < rawCertBytes.length; j++) {
byte v = rawCertBytes[j];
int d = (v >> 4) & 0xF;
fdroidSig[j * 2] = (byte) (d >= 10 ? ('a' + d - 10) : ('0' + d));
d = v & 0xF;
fdroidSig[j * 2 + 1] = (byte) (d >= 10 ? ('a' + d - 10) : ('0' + d));
}
apk.sig = Utils.hashBytes(fdroidSig, "md5");
this.installedApk = apk;
}
public boolean isValid() {
if (TextUtils.isEmpty(this.name)
|| TextUtils.isEmpty(this.id))
return false;
if (this.installedApk == null)
return false;
if (TextUtils.isEmpty(this.installedApk.sig))
return false;
File apkFile = this.installedApk.installedFile;
if (apkFile == null || !apkFile.canRead())
return false;
return true;
}
public ContentValues toContentValues() {
ContentValues values = new ContentValues();

View File

@ -18,6 +18,18 @@ public class AppProvider extends FDroidProvider {
private Helper() {}
public static int count(Context context, Uri uri) {
String[] projection = new String[] { AppProvider.DataColumns._COUNT };
Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
int count = 0;
if (cursor != null && cursor.getCount() == 1) {
cursor.moveToFirst();
count = cursor.getInt(0);
cursor.close();
}
return count;
}
public static List<App> all(ContentResolver resolver) {
return all(resolver, DataColumns.ALL);
}
@ -808,9 +820,10 @@ public class AppProvider extends FDroidProvider {
Log.d("FDroid", "Updating icon paths for apps belonging to repos with version >= " + Repo.VERSION_DENSITY_SPECIFIC_ICONS);
String iconsDir = Utils.getIconsDir(getContext());
Log.d("FDroid", "Using icon dir '"+iconsDir+"'");
String repoVersion = Integer.toString(Repo.VERSION_DENSITY_SPECIFIC_ICONS);
String query = getIconUpdateQuery();
String[] params = { iconsDir, repoVersion };
String[] params = { repoVersion, iconsDir };
write().execSQL(query, params);
}

View File

@ -91,12 +91,13 @@ public class DBHelper extends SQLiteOpenHelper {
public static final String TABLE_INSTALLED_APP = "fdroid_installedApp";
private static final String CREATE_TABLE_INSTALLED_APP = "CREATE TABLE " + TABLE_INSTALLED_APP
+ " ( "
+ "appId TEXT NOT NULL PRIMARY KEY, "
+ "versionCode INT NOT NULL, "
+ "versionName TEXT NOT NULL "
+ InstalledAppProvider.DataColumns.APP_ID + " TEXT NOT NULL PRIMARY KEY, "
+ InstalledAppProvider.DataColumns.VERSION_CODE + " INT NOT NULL, "
+ InstalledAppProvider.DataColumns.VERSION_NAME + " TEXT NOT NULL, "
+ InstalledAppProvider.DataColumns.APPLICATION_LABEL + " TEXT NOT NULL "
+ " );";
private static final int DB_VERSION = 43;
private static final int DB_VERSION = 46;
private Context context;
@ -249,8 +250,8 @@ public class DBHelper extends SQLiteOpenHelper {
addLastUpdatedToRepo(db, oldVersion);
renameRepoId(db, oldVersion);
populateRepoNames(db, oldVersion);
if (oldVersion < 43) createInstalledApp(db);
addAppLabelToInstalledCache(db, oldVersion);
}
/**
@ -322,7 +323,7 @@ public class DBHelper extends SQLiteOpenHelper {
* calculate its fingerprint and save it to the database.
*/
private void addFingerprintToRepo(SQLiteDatabase db, int oldVersion) {
if (oldVersion < 29) {
if (oldVersion < 44) {
if (!columnExists(db, TABLE_REPO, "fingerprint"))
db.execSQL("alter table " + TABLE_REPO + " add column fingerprint text");
List<Repo> oldrepos = new ArrayList<Repo>();
@ -398,6 +399,17 @@ public class DBHelper extends SQLiteOpenHelper {
db.execSQL(CREATE_TABLE_INSTALLED_APP);
}
private void addAppLabelToInstalledCache(SQLiteDatabase db, int oldVersion) {
if (oldVersion < 45) {
Log.i(TAG, "Adding applicationLabel to installed app table. " +
"Turns out we will need to repopulate the cache after doing this, " +
"so just dropping and recreating the table (instead of altering and adding a column). " +
"This will force the entire cache to be rebuilt, including app names.");
db.execSQL("DROP TABLE fdroid_installedApp;");
createInstalledApp(db);
}
}
private static boolean columnExists(SQLiteDatabase db,
String table, String column) {
return (db.rawQuery( "select * from " + table + " limit 0,1", null )

View File

@ -135,6 +135,8 @@ public class InstalledAppCacheUpdater {
.withValue(InstalledAppProvider.DataColumns.APP_ID, info.packageName)
.withValue(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode)
.withValue(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName)
.withValue(InstalledAppProvider.DataColumns.APPLICATION_LABEL,
InstalledAppProvider.getApplicationLabel(context, info.packageName))
.build();
ops.add(op);
}

View File

@ -3,9 +3,14 @@ package org.fdroid.fdroid.data;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import org.fdroid.fdroid.R;
import java.util.HashMap;
@ -45,20 +50,28 @@ public class InstalledAppProvider extends FDroidProvider {
public interface DataColumns {
public static final String _ID = "rowid as _id"; // Required for CursorLoaders
public static final String APP_ID = "appId";
public static final String VERSION_CODE = "versionCode";
public static final String VERSION_NAME = "versionName";
public static final String APPLICATION_LABEL = "applicationLabel";
public static String[] ALL = { APP_ID, VERSION_CODE, VERSION_NAME };
public static String[] ALL = {
_ID, APP_ID, VERSION_CODE, VERSION_NAME, APPLICATION_LABEL,
};
}
private static final String PROVIDER_NAME = "InstalledAppProvider";
private static final String PATH_SEARCH = "search";
private static final int CODE_SEARCH = CODE_SINGLE + 1;
private static final UriMatcher matcher = new UriMatcher(-1);
static {
matcher.addURI(getAuthority(), null, CODE_LIST);
matcher.addURI(getAuthority(), PATH_SEARCH + "/*", CODE_SEARCH);
matcher.addURI(getAuthority(), "*", CODE_SINGLE);
}
@ -70,6 +83,27 @@ public class InstalledAppProvider extends FDroidProvider {
return Uri.withAppendedPath(getContentUri(), appId);
}
public static Uri getSearchUri(String keywords) {
return getContentUri().buildUpon()
.appendPath(PATH_SEARCH)
.appendPath(keywords)
.build();
}
public static String getApplicationLabel(Context context, String packageName) {
PackageManager pm = context.getPackageManager();
ApplicationInfo appInfo;
try {
appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
return appInfo.loadLabel(pm).toString();
} catch (NameNotFoundException e) {
e.printStackTrace();
} catch (NotFoundException e) {
Log.d("InstalledAppProvider", "getApplicationLabel: " + e.getMessage());
}
return packageName; // all else fails, return id
}
@Override
protected String getTableName() {
return DBHelper.TABLE_INSTALLED_APP;
@ -93,8 +127,16 @@ public class InstalledAppProvider extends FDroidProvider {
return new QuerySelection("appId = ?", new String[] { appId } );
}
private QuerySelection querySearch(String keywords) {
return new QuerySelection("applicationLabel LIKE ?", new String[] { "%" + keywords + "%" } );
}
@Override
public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) {
if (sortOrder == null) {
sortOrder = DataColumns.APPLICATION_LABEL;
}
QuerySelection selection = new QuerySelection(customSelection, selectionArgs);
switch (matcher.match(uri)) {
case CODE_LIST:
@ -104,13 +146,17 @@ public class InstalledAppProvider extends FDroidProvider {
selection = selection.add(queryApp(uri.getLastPathSegment()));
break;
case CODE_SEARCH:
selection = selection.add(querySearch(uri.getLastPathSegment()));
break;
default:
String message = "Invalid URI for installed app content provider: " + uri;
Log.e("FDroid", message);
throw new UnsupportedOperationException(message);
}
Cursor cursor = read().query(getTableName(), projection, selection.getSelection(), selection.getArgs(), null, null, null);
Cursor cursor = read().query(getTableName(), projection, selection.getSelection(), selection.getArgs(), null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}

View File

@ -2,6 +2,7 @@ package org.fdroid.fdroid.data;
import android.content.ContentValues;
import android.database.Cursor;
import android.text.TextUtils;
import android.util.Log;
import org.fdroid.fdroid.Utils;
@ -78,7 +79,13 @@ public class Repo extends ValueObject {
}
public boolean isSigned() {
return this.pubkey != null && this.pubkey.length() > 0;
return !TextUtils.isEmpty(this.pubkey);
}
// this happens when a repo is configed with a fingerprint, but the client
// has not connected to it yet to download its pubkey
public boolean isSignedButUnverified() {
return TextUtils.isEmpty(this.pubkey) && !TextUtils.isEmpty(this.fingerprint);
}
public boolean hasBeenUpdated() {

View File

@ -0,0 +1,419 @@
package org.fdroid.fdroid.localrepo;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.*;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.graphics.*;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Bitmap.Config;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.io.*;
import java.security.cert.CertificateEncodingException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Map.Entry;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
public class LocalRepoManager {
private static final String TAG = "LocalRepoManager";
// For ref, official F-droid repo presently uses a maxage of 14 days
private static final String DEFAULT_REPO_MAX_AGE_DAYS = "14";
private final PackageManager pm;
private final AssetManager assetManager;
private final SharedPreferences prefs;
private final String fdroidPackageName;
private String ipAddressString = "UNSET";
private String uriString = "UNSET";
private Map<String, App> apps = new HashMap<String, App>();
public final File xmlIndex;
public final File webRoot;
public final File fdroidDir;
public final File repoDir;
public final File iconsDir;
public LocalRepoManager(Context c) {
pm = c.getPackageManager();
assetManager = c.getAssets();
prefs = PreferenceManager.getDefaultSharedPreferences(c);
fdroidPackageName = c.getPackageName();
webRoot = c.getFilesDir();
/* /fdroid/repo is the standard path for user repos */
fdroidDir = new File(webRoot, "fdroid");
repoDir = new File(fdroidDir, "repo");
iconsDir = new File(repoDir, "icons");
xmlIndex = new File(repoDir, "index.xml");
if (!fdroidDir.exists())
if (!fdroidDir.mkdir())
Log.e(TAG, "Unable to create empty base: " + fdroidDir);
if (!repoDir.exists())
if (!repoDir.mkdir())
Log.e(TAG, "Unable to create empty repo: " + repoDir);
if (!iconsDir.exists())
if (!iconsDir.mkdir())
Log.e(TAG, "Unable to create icons folder: " + iconsDir);
}
public void setUriString(String uriString) {
this.uriString = uriString;
}
private String writeFdroidApkToWebroot(String repoAddress) {
ApplicationInfo appInfo;
String fdroidClientURL = "https://f-droid.org/FDroid.apk";
try {
appInfo = pm.getApplicationInfo(fdroidPackageName, PackageManager.GET_META_DATA);
File apkFile = new File(appInfo.publicSourceDir);
File fdroidApkLink = new File(webRoot, "fdroid.client.apk");
fdroidApkLink.delete();
if (Utils.symlinkOrCopyFile(apkFile, fdroidApkLink))
fdroidClientURL = "/" + fdroidApkLink.getName();
} catch (NameNotFoundException e) {
e.printStackTrace();
}
return fdroidClientURL;
}
public void writeIndexPage(String repoAddress) {
final String fdroidClientURL = writeFdroidApkToWebroot(repoAddress);
try {
File indexHtml = new File(webRoot, "index.html");
BufferedReader in = new BufferedReader(
new InputStreamReader(assetManager.open("index.template.html"), "UTF-8"));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(indexHtml)));
String line;
while ((line = in.readLine()) != null) {
line = line.replaceAll("\\{\\{REPO_URL\\}\\}", repoAddress);
line = line.replaceAll("\\{\\{CLIENT_URL\\}\\}", fdroidClientURL);
out.write(line);
}
in.close();
out.close();
// make symlinks/copies in each subdir of the repo to make sure that
// the user will always find the bootstrap page.
File fdroidDirIndex = new File(fdroidDir, "index.html");
fdroidDirIndex.delete();
Utils.symlinkOrCopyFile(indexHtml, fdroidDirIndex);
File repoDirIndex = new File(repoDir, "index.html");
repoDirIndex.delete();
Utils.symlinkOrCopyFile(indexHtml, repoDirIndex);
// add in /FDROID/REPO to support bad QR Scanner apps
File fdroidCAPS = new File(fdroidDir.getParentFile(), "FDROID");
fdroidCAPS.mkdir();
File repoCAPS = new File(fdroidCAPS, "REPO");
repoCAPS.mkdir();
File fdroidCAPSIndex = new File(fdroidCAPS, "index.html");
fdroidCAPSIndex.delete();
Utils.symlinkOrCopyFile(indexHtml, fdroidCAPSIndex);
File repoCAPSIndex = new File(repoCAPS, "index.html");
repoCAPSIndex.delete();
Utils.symlinkOrCopyFile(indexHtml, repoCAPSIndex);
} catch (IOException e) {
e.printStackTrace();
}
}
private void deleteContents(File path) {
if (path.exists()) {
for (File file : path.listFiles()) {
if (file.isDirectory()) {
deleteContents(file);
} else {
file.delete();
}
}
}
}
public void deleteRepo() {
deleteContents(repoDir);
}
public void copyApksToRepo() {
copyApksToRepo(new ArrayList<String>(apps.keySet()));
}
public void copyApksToRepo(List<String> appsToCopy) {
for (String packageName : appsToCopy) {
App app = apps.get(packageName);
File outFile = new File(repoDir, app.installedApk.apkName);
if (app.installedApk == null
|| !Utils.symlinkOrCopyFile(app.installedApk.installedFile, outFile)) {
throw new IllegalStateException("Unable to copy APK");
}
}
}
public interface ScanListener {
public void processedApp(String packageName, int index, int total);
}
@TargetApi(9)
public void addApp(Context context, String packageName) {
App app;
try {
app = new App(context, pm, packageName);
if (!app.isValid())
return;
PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_META_DATA);
app.icon = getIconFile(packageName, packageInfo.versionCode).getName();
} catch (NameNotFoundException e) {
e.printStackTrace();
return;
} catch (CertificateEncodingException e) {
e.printStackTrace();
return;
} catch (IOException e) {
e.printStackTrace();
return;
}
Log.i(TAG, "apps.put: " + packageName);
apps.put(packageName, app);
}
public void removeApp(String packageName) {
apps.remove(packageName);
}
public List<String> getApps() {
return new ArrayList<String>(apps.keySet());
}
public void copyIconsToRepo() {
ApplicationInfo appInfo;
for (App app : apps.values()) {
if (app.installedApk != null) {
try {
appInfo = pm.getApplicationInfo(app.id, PackageManager.GET_META_DATA);
copyIconToRepo(appInfo.loadIcon(pm), app.id, app.installedApk.vercode);
} catch (NameNotFoundException e) {
e.printStackTrace();
}
}
}
}
/**
* Extracts the icon from an APK and writes it to the repo as a PNG
*
* @return path to the PNG file
*/
public void copyIconToRepo(Drawable drawable, String packageName, int versionCode) {
Bitmap bitmap;
if (drawable instanceof BitmapDrawable) {
bitmap = ((BitmapDrawable) drawable).getBitmap();
} else {
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(), Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
}
File png = getIconFile(packageName, versionCode);
OutputStream out;
try {
out = new BufferedOutputStream(new FileOutputStream(png));
bitmap.compress(CompressFormat.PNG, 100, out);
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private File getIconFile(String packageName, int versionCode) {
return new File(iconsDir, packageName + "_" + versionCode + ".png");
}
// TODO this needs to be ported to < android-8
@TargetApi(8)
public void writeIndexXML() throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.newDocument();
Element rootElement = doc.createElement("fdroid");
doc.appendChild(rootElement);
// max age is an EditTextPreference, which is always a String
int repoMaxAge = Float.valueOf(prefs.getString("max_repo_age_days",
DEFAULT_REPO_MAX_AGE_DAYS)).intValue();
String repoName = prefs.getString("repo_name", Utils.getDefaultRepoName());
Element repo = doc.createElement("repo");
repo.setAttribute("icon", "blah.png");
repo.setAttribute("maxage", String.valueOf(repoMaxAge));
repo.setAttribute("name", repoName + " on " + ipAddressString);
long timestamp = System.currentTimeMillis() / 1000L;
repo.setAttribute("timestamp", String.valueOf(timestamp));
repo.setAttribute("url", uriString);
rootElement.appendChild(repo);
Element repoDesc = doc.createElement("description");
repoDesc.setTextContent("A local FDroid repo generated from apps installed on " + repoName);
repo.appendChild(repoDesc);
SimpleDateFormat dateToStr = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
for (Entry<String, App> entry : apps.entrySet()) {
App app = entry.getValue();
Element application = doc.createElement("application");
application.setAttribute("id", app.id);
rootElement.appendChild(application);
Element appID = doc.createElement("id");
appID.setTextContent(app.id);
application.appendChild(appID);
Element added = doc.createElement("added");
added.setTextContent(dateToStr.format(app.added));
application.appendChild(added);
Element lastUpdated = doc.createElement("lastupdated");
lastUpdated.setTextContent(dateToStr.format(app.lastUpdated));
application.appendChild(lastUpdated);
Element name = doc.createElement("name");
name.setTextContent(app.name);
application.appendChild(name);
Element summary = doc.createElement("summary");
summary.setTextContent(app.summary);
application.appendChild(summary);
Element desc = doc.createElement("desc");
desc.setTextContent(app.description);
application.appendChild(desc);
Element icon = doc.createElement("icon");
icon.setTextContent(app.icon);
application.appendChild(icon);
Element license = doc.createElement("license");
license.setTextContent("Unknown");
application.appendChild(license);
Element categories = doc.createElement("categories");
categories.setTextContent("LocalRepo," + repoName);
application.appendChild(categories);
Element category = doc.createElement("category");
category.setTextContent("LocalRepo," + repoName);
application.appendChild(category);
Element web = doc.createElement("web");
application.appendChild(web);
Element source = doc.createElement("source");
application.appendChild(source);
Element tracker = doc.createElement("tracker");
application.appendChild(tracker);
Element marketVersion = doc.createElement("marketversion");
marketVersion.setTextContent(app.installedApk.version);
application.appendChild(marketVersion);
Element marketVerCode = doc.createElement("marketvercode");
marketVerCode.setTextContent(String.valueOf(app.installedApk.vercode));
application.appendChild(marketVerCode);
Element packageNode = doc.createElement("package");
Element version = doc.createElement("version");
version.setTextContent(app.installedApk.version);
packageNode.appendChild(version);
// F-Droid unfortunately calls versionCode versioncode...
Element versioncode = doc.createElement("versioncode");
versioncode.setTextContent(String.valueOf(app.installedApk.vercode));
packageNode.appendChild(versioncode);
Element apkname = doc.createElement("apkname");
apkname.setTextContent(app.installedApk.apkName);
packageNode.appendChild(apkname);
Element hash = doc.createElement("hash");
hash.setAttribute("type", app.installedApk.hashType);
hash.setTextContent(app.installedApk.hash.toLowerCase(Locale.US));
packageNode.appendChild(hash);
Element sig = doc.createElement("sig");
sig.setTextContent(app.installedApk.sig.toLowerCase(Locale.US));
packageNode.appendChild(sig);
Element size = doc.createElement("size");
size.setTextContent(String.valueOf(app.installedApk.installedFile.length()));
packageNode.appendChild(size);
Element sdkver = doc.createElement("sdkver");
sdkver.setTextContent(String.valueOf(app.installedApk.minSdkVersion));
packageNode.appendChild(sdkver);
Element apkAdded = doc.createElement("added");
apkAdded.setTextContent(dateToStr.format(app.installedApk.added));
packageNode.appendChild(apkAdded);
Element features = doc.createElement("features");
if (app.installedApk.features != null)
features.setTextContent(Utils.CommaSeparatedList.str(app.installedApk.features));
packageNode.appendChild(features);
Element permissions = doc.createElement("permissions");
if (app.installedApk.permissions != null) {
StringBuilder buff = new StringBuilder();
for (String permission : app.installedApk.permissions) {
buff.append(permission.replace("android.permission.", ""));
buff.append(",");
}
String out = buff.toString();
if (!TextUtils.isEmpty(out))
permissions.setTextContent(out.substring(0, out.length() - 1));
}
packageNode.appendChild(permissions);
application.appendChild(packageNode);
}
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
DOMSource domSource = new DOMSource(doc);
StreamResult result = new StreamResult(xmlIndex);
transformer.transform(domSource, result);
}
}

View File

@ -0,0 +1,172 @@
package org.fdroid.fdroid.localrepo;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.net.LocalHTTPD;
import org.fdroid.fdroid.net.WifiStateChangeService;
import org.fdroid.fdroid.views.LocalRepoActivity;
import java.io.IOException;
import java.net.BindException;
import java.util.Random;
public class LocalRepoService extends Service {
private static final String TAG = "LocalRepoService";
public static final String STATE = "org.fdroid.fdroid.action.LOCAL_REPO_STATE";
public static final String STARTED = "org.fdroid.fdroid.category.LOCAL_REPO_STARTED";
public static final String STOPPED = "org.fdroid.fdroid.category.LOCAL_REPO_STOPPED";
private NotificationManager notificationManager;
// Unique Identification Number for the Notification.
// We use it on Notification start, and to cancel it.
private int NOTIFICATION = R.string.local_repo_running;
private Handler webServerThreadHandler = null;
public static int START = 1111111;
public static int STOP = 12345678;
public static int RESTART = 87654;
final Messenger messenger = new Messenger(new StartStopHandler(this));
static class StartStopHandler extends Handler {
private static LocalRepoService service;
public StartStopHandler(LocalRepoService service) {
StartStopHandler.service = service;
}
@Override
public void handleMessage(Message msg) {
if (msg.arg1 == START) {
service.startWebServer();
} else if (msg.arg1 == STOP) {
service.stopWebServer();
} else if (msg.arg1 == RESTART) {
service.stopWebServer();
service.startWebServer();
} else {
Log.e(TAG, "unsupported msg.arg1, ignored");
}
}
}
private BroadcastReceiver onWifiChange = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent i) {
stopWebServer();
startWebServer();
}
};
@Override
public void onCreate() {
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
// launch LocalRepoActivity if the user selects this notification
Intent intent = new Intent(this, LocalRepoActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, 0);
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle(getText(R.string.local_repo_running))
.setContentText(getText(R.string.touch_to_configure_local_repo))
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentIntent(contentIntent)
.build();
startForeground(NOTIFICATION, notification);
startWebServer();
LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
new IntentFilter(WifiStateChangeService.BROADCAST));
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// We want this service to continue running until it is explicitly
// stopped, so return sticky.
return START_STICKY;
}
@Override
public void onDestroy() {
stopWebServer();
notificationManager.cancel(NOTIFICATION);
LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange);
}
@Override
public IBinder onBind(Intent intent) {
return messenger.getBinder();
}
private void startWebServer() {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
Runnable webServer = new Runnable() {
// Tell Eclipse this is not a leak because of Looper use.
@SuppressLint("HandlerLeak")
@Override
public void run() {
final LocalHTTPD localHttpd = new LocalHTTPD(getFilesDir(),
prefs.getBoolean("use_https", false));
Looper.prepare(); // must be run before creating a Handler
webServerThreadHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.i(TAG, "we've been asked to stop the webserver: " + msg.obj);
localHttpd.stop();
}
};
try {
localHttpd.start();
} catch (BindException e) {
int prev = FDroidApp.port;
FDroidApp.port = FDroidApp.port + new Random().nextInt(1111);
Log.w(TAG, "port " + prev + " occupied, trying on " + FDroidApp.port + "!");
startService(new Intent(LocalRepoService.this, WifiStateChangeService.class));
} catch (IOException e) {
e.printStackTrace();
}
Looper.loop(); // start the message receiving loop
}
};
new Thread(webServer).start();
Intent intent = new Intent(STATE);
intent.putExtra(STATE, STARTED);
LocalBroadcastManager.getInstance(LocalRepoService.this).sendBroadcast(intent);
}
private void stopWebServer() {
if (webServerThreadHandler == null) {
Log.i(TAG, "null handler in stopWebServer");
return;
}
Message msg = webServerThreadHandler.obtainMessage();
msg.obj = webServerThreadHandler.getLooper().getThread().getName() + " says stop";
webServerThreadHandler.sendMessage(msg);
Intent intent = new Intent(STATE);
intent.putExtra(STATE, STOPPED);
LocalBroadcastManager.getInstance(LocalRepoService.this).sendBroadcast(intent);
}
}

View File

@ -0,0 +1,341 @@
package org.fdroid.fdroid.net;
import android.util.Log;
import android.webkit.MimeTypeMap;
import fi.iki.elonen.NanoHTTPD;
import org.fdroid.fdroid.FDroidApp;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import javax.net.ssl.SSLServerSocketFactory;
public class LocalHTTPD extends NanoHTTPD {
private static final String TAG = LocalHTTPD.class.getCanonicalName();
private final File webRoot;
private final boolean logRequests;
public LocalHTTPD(File webRoot, boolean useHttps) {
super(FDroidApp.ipAddressString, FDroidApp.port);
this.logRequests = false;
this.webRoot = webRoot;
if (useHttps)
enableHTTPS();
}
/**
* URL-encodes everything between "/"-characters. Encodes spaces as '%20'
* instead of '+'.
*/
private String encodeUriBetweenSlashes(String uri) {
String newUri = "";
StringTokenizer st = new StringTokenizer(uri, "/ ", true);
while (st.hasMoreTokens()) {
String tok = st.nextToken();
if (tok.equals("/"))
newUri += "/";
else if (tok.equals(" "))
newUri += "%20";
else {
try {
newUri += URLEncoder.encode(tok, "UTF-8");
} catch (UnsupportedEncodingException ignored) {
}
}
}
return newUri;
}
@Override
public Response serve(IHTTPSession session) {
Map<String, String> header = session.getHeaders();
Map<String, String> parms = session.getParms();
String uri = session.getUri();
if (logRequests) {
Log.i(TAG, session.getMethod() + " '" + uri + "' ");
Iterator<String> e = header.keySet().iterator();
while (e.hasNext()) {
String value = e.next();
Log.i(TAG, " HDR: '" + value + "' = '" + header.get(value) + "'");
}
e = parms.keySet().iterator();
while (e.hasNext()) {
String value = e.next();
Log.i(TAG, " PRM: '" + value + "' = '" + parms.get(value) + "'");
}
}
if (!webRoot.isDirectory()) {
return createResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT,
"INTERNAL ERRROR: given path is not a directory (" + webRoot + ").");
}
return respond(Collections.unmodifiableMap(header), uri);
}
private void enableHTTPS() {
// TODO copy implementation from Kerplapp
}
private Response respond(Map<String, String> headers, String uri) {
// Remove URL arguments
uri = uri.trim().replace(File.separatorChar, '/');
if (uri.indexOf('?') >= 0) {
uri = uri.substring(0, uri.indexOf('?'));
}
// Prohibit getting out of current directory
if (uri.contains("../")) {
return createResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
"FORBIDDEN: Won't serve ../ for security reasons.");
}
File f = new File(webRoot, uri);
if (!f.exists()) {
return createResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT,
"Error 404, file not found.");
}
// Browsers get confused without '/' after the directory, send a
// redirect.
if (f.isDirectory() && !uri.endsWith("/")) {
uri += "/";
Response res = createResponse(Response.Status.REDIRECT, NanoHTTPD.MIME_HTML,
"<html><body>Redirected: <a href=\"" +
uri + "\">" + uri + "</a></body></html>");
res.addHeader("Location", uri);
return res;
}
if (f.isDirectory()) {
// First look for index files (index.html, index.htm, etc) and if
// none found, list the directory if readable.
String indexFile = findIndexFileInDirectory(f);
if (indexFile == null) {
if (f.canRead()) {
// No index file, list the directory if it is readable
return createResponse(Response.Status.OK, NanoHTTPD.MIME_HTML,
listDirectory(uri, f));
} else {
return createResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
"FORBIDDEN: No directory listing.");
}
} else {
return respond(headers, uri + indexFile);
}
}
Response response = null;
response = serveFile(uri, headers, f, getMimeTypeForFile(uri));
return response != null ? response :
createResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT,
"Error 404, file not found.");
}
/**
* Serves file from homeDir and its' subdirectories (only). Uses only URI,
* ignores all headers and HTTP parameters.
*/
Response serveFile(String uri, Map<String, String> header, File file, String mime) {
Response res;
try {
// Calculate etag
String etag = Integer
.toHexString((file.getAbsolutePath() + file.lastModified() + "" + file.length())
.hashCode());
// Support (simple) skipping:
long startFrom = 0;
long endAt = -1;
String range = header.get("range");
if (range != null) {
if (range.startsWith("bytes=")) {
range = range.substring("bytes=".length());
int minus = range.indexOf('-');
try {
if (minus > 0) {
startFrom = Long.parseLong(range.substring(0, minus));
endAt = Long.parseLong(range.substring(minus + 1));
}
} catch (NumberFormatException ignored) {
}
}
}
// Change return code and add Content-Range header when skipping is
// requested
long fileLen = file.length();
if (range != null && startFrom >= 0) {
if (startFrom >= fileLen) {
res = createResponse(Response.Status.RANGE_NOT_SATISFIABLE,
NanoHTTPD.MIME_PLAINTEXT, "");
res.addHeader("Content-Range", "bytes 0-0/" + fileLen);
res.addHeader("ETag", etag);
} else {
if (endAt < 0) {
endAt = fileLen - 1;
}
long newLen = endAt - startFrom + 1;
if (newLen < 0) {
newLen = 0;
}
final long dataLen = newLen;
FileInputStream fis = new FileInputStream(file) {
@Override
public int available() throws IOException {
return (int) dataLen;
}
};
fis.skip(startFrom);
res = createResponse(Response.Status.PARTIAL_CONTENT, mime, fis);
res.addHeader("Content-Length", "" + dataLen);
res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/"
+ fileLen);
res.addHeader("ETag", etag);
}
} else {
if (etag.equals(header.get("if-none-match")))
res = createResponse(Response.Status.NOT_MODIFIED, mime, "");
else {
res = createResponse(Response.Status.OK, mime, new FileInputStream(file));
res.addHeader("Content-Length", "" + fileLen);
res.addHeader("ETag", etag);
}
}
} catch (IOException ioe) {
res = createResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
"FORBIDDEN: Reading file failed.");
}
return res;
}
// Announce that the file server accepts partial content requests
private Response createResponse(Response.Status status, String mimeType, InputStream message) {
Response res = new Response(status, mimeType, message);
res.addHeader("Accept-Ranges", "bytes");
return res;
}
// Announce that the file server accepts partial content requests
private Response createResponse(Response.Status status, String mimeType, String message) {
Response res = new Response(status, mimeType, message);
res.addHeader("Accept-Ranges", "bytes");
return res;
}
public static String getMimeTypeForFile(String uri) {
String type = null;
String extension = MimeTypeMap.getFileExtensionFromUrl(uri);
if (extension != null) {
MimeTypeMap mime = MimeTypeMap.getSingleton();
type = mime.getMimeTypeFromExtension(extension);
}
return type;
}
private String findIndexFileInDirectory(File directory) {
String indexFileName = "index.html";
File indexFile = new File(directory, indexFileName);
if (indexFile.exists()) {
return indexFileName;
}
return null;
}
private String listDirectory(String uri, File f) {
String heading = "Directory " + uri;
StringBuilder msg = new StringBuilder("<html><head><title>" + heading
+ "</title><style><!--\n" +
"span.dirname { font-weight: bold; }\n" +
"span.filesize { font-size: 75%; }\n" +
"// -->\n" +
"</style>" +
"</head><body><h1>" + heading + "</h1>");
String up = null;
if (uri.length() > 1) {
String u = uri.substring(0, uri.length() - 1);
int slash = u.lastIndexOf('/');
if (slash >= 0 && slash < u.length()) {
up = uri.substring(0, slash + 1);
}
}
List<String> files = Arrays.asList(f.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return new File(dir, name).isFile();
}
}));
Collections.sort(files);
List<String> directories = Arrays.asList(f.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return new File(dir, name).isDirectory();
}
}));
Collections.sort(directories);
if (up != null || directories.size() + files.size() > 0) {
msg.append("<ul>");
if (up != null || directories.size() > 0) {
msg.append("<section class=\"directories\">");
if (up != null) {
msg.append("<li><a rel=\"directory\" href=\"").append(up)
.append("\"><span class=\"dirname\">..</span></a></b></li>");
}
for (String directory : directories) {
String dir = directory + "/";
msg.append("<li><a rel=\"directory\" href=\"").append(encodeUriBetweenSlashes(uri + dir))
.append("\"><span class=\"dirname\">").append(dir)
.append("</span></a></b></li>");
}
msg.append("</section>");
}
if (files.size() > 0) {
msg.append("<section class=\"files\">");
for (String file : files) {
msg.append("<li><a href=\"").append(encodeUriBetweenSlashes(uri + file))
.append("\"><span class=\"filename\">").append(file)
.append("</span></a>");
File curFile = new File(f, file);
long len = curFile.length();
msg.append("&nbsp;<span class=\"filesize\">(");
if (len < 1024) {
msg.append(len).append(" bytes");
} else if (len < 1024 * 1024) {
msg.append(len / 1024).append(".").append(len % 1024 / 10 % 100)
.append(" KB");
} else {
msg.append(len / (1024 * 1024)).append(".")
.append(len % (1024 * 1024) / 10 % 100).append(" MB");
}
msg.append(")</span></li>");
}
msg.append("</section>");
}
msg.append("</ul>");
}
msg.append("</body></html>");
return msg.toString();
}
}

View File

@ -0,0 +1,89 @@
package org.fdroid.fdroid.net;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.AsyncTask;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Utils;
import java.util.Locale;
public class WifiStateChangeService extends Service {
public static final String BROADCAST = "org.fdroid.fdroid.action.WIFI_CHANGE";
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new WaitForWifiAsyncTask().execute();
return START_NOT_STICKY;
}
public class WaitForWifiAsyncTask extends AsyncTask<Void, Void, Void> {
private static final String TAG = "WaitForWifiAsyncTask";
private WifiManager wifiManager;
@Override
protected Void doInBackground(Void... params) {
wifiManager = (WifiManager) getSystemService(WIFI_SERVICE);
try {
while (!wifiManager.isWifiEnabled()) {
Log.i(TAG, "waiting for the wifi to be enabled...");
Thread.sleep(3000);
}
int ipAddress = wifiManager.getConnectionInfo().getIpAddress();
while (ipAddress == 0) {
Log.i(TAG, "waiting for an IP address...");
Thread.sleep(3000);
ipAddress = wifiManager.getConnectionInfo().getIpAddress();
}
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
ipAddress = wifiInfo.getIpAddress();
FDroidApp.ipAddressString = String.format(Locale.ENGLISH, "%d.%d.%d.%d",
(ipAddress & 0xff),
(ipAddress >> 8 & 0xff),
(ipAddress >> 16 & 0xff),
(ipAddress >> 24 & 0xff));
FDroidApp.ssid = wifiInfo.getSSID().replaceAll("^\"(.*)\"$", "$1");
FDroidApp.bssid = wifiInfo.getBSSID();
String scheme;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(WifiStateChangeService.this);
if (prefs.getBoolean("use_https", false))
scheme = "https";
else
scheme = "http";
FDroidApp.repo.address = String.format(Locale.ENGLISH, "%s://%s:%d/fdroid/repo",
scheme, FDroidApp.ipAddressString, FDroidApp.port);
FDroidApp.localRepo.setUriString(FDroidApp.repo.address);
FDroidApp.localRepo.writeIndexPage(
Utils.getSharingUri(WifiStateChangeService.this, FDroidApp.repo).toString());
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(Void result) {
Intent intent = new Intent(BROADCAST);
LocalBroadcastManager.getInstance(WifiStateChangeService.this).sendBroadcast(intent);
WifiStateChangeService.this.stopSelf();
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}

View File

@ -43,6 +43,7 @@ abstract public class RepoUpdater {
protected final Repo repo;
private List<App> apps = new ArrayList<App>();
private List<Apk> apks = new ArrayList<Apk>();
protected boolean usePubkeyInJar = false;
protected boolean hasChanged = false;
protected ProgressListener progressListener;
@ -230,9 +231,13 @@ abstract public class RepoUpdater {
values.put(RepoProvider.DataColumns.LAST_ETAG, etag);
}
// We read an unsigned index, but that indicates that
// a signed version is now available...
if (handler.getPubKey() != null && repo.pubkey == null) {
/*
* We read an unsigned index that indicates that a signed version
* is available. Or we received a repo config that included the
* fingerprint, so we need to save the pubkey now.
*/
if (handler.getPubKey() != null &&
(repo.pubkey == null || usePubkeyInJar)) {
// 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
@ -241,6 +246,7 @@ abstract public class RepoUpdater {
Log.d("FDroid",
"Public key found - switching to signed repo for future updates");
values.put(RepoProvider.DataColumns.PUBLIC_KEY, handler.getPubKey());
usePubkeyInJar = false;
}
if (handler.getVersion() != -1 && handler.getVersion() != repo.version) {

View File

@ -31,6 +31,7 @@ public class SignedRepoUpdater extends RepoUpdater {
String certdata = Hasher.hex(cert);
if (repo.pubkey == null && repo.fingerprint.equals(Utils.calcFingerprint(cert))) {
repo.pubkey = certdata;
usePubkeyInJar = true;
}
if (repo.pubkey != null && repo.pubkey.equals(certdata)) {
match = true;
@ -51,6 +52,13 @@ public class SignedRepoUpdater extends RepoUpdater {
InputStream input = null;
OutputStream output = null;
try {
/*
* JarFile.getInputStream() provides the signature check, even
* though the Android docs do not mention this, the Java docs do
* and Android seems to implement it the same:
* http://docs.oracle.com/javase/6/docs/api/java/util/jar/JarFile.html#getInputStream(java.util.zip.ZipEntry)
* https://developer.android.com/reference/java/util/jar/JarFile.html#getInputStream(java.util.zip.ZipEntry)
*/
input = jarFile.getInputStream(indexEntry);
output = new FileOutputStream(indexFile);
Utils.copy(input, output);

View File

@ -1,10 +1,7 @@
package org.fdroid.fdroid.views;
import android.database.Cursor;
import android.net.Uri;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentPagerAdapter;
import org.fdroid.fdroid.FDroid;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.AppProvider;
@ -26,16 +23,10 @@ public class AppListFragmentPageAdapter extends FragmentPagerAdapter {
}
private String getUpdateTabTitle() {
Uri uri = AppProvider.getCanUpdateUri();
String[] projection = new String[] { AppProvider.DataColumns._COUNT };
Cursor cursor = parent.getContentResolver().query(uri, projection, null, null, null);
String suffix = "";
if (cursor != null && cursor.getCount() == 1) {
cursor.moveToFirst();
int count = cursor.getInt(0);
suffix = " (" + count + ")";
}
return parent.getString(R.string.tab_updates) + suffix;
int updateCount = AppProvider.Helper.count(parent, AppProvider.getCanUpdateUri());
// TODO: Make RTL friendly, probably by having a different string for both tab_updates_none and tab_updates
return parent.getString(R.string.tab_updates) + " (" + updateCount + ")";
}
@Override

View File

@ -1,37 +0,0 @@
package org.fdroid.fdroid.views;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import android.widget.ListView;
/**
* There are three main app-lists in the UI:
* - Available
* - Installed
* - Apps which can be updated
* This class provides a View which knows about these app lists, but can have
* different contents (e.g. a drop down list of categories). It allows us to
* get a reference to the selected item in the FDroid Activity, without having
* to know which list we are actually looking at.
*/
public class AppListView extends LinearLayout {
private ListView appList;
public AppListView(Context context) {
super(context);
}
public AppListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setAppList(ListView appList) {
this.appList = appList;
}
public ListView getAppList() {
return appList;
}
}

View File

@ -0,0 +1,330 @@
package org.fdroid.fdroid.views;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.*;
import android.content.res.Configuration;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.*;
import android.widget.*;
import org.fdroid.fdroid.*;
import org.fdroid.fdroid.localrepo.LocalRepoService;
import org.fdroid.fdroid.net.WifiStateChangeService;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
public class LocalRepoActivity extends Activity {
private static final String TAG = "LocalRepoActivity";
private ProgressDialog repoProgress;
private WifiManager wifiManager;
private Button enableWifiButton;
private CheckBox repoSwitch;
private Timer stopTimer;
private int SET_IP_ADDRESS = 7345;
private int UPDATE_REPO = 7346;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((FDroidApp) getApplication()).applyTheme(this);
setContentView(R.layout.local_repo_activity);
enableWifiButton = (Button) findViewById(R.id.enable_wifi);
repoSwitch = (CheckBox) findViewById(R.id.repoSwitch);
wifiManager = (WifiManager) getSystemService(WIFI_SERVICE);
}
@Override
public void onResume() {
super.onResume();
resetNetworkInfo();
LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
new IntentFilter(WifiStateChangeService.BROADCAST));
LocalBroadcastManager.getInstance(this).registerReceiver(onLocalRepoChange,
new IntentFilter(LocalRepoService.STATE));
// if no local repo exists, create one with only FDroid in it
if (!FDroidApp.localRepo.xmlIndex.exists())
new UpdateAsyncTask(this, new String[] {
getPackageName(),
}).execute();
// start repo by default
FDroidApp.startLocalRepoService(LocalRepoActivity.this);
// automatically turn off after 15 minutes
stopTimer = new Timer();
stopTimer.schedule(new TimerTask() {
@Override
public void run() {
FDroidApp.stopLocalRepoService(LocalRepoActivity.this);
}
}, 900000); // 15 minutes
}
@Override
public void onPause() {
super.onPause();
LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange);
LocalBroadcastManager.getInstance(this).unregisterReceiver(onLocalRepoChange);
}
private BroadcastReceiver onWifiChange = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent i) {
resetNetworkInfo();
}
};
private BroadcastReceiver onLocalRepoChange = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent i) {
String state = i.getStringExtra(LocalRepoService.STATE);
if (state != null && state.equals(LocalRepoService.STARTED))
setRepoSwitchChecked(true);
else
setRepoSwitchChecked(false);
}
};
private void resetNetworkInfo() {
int wifiState = wifiManager.getWifiState();
if (wifiState == WifiManager.WIFI_STATE_ENABLED) {
setUIFromWifi();
wireRepoSwitchToWebServer();
repoSwitch.setVisibility(View.VISIBLE);
enableWifiButton.setVisibility(View.GONE);
} else {
repoSwitch.setChecked(false);
repoSwitch.setVisibility(View.GONE);
enableWifiButton.setVisibility(View.VISIBLE);
enableWifiButton.setText(R.string.enable_wifi);
enableWifiButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
enableWifiButton.setText(R.string.enabling_wifi);
wifiManager.setWifiEnabled(true);
/*
* Once the wifi is connected to a network, then
* WifiStateChangeReceiver will receive notice, and kick off
* the process of getting the info about the wifi
* connection.
*/
}
});
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.local_repo_activity, menu);
if (Build.VERSION.SDK_INT < 11) // TODO remove after including appcompat-v7
menu.findItem(R.id.menu_setup_repo).setVisible(false);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_setup_repo:
startActivityForResult(new Intent(this, SelectLocalAppsActivity.class), UPDATE_REPO);
return true;
case R.id.menu_send_fdroid_via_wifi:
startActivity(new Intent(this, QrWizardWifiNetworkActivity.class));
return true;
case R.id.menu_settings:
startActivityForResult(new Intent(this, PreferencesActivity.class), SET_IP_ADDRESS);
return true;
}
return false;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK)
return;
if (requestCode == SET_IP_ADDRESS) {
setUIFromWifi();
} else if (requestCode == UPDATE_REPO) {
setUIFromWifi();
new UpdateAsyncTask(this, FDroidApp.selectedApps.toArray(new String[0]))
.execute();
}
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case 0:
repoProgress = new ProgressDialog(this);
repoProgress.setMessage("Scanning Apps. Please wait...");
repoProgress.setIndeterminate(false);
repoProgress.setMax(100);
repoProgress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
repoProgress.setCancelable(false);
repoProgress.show();
return repoProgress;
default:
return null;
}
}
private void wireRepoSwitchToWebServer() {
repoSwitch.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setRepoSwitchChecked(repoSwitch.isChecked());
if (repoSwitch.isChecked()) {
FDroidApp.startLocalRepoService(LocalRepoActivity.this);
} else {
FDroidApp.stopLocalRepoService(LocalRepoActivity.this);
stopTimer.cancel(); // disable automatic stop
}
}
});
}
private void setRepoSwitchChecked(boolean checked) {
repoSwitch.setChecked(checked);
if (checked) {
repoSwitch.setText(R.string.local_repo_running);
} else {
repoSwitch.setText(R.string.touch_to_turn_on_local_repo);
}
}
@TargetApi(14)
private void setUIFromWifi() {
if (TextUtils.isEmpty(FDroidApp.repo.address))
return;
// the fingerprint is not useful on the button label
String buttonLabel = FDroidApp.repo.address.replaceAll("\\?.*$", "");
TextView sharingUriTextView = (TextView) findViewById(R.id.sharing_uri);
sharingUriTextView.setText(buttonLabel);
/*
* Set URL to UPPER for compact QR Code, FDroid will translate it back.
* Remove the SSID from the query string since SSIDs are case-sensitive.
* Instead the receiver will have to rely on the BSSID to find the right
* wifi AP to join. Lots of QR Scanners are buggy and do not respect
* custom URI schemes, so we have to use http:// or https:// :-(
*/
final String qrUriString = Utils.getSharingUri(this, FDroidApp.repo).toString()
.replaceFirst("fdroidrepo", "http")
.replaceAll("ssid=[^?]*", "")
.toUpperCase(Locale.ENGLISH);
Log.i("QRURI", qrUriString);
if (Build.VERSION.SDK_INT >= 8) // zxing requires >= 8
new QrGenAsyncTask(this, R.id.repoQrCode).execute(qrUriString);
TextView wifiNetworkNameTextView = (TextView) findViewById(R.id.wifi_network);
wifiNetworkNameTextView.setText(FDroidApp.ssid);
TextView fingerprintTextView = (TextView) findViewById(R.id.fingerprint);
if (FDroidApp.repo.fingerprint != null) {
fingerprintTextView.setVisibility(View.VISIBLE);
fingerprintTextView.setText(FDroidApp.repo.fingerprint);
} else {
fingerprintTextView.setVisibility(View.GONE);
}
// the required NFC API was added in 4.0 aka Ice Cream Sandwich
if (Build.VERSION.SDK_INT >= 14) {
NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
if (nfcAdapter == null)
return;
nfcAdapter.setNdefPushMessage(new NdefMessage(new NdefRecord[] {
NdefRecord.createUri(Utils.getSharingUri(this, FDroidApp.repo)),
}), this);
}
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
// ignore orientation/keyboard change
super.onConfigurationChanged(newConfig);
}
class UpdateAsyncTask extends AsyncTask<Void, String, Void> {
private static final String TAG = "UpdateAsyncTask";
private ProgressDialog progressDialog;
private String[] selectedApps;
private Uri sharingUri;
public UpdateAsyncTask(Context c, String[] apps) {
selectedApps = apps;
progressDialog = new ProgressDialog(c);
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.setTitle(R.string.updating);
sharingUri = Utils.getSharingUri(c, FDroidApp.repo);
}
@Override
protected void onPreExecute() {
progressDialog.show();
}
@Override
protected Void doInBackground(Void... params) {
try {
publishProgress(getString(R.string.deleting_repo));
FDroidApp.localRepo.deleteRepo();
for (String app : selectedApps) {
publishProgress(String.format(getString(R.string.adding_apks_format), app));
FDroidApp.localRepo.addApp(getApplicationContext(), app);
}
FDroidApp.localRepo.writeIndexPage(sharingUri.toString());
publishProgress(getString(R.string.writing_index_xml));
FDroidApp.localRepo.writeIndexXML();
publishProgress(getString(R.string.linking_apks));
FDroidApp.localRepo.copyApksToRepo();
publishProgress(getString(R.string.copying_icons));
// run the icon copy without progress, its not a blocker
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
FDroidApp.localRepo.copyIconsToRepo();
return null;
}
}.execute();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onProgressUpdate(String... progress) {
super.onProgressUpdate(progress);
progressDialog.setMessage(progress[0]);
}
@Override
protected void onPostExecute(Void result) {
progressDialog.dismiss();
Toast.makeText(getBaseContext(), R.string.updated_local_repo, Toast.LENGTH_SHORT)
.show();
}
}
}

View File

@ -0,0 +1,81 @@
package org.fdroid.fdroid.views;
import android.app.Activity;
import android.content.*;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.QrGenAsyncTask;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.net.WifiStateChangeService;
public class QrWizardDownloadActivity extends Activity {
private static final String TAG = "QrWizardDownloadActivity";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((FDroidApp) getApplication()).applyTheme(this);
setContentView(R.layout.qr_wizard_activity);
TextView instructions = (TextView) findViewById(R.id.qrWizardInstructions);
instructions.setText(R.string.qr_wizard_download_instructions);
Button next = (Button) findViewById(R.id.qrNextButton);
next.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
setResult(RESULT_OK);
finish();
}
});
}
@Override
public void onResume() {
super.onResume();
resetNetworkInfo();
LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
new IntentFilter(WifiStateChangeService.BROADCAST));
}
@Override
public void onPause() {
super.onPause();
LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange);
}
private BroadcastReceiver onWifiChange = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent i) {
Log.i(TAG, "onWifiChange.onReceive()");
resetNetworkInfo();
}
};
private void resetNetworkInfo() {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
String qrString = "";
if (prefs.getBoolean("use_https", false))
qrString += "https";
else
qrString += "http";
qrString += "://" + FDroidApp.ipAddressString;
qrString += ":" + FDroidApp.port;
if (Build.VERSION.SDK_INT >= 8) // zxing requires >= 8
new QrGenAsyncTask(this, R.id.qrWizardImage).execute(qrString);
Log.i(TAG, "qr: " + qrString);
TextView wifiNetworkName = (TextView) findViewById(R.id.qrWifiNetworkName);
wifiNetworkName.setText(qrString.replaceFirst("http://", ""));
}
}

View File

@ -0,0 +1,108 @@
package org.fdroid.fdroid.views;
import android.app.Activity;
import android.content.*;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.QrGenAsyncTask;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.net.WifiStateChangeService;
public class QrWizardWifiNetworkActivity extends Activity {
private static final String TAG = "QrWizardWifiNetworkActivity";
private WifiManager wifiManager;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
wifiManager = (WifiManager) getSystemService(WIFI_SERVICE);
wifiManager.setWifiEnabled(true);
FDroidApp.startLocalRepoService(this);
((FDroidApp) getApplication()).applyTheme(this);
setContentView(R.layout.qr_wizard_activity);
TextView instructions = (TextView) findViewById(R.id.qrWizardInstructions);
instructions.setText(R.string.qr_wizard_wifi_network_instructions);
Button next = (Button) findViewById(R.id.qrNextButton);
next.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(getBaseContext(), QrWizardDownloadActivity.class);
startActivityForResult(intent, 0);
finish();
}
});
}
@Override
public void onResume() {
super.onResume();
resetNetworkInfo();
LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
new IntentFilter(WifiStateChangeService.BROADCAST));
}
@Override
public void onPause() {
super.onPause();
LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange);
}
private BroadcastReceiver onWifiChange = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent i) {
Log.i(TAG, "onWifiChange.onReceive()");
resetNetworkInfo();
}
};
private void resetNetworkInfo() {
int wifiState = wifiManager.getWifiState();
if (wifiState == WifiManager.WIFI_STATE_ENABLED) {
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
// http://zxing.appspot.com/generator/
// WIFI:S:openwireless.org;; // no pw
// WIFI:S:openwireless.org;T:WPA;;
// WIFI:S:openwireless.org;T:WEP;;
// WIFI:S:openwireless.org;H:true;; // hidden
// WIFI:S:openwireless.org;T:WPA;H:true;; // all
String qrString = "WIFI:S:";
qrString += wifiInfo.getSSID();
// TODO get encryption state (none, WEP, WPA)
/*
* WifiConfiguration wc = null; for (WifiConfiguration i :
* wifiManager.getConfiguredNetworks()) { if (i.status ==
* WifiConfiguration.Status.CURRENT) { wc = i; break; } } if (wc !=
* null)
*/
if (wifiInfo.getHiddenSSID())
qrString += ";H:true";
qrString += ";;";
if (Build.VERSION.SDK_INT >= 8) // zxing requires >= 8
new QrGenAsyncTask(this, R.id.qrWizardImage).execute(qrString);
TextView wifiNetworkName = (TextView) findViewById(R.id.qrWifiNetworkName);
wifiNetworkName.setText(wifiInfo.getSSID());
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// this wizard is done, clear this Activity from the history
if (resultCode == RESULT_OK)
finish();
}
}

View File

@ -97,10 +97,16 @@ public class RepoAdapter extends CursorAdapter {
// 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);
TextView signedView = (TextView) view.findViewById(R.id.repo_unsigned);
if (repo.isSigned()) {
signedView.setVisibility(View.INVISIBLE);
} else if (repo.isSignedButUnverified()) {
signedView.setText(R.string.unverified);
signedView.setTextColor(view.getResources().getColor(R.color.unverified));
signedView.setVisibility(View.VISIBLE);
} else {
signedView.setText(R.string.unsigned);
signedView.setTextColor(view.getResources().getColor(R.color.unsigned));
signedView.setVisibility(View.VISIBLE);
}
}

View File

@ -4,8 +4,6 @@ package org.fdroid.fdroid.views;
import android.annotation.TargetApi;
import android.content.Intent;
import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
@ -13,12 +11,12 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.app.FragmentActivity;
import android.text.TextUtils;
import android.util.Log;
import android.widget.LinearLayout;
import android.widget.Toast;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.ActionBarCompat;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
@ -27,7 +25,6 @@ import org.fdroid.fdroid.views.fragments.RepoDetailsFragment;
public class RepoDetailsActivity extends FragmentActivity {
public static final String TAG = "RepoDetailsActivity";
private WifiManager wifiManager;
private Repo repo;
static final String MIME_TYPE = "application/vnd.org.fdroid.fdroid.repo";
@ -67,8 +64,6 @@ public class RepoDetailsActivity extends FragmentActivity {
ActionBarCompat.create(this).setDisplayHomeAsUpEnabled(true);
setTitle(repo.getName());
wifiManager = (WifiManager) getSystemService(WIFI_SERVICE);
}
@TargetApi(14)
@ -80,7 +75,7 @@ public class RepoDetailsActivity extends FragmentActivity {
return;
}
nfcAdapter.setNdefPushMessage(new NdefMessage(new NdefRecord[] {
NdefRecord.createUri(getSharingUri()),
NdefRecord.createUri(Utils.getSharingUri(this, repo)),
}), this);
findViewById(android.R.id.content).post(new Runnable() {
@Override
@ -128,19 +123,4 @@ public class RepoDetailsActivity extends FragmentActivity {
finish();
}
}
protected Uri getSharingUri() {
Uri uri = Uri.parse(repo.address.replaceFirst("http", "fdroidrepo"));
Uri.Builder b = uri.buildUpon();
b.appendQueryParameter("fingerprint", repo.fingerprint);
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
String ssid = wifiInfo.getSSID().replaceAll("^\"(.*)\"$", "$1");
String bssid = wifiInfo.getBSSID();
if (!TextUtils.isEmpty(bssid)) {
b.appendQueryParameter("bssid", Uri.encode(bssid));
if (!TextUtils.isEmpty(ssid))
b.appendQueryParameter("ssid", Uri.encode(ssid));
}
return b.build();
}
}

View File

@ -0,0 +1,97 @@
package org.fdroid.fdroid.views;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.*;
import android.widget.SearchView;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.PreferencesActivity;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.views.fragments.SelectLocalAppsFragment;
@TargetApi(11)
// TODO replace with appcompat-v7
public class SelectLocalAppsActivity extends Activity {
private static final String TAG = "SelectLocalAppsActivity";
private SelectLocalAppsFragment selectLocalAppsFragment = null;
private SearchView searchView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((FDroidApp) getApplication()).applyTheme(this);
setContentView(R.layout.select_local_apps_activity);
}
@Override
protected void onResume() {
super.onResume();
if (selectLocalAppsFragment == null)
selectLocalAppsFragment = (SelectLocalAppsFragment) getFragmentManager()
.findFragmentById(R.id.fragment_app_list);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.select_local_apps_activity, menu);
searchView = (SearchView) menu.findItem(R.id.action_search).getActionView();
searchView.setOnQueryTextListener(selectLocalAppsFragment);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
setResult(RESULT_CANCELED);
finish();
return true;
case R.id.action_search:
SearchView searchView = (SearchView) item.getActionView();
searchView.setIconified(false);
return true;
case R.id.action_settings:
startActivity(new Intent(this, PreferencesActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
public ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.select_local_apps_action_mode, menu);
menu.findItem(R.id.action_search).setActionView(searchView);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false; // Return false if nothing is done
}
@Override
public boolean onActionItemClicked(final ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.action_update_repo:
setResult(RESULT_OK);
finish();
return true;
default:
return false;
}
}
@Override
public void onDestroyActionMode(ActionMode mode) {
setResult(RESULT_CANCELED);
finish();
}
};
}

View File

@ -63,7 +63,6 @@ abstract public class AppListFragment extends ListFragment implements
// onActivityCreated" according to the docs.
getListView().setFastScrollEnabled(true);
getListView().setOnItemClickListener(this);
getListView().setOnScrollListener(new PauseOnScrollListener(ImageLoader.getInstance(), false, true));
}
@Override

View File

@ -63,8 +63,6 @@ public class RepoListFragment extends ListFragment
private final int UPDATE_REPOS = 2;
private final int SCAN_FOR_REPOS = 3;
private WifiManager wifiManager;
public boolean hasChanged() {
return changed;
}
@ -85,13 +83,11 @@ public class RepoListFragment extends ListFragment
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
Log.i("FDroid", "Repo cursor loaded.");
repoAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
Log.i("FDroid", "Repo cursor reset.");
repoAdapter.swapCursor(null);
}
@ -186,79 +182,6 @@ public class RepoListFragment extends ListFragment
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
/* let's see if someone is trying to send us a new repo */
Intent intent = getActivity().getIntent();
/* an URL from a click, NFC, QRCode scan, etc */
Uri uri = intent.getData();
if (uri != null) {
// scheme and host should only ever be pure ASCII aka Locale.ENGLISH
String scheme = intent.getScheme();
String host = uri.getHost();
if (scheme == null || host == null) {
String msg = String.format(getString(R.string.malformed_repo_uri), uri);
Toast.makeText(getActivity(), msg, Toast.LENGTH_LONG).show();
return;
}
if (scheme.equals("FDROIDREPO") || scheme.equals("FDROIDREPOS")) {
/*
* QRCodes are more efficient in all upper case, so QR URIs are
* encoded in all upper case, then forced to lower case.
* Checking if the special F-Droid scheme being all is upper
* case means it should be downcased.
*/
uri = Uri.parse(uri.toString().toLowerCase(Locale.ENGLISH));
} else if (uri.getPath().startsWith("/FDROID/REPO")) {
/*
* some QR scanners chop off the fdroidrepo:// and just try
* http://, then the incoming URI does not get downcased
* properly, and the query string is stripped off. So just
* downcase the path, and carry on to get something working.
*/
uri = Uri.parse(uri.toString().toLowerCase(Locale.ENGLISH));
}
// make scheme and host lowercase so they're readable in dialogs
scheme = scheme.toLowerCase(Locale.ENGLISH);
host = host.toLowerCase(Locale.ENGLISH);
String fingerprint = uri.getQueryParameter("fingerprint");
if (scheme.equals("fdroidrepos") || scheme.equals("fdroidrepo")
|| scheme.equals("https") || scheme.equals("http")) {
isImportingRepo = true;
/* sanitize and format for function and readability */
String uriString = uri.toString()
.replaceAll("\\?.*$", "") // remove the whole query
.replaceAll("/*$", "") // remove all trailing slashes
.replace(uri.getHost(), host) // downcase host name
.replace(intent.getScheme(), scheme) // downcase scheme
.replace("fdroidrepo", "http"); // proper repo address
showAddRepo(uriString, fingerprint);
// if this is a local repo, check we're on the same wifi
String uriBssid = uri.getQueryParameter("bssid");
if (!TextUtils.isEmpty(uriBssid)) {
if (uri.getPort() != 8888
&& !host.matches("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+")) {
Log.i("ManageRepo", "URI is not local repo: " + uri);
return;
}
Activity a = getActivity();
if (wifiManager == null)
wifiManager = (WifiManager) a.getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
String bssid = wifiInfo.getBSSID().toLowerCase(Locale.ENGLISH);
uriBssid = Uri.decode(uriBssid).toLowerCase(Locale.ENGLISH);
if (!bssid.equals(uriBssid)) {
String msg = String.format(getString(R.string.not_on_same_wifi),
uri.getQueryParameter("ssid"));
Toast.makeText(a, msg, Toast.LENGTH_LONG).show();
}
// TODO we should help the user to the right thing here,
// instead of just showing a message!
}
}
}
}
@Override
@ -375,6 +298,11 @@ public class RepoListFragment extends ListFragment
nsdHelper.discoverServices();
}
public void importRepo(String uri, String fingerprint) {
isImportingRepo = true;
showAddRepo(uri, fingerprint);
}
private void showAddRepo() {
showAddRepo(getNewRepoUri(), null);
}
@ -385,6 +313,11 @@ public class RepoListFragment extends ListFragment
final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri);
final EditText fingerprintEditText = (EditText) view.findViewById(R.id.edit_fingerprint);
/*
* If the "add new repo" dialog is launched by an action outside of
* FDroid, i.e. a URL, then check to see if any existing repos match,
* and change the action accordingly.
*/
final Repo repo = (newAddress != null && isImportingRepo)
? RepoProvider.Helper.findByAddress(getActivity(), newAddress)
: null;
@ -431,6 +364,7 @@ public class RepoListFragment extends ListFragment
final Button addButton = alrt.getButton(DialogInterface.BUTTON_POSITIVE);
alrt.setTitle(R.string.repo_exists);
overwriteMessage.setVisibility(View.VISIBLE);
if (newFingerprint != null)
newFingerprint = newFingerprint.toUpperCase(Locale.ENGLISH);
if (repo.fingerprint == null && newFingerprint != null) {
// we're upgrading from unsigned to signed repo

View File

@ -0,0 +1,214 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.fdroid.fdroid.views.fragments;
import android.annotation.TargetApi;
import android.app.ListFragment;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.CursorLoader;
import android.content.Loader;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.ActionMode;
import android.view.View;
import android.widget.*;
import android.widget.SearchView.OnQueryTextListener;
import android.widget.SimpleCursorAdapter.ViewBinder;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.data.InstalledAppProvider.DataColumns;
import org.fdroid.fdroid.views.SelectLocalAppsActivity;
import java.util.HashSet;
//TODO replace with appcompat-v7
@TargetApi(11)
public class SelectLocalAppsFragment extends ListFragment
implements LoaderCallbacks<Cursor>, OnQueryTextListener {
private PackageManager packageManager;
private Drawable defaultAppIcon;
private SelectLocalAppsActivity selectLocalAppsActivity;
private ActionMode mActionMode = null;
private String mCurrentFilterString;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setEmptyText(getString(R.string.no_applications_found));
packageManager = getActivity().getPackageManager();
defaultAppIcon = getActivity().getResources()
.getDrawable(android.R.drawable.sym_def_app_icon);
selectLocalAppsActivity = (SelectLocalAppsActivity) getActivity();
ListView listView = getListView();
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
SimpleCursorAdapter adapter = new SimpleCursorAdapter(getActivity(),
R.layout.select_local_apps_list_item,
null,
new String[] {
InstalledAppProvider.DataColumns.APPLICATION_LABEL,
InstalledAppProvider.DataColumns.APP_ID,
},
new int[] {
R.id.application_label,
R.id.package_name,
},
0);
adapter.setViewBinder(new ViewBinder() {
@Override
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
Log.i("SelectLocalAppsFragment", "ViewBinder " + columnIndex);
if (columnIndex == cursor.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)) {
String packageName = cursor.getString(columnIndex);
TextView textView = (TextView) view.findViewById(R.id.package_name);
textView.setText(packageName);
LinearLayout ll = (LinearLayout) view.getParent().getParent();
ImageView iconView = (ImageView) ll.getChildAt(0);
Drawable icon;
try {
icon = packageManager.getApplicationIcon(packageName);
} catch (NameNotFoundException e) {
icon = defaultAppIcon;
}
iconView.setImageDrawable(icon);
return true;
}
return false;
}
});
setListAdapter(adapter);
setListShown(false); // start out with a progress indicator
// either reconnect with an existing loader or start a new one
getLoaderManager().initLoader(0, null, this);
// build list of existing apps from what is on the file system
if (FDroidApp.selectedApps == null) {
FDroidApp.selectedApps = new HashSet<String>();
for (String filename : FDroidApp.localRepo.repoDir.list()) {
if (filename.matches(".*\\.apk")) {
String packageName = filename.substring(0, filename.indexOf("_"));
FDroidApp.selectedApps.add(packageName);
}
}
}
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
if (mActionMode == null)
mActionMode = selectLocalAppsActivity
.startActionMode(selectLocalAppsActivity.mActionModeCallback);
Cursor c = (Cursor) l.getAdapter().getItem(position);
String packageName = c.getString(c.getColumnIndex(DataColumns.APP_ID));
if (FDroidApp.selectedApps.contains(packageName)) {
FDroidApp.selectedApps.remove(packageName);
} else {
FDroidApp.selectedApps.add(packageName);
}
}
@Override
public CursorLoader onCreateLoader(int id, Bundle args) {
Uri baseUri;
if (TextUtils.isEmpty(mCurrentFilterString)) {
baseUri = InstalledAppProvider.getContentUri();
} else {
baseUri = InstalledAppProvider.getSearchUri(mCurrentFilterString);
}
CursorLoader loader = new CursorLoader(
this.getActivity(),
baseUri,
InstalledAppProvider.DataColumns.ALL,
null,
null,
InstalledAppProvider.DataColumns.APPLICATION_LABEL);
return loader;
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
((SimpleCursorAdapter) this.getListAdapter()).swapCursor(cursor);
ListView listView = getListView();
int count = listView.getCount();
String fdroid = loader.getContext().getPackageName();
for (int i = 0; i < count; i++) {
Cursor c = ((Cursor) listView.getItemAtPosition(i));
String packageName = c.getString(c.getColumnIndex(DataColumns.APP_ID));
if (TextUtils.equals(packageName, fdroid)) {
listView.setItemChecked(i, true); // always include FDroid
} else {
for (String selected : FDroidApp.selectedApps) {
if (TextUtils.equals(packageName, selected)) {
listView.setItemChecked(i, true);
}
}
}
}
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
((SimpleCursorAdapter) this.getListAdapter()).swapCursor(null);
}
@Override
public boolean onQueryTextChange(String newText) {
String newFilter = !TextUtils.isEmpty(newText) ? newText : null;
if (mCurrentFilterString == null && newFilter == null) {
return true;
}
if (mCurrentFilterString != null && mCurrentFilterString.equals(newFilter)) {
return true;
}
mCurrentFilterString = newFilter;
getLoaderManager().restartLoader(0, null, this);
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
// this is not needed since we respond to every change in text
return true;
}
public String getCurrentFilterString() {
return mCurrentFilterString;
}
}

BIN
test/assets/simpleIndex.jar Normal file

Binary file not shown.

View File

@ -0,0 +1,6 @@
<?xml version="1.0" ?><fdroid><repo icon="fdroid-icon.png" name="F-Droid" pubkey="308201ee30820157a0030201020204300d845b300d06092a864886f70d01010b0500302a3110300e060355040b1307462d44726f6964311630140603550403130d70616c6174736368696e6b656e301e170d3134303432373030303633315a170d3431303931323030303633315a302a3110300e060355040b1307462d44726f6964311630140603550403130d70616c6174736368696e6b656e30819f300d06092a864886f70d010101050003818d0030818902818100a439472e4b6d01141bfc94ecfe131c7c728fdda670bb14c57ca60bd1c38a8b8bc0879d22a0a2d0bc0d6fdd4cb98d1d607c2caefbe250a0bd0322aedeb365caf9b236992fac13e6675d3184a6c7c6f07f73410209e399a9da8d5d7512bbd870508eebacff8b57c3852457419434d34701ccbf692267cbc3f42f1c5d1e23762d790203010001a321301f301d0603551d0e041604140b1840691dab909746fde4bfe28207d1cae15786300d06092a864886f70d01010b05000381810062424c928ffd1b6fd419b44daafef01ca982e09341f7077fb865905087aeac882534b3bd679b51fdfb98892cef38b63131c567ed26c9d5d9163afc775ac98ad88c405d211d6187bde0b0d236381cc574ba06ef9080721a92ae5a103a7301b2c397eecc141cc850dd3e123813ebc41c59d31ddbcb6e984168280c53272f6a442b" timestamp="1398733213" url="https://f-droid.org/repo" version="12"><description>
The official repository of the F-Droid client. Applications in this repository
are either official binaries built by the original application developers, or
are binaries built from source by the admin of f-droid.org using the tools on
https://gitorious.org/f-droid.
</description></repo></fdroid>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,5 @@
Downloaded from:
https://archive.apache.org/dist/commons/io/binaries/commons-io-2.2-bin.zip
https://archive.apache.org/dist/commons/io/binaries/commons-io-2.2-bin.zip.asc
https://archive.apache.org/dist/commons/io/binaries/commons-io-2.2-bin.zip.sha1

View File

@ -9,9 +9,11 @@ import android.net.Uri;
import android.os.Build;
import android.provider.ContactsContract;
import android.test.ProviderTestCase2MockContext;
import mock.MockContextEmptyComponents;
import mock.MockContextSwappableComponents;
import mock.MockFDroidResources;
import org.fdroid.fdroid.data.*;
import java.util.List;
@ -151,6 +153,7 @@ public abstract class FDroidProviderTest<T extends FDroidProvider> extends Provi
InstalledAppProvider.DataColumns.APP_ID,
InstalledAppProvider.DataColumns.VERSION_CODE,
InstalledAppProvider.DataColumns.VERSION_NAME,
InstalledAppProvider.DataColumns.APPLICATION_LABEL,
};
Cursor cursor = getMockContentResolver().query(uri, projection, null, null, null);

View File

@ -0,0 +1,129 @@
package org.fdroid.fdroid;
import android.test.AndroidTestCase;
public class UtilsTest extends AndroidTestCase {
String fdroidFingerprint = "43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB";
String fdroidPubkey = "3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef";
String gpRepoFingerprint = "59050C8155DCA377F23D5A15B77D3713400CDBD8B42FBFBE0E3F38096E68CECE";
String gpRepoPubkey = "308203c5308202ada00302010202047b7cf549300d06092a864886f70d01010b0500308192310b30090603550406130255533111300f060355040813084e657720596f726b3111300f060355040713084e657720596f726b311d301b060355040a131454686520477561726469616e2050726f6a656374311f301d060355040b1316477561726469616e20462d44726f6964204275696c64311d301b06035504031314677561726469616e70726f6a6563742e696e666f301e170d3132313032393130323530305a170d3430303331363130323530305a308192310b30090603550406130255533111300f060355040813084e657720596f726b3111300f060355040713084e657720596f726b311d301b060355040a131454686520477561726469616e2050726f6a656374311f301d060355040b1316477561726469616e20462d44726f6964204275696c64311d301b06035504031314677561726469616e70726f6a6563742e696e666f30820122300d06092a864886f70d01010105000382010f003082010a0282010100b7f1f635fa3fce1a8042aaa960c2dc557e4ad2c082e5787488cba587fd26207cf59507919fc4dcebda5c8c0959d14146d0445593aa6c29dc639570b71712451fd5c231b0c9f5f0bec380503a1c2a3bc00048bc5db682915afa54d1ecf67b45e1e05c0934b3037a33d3a565899131f27a72c03a5de93df17a2376cc3107f03ee9d124c474dfab30d4053e8f39f292e2dcb6cc131bce12a0c5fc307985195d256bf1d7a2703d67c14bf18ed6b772bb847370b20335810e337c064fef7e2795a524c664a853cd46accb8494f865164dabfb698fa8318236432758bc40d52db00d5ce07fe2210dc06cd95298b4f09e6c9b7b7af61c1d62ea43ea36a2331e7b2d4e250203010001a321301f301d0603551d0e0416041404d763e981cf3a295b94a790d8536a783097232b300d06092a864886f70d01010b05000382010100654e6484ff032c54fed1d96d3c8e731302be9dbd7bb4fe635f2dac05b69f3ecbb5acb7c9fe405e2a066567a8f5c2beb8b199b5a4d5bb1b435cf02df026d4fb4edd9d8849078f085b00950083052d57467d65c6eebd98f037cff9b148d621cf8819c4f7dc1459bf8fc5c7d76f901495a7caf35d1e5c106e1d50610c4920c3c1b50adcfbd4ad83ce7353cdea7d856bba0419c224f89a2f3ebc203d20eb6247711ad2b55fd4737936dc42ced7a047cbbd24012079204a2883b6d55d5d5b66d9fd82fb51fca9a5db5fad9af8564cb380ff30ae8263dbbf01b46e01313f53279673daa3f893380285646b244359203e7eecde94ae141b7dfa8e6499bb8e7e0b25ab85";
String gpTest0Fingerprint = "C4DC0B2AB5AB58F0CDBF97FF903CF12415F468D90B11877803BC172D31012B2E";
String gpTest0Pubkey = "308204f3308202dba003020102020436aac0dc300d06092a864886f70d01010b0500302a3110300e060355040b1307462d44726f6964311630140603550403130d70616c6174736368696e6b656e301e170d3133313130353232353534325a170d3133313130363232353534325a302a3110300e060355040b1307462d44726f6964311630140603550403130d70616c6174736368696e6b656e30820222300d06092a864886f70d01010105000382020f003082020a0282020100b1f3cd3db9207f80e9d854159d40a15344bfcc377fba61983d1ac843e52e2fc1a81d96325174328f77dbe382b2b239567d50ad2e1fea13f1272b0370693acd03b9aef3e5a908118065f21193735552c123a9f59f99c2822b7bba7082c72649e17666ac70d332f1c7cf20830373c86f11d2f80a2aa0307c3b526b8769b69371555540f246ca892db4b51226788bb3b869284254266f3ccb1d7b5b08a2cf398f53877b09da0f1cc922ecc928c477660979d07998b29678feaea9b5c93d3a12f89f695eeda766280df22b688e1da15d979845a81c81f9d1252e2e5fd415df2eb0f28cb339a9d9bc13ec1a059333ca766a0982464f8d9a67397f066b3926aa5ac6f2216962da5705d2b9723353ac3b670f5ab4d365cde4e5d0375ca52e7e8c151dd90eda0025be09feae4c94c59608243b45f0527ad8d46e0a0bc97ac27870af53c0550706502ecfa56a30d7442012e6115ada79243481b759319def848199df423c9664574d8d8a7f8949e9f3549e8695fa0b02eab1dc8e30c6432d159076ceb91b709bd848f4e5d74a0880c1ead0534b1f8a354edd05a6d7b44f9a566f9e419bab6834ff2f2d2a54c797b407ccb2d4648247e69b2b85186f9ebd087879a580be281b73f46975e5c94b5a935adf019d6d56992742ebb23865f94a14ed17fc7fb0fbea43eb686760298ae98b197ac8da2ec0b61be240b6f2a574208da9e0fd9e14d90203010001a321301f301d0603551d0e04160414282e3362786f92645dd7809905166e473bbfc722300d06092a864886f70d01010b05000382020100295efaa7d0e985b408a7c6f2891cae1fa7b6338774eee624edd838c0fbaadc755d140ed6007b91e662434010659a4a5597709e23828a1a5e9846b4369ee8fcef10b85fc64db7726aee8c8d93753d4828250323ebdb768ed9958f4c2c61eb48d2329a0196a47898662ed9418e5ba223c4c1e285e94bfe0f5d5b4813b9d0b6b49d304a79879698d320e1ff5e36be441f1dcda5715d4644825d669b15de2765d285253231fbe052360426fe976af404381909043cfe8e7a537275dc75f367235eb0fc357884ea36f00cdb21fbc75ca2ac9c53adc202456e40d0e950af09c4f5de3d876f43fda7880be4800ff2635f681c19a5b8f1cd68319e78f5ff8e29f5225db849f03d473926aa2d492df970cbcba266211003e7c84f5852ea089b62679acd6243e56b18384596443c379effa1419027345bb929a46193c5c1f6274c83f14a8720189ab178945ef56a99fb16ac8aedb6d8b9ec10bd1f6935b189aa9470947d909bf8d0630e8b189696a79e9560631fa79cc22cddc17594c2c86e03fa7102811154d10aa2ff631732c491969b8a356632eabcf22596db4eb011cfaf426ec972967e2c5f426fa1f3e9d8c1d90fbb7665660d02456d9b7c1fa6bb68b0d53c29c6ef4e7c81f91c1819f754a53a03124a36b539cde945287c5be8817244c1548c17ff671f729545dc9155c94f01ceb620333f10000acbeba866cb242155daa76a5169";
String gpTest1Fingerprint = "C63AED1AC79D37C7B0474472AC6EFA6C3AB2B11A767A4F42CF360FA5496E3C50";
String gpTest1Pubkey = "3082039a30820282020900aa6887be1ec84bde300d06092a864886f70d010105050030818e310b30090603550406130255533111300f06035504080c084e657720596f726b311e301c060355040a0c15477561726469616e2050726f6a65637420546573743122302006035504030c19746573742e677561726469616e70726f6a6563742e696e666f3128302606092a864886f70d01090116197465737440677561726469616e70726f6a6563742e696e666f301e170d3134303332383230343132365a170d3431303831323230343132365a30818e310b30090603550406130255533111300f06035504080c084e657720596f726b311e301c060355040a0c15477561726469616e2050726f6a65637420546573743122302006035504030c19746573742e677561726469616e70726f6a6563742e696e666f3128302606092a864886f70d01090116197465737440677561726469616e70726f6a6563742e696e666f30820122300d06092a864886f70d01010105000382010f003082010a02820101009f4895a4a160d14e9de49dd61ac9434715c2aea25a9de75f0361e3f9bd77306cff7a8f508f9a9edc31dfb5b3aa2571e22b1711c08f0616892fa4efdf94321ec93211486b314bcf27385f670492683a0e50f5a022ede2bfc00c69b14e8c8678f313d6d280feb9c53445f087fa9d12a31392ca63d75351587e3cd2337fbf95fd7c2a9322883d74f18680165a697d4a1a4fa3bd835bd45f00561447350af4ec6b6740c0ae7950ff53c386a2efc43a280e4270912d20eb464761799fdbbae50dd0df01f9b25673499029a2e869203e7d63e7ca98826dabf856c965f472de691ddc77f6ed8db468684baf76f7f1cdf7fc3a07109ad8aea8e332a807bedbb8143bbe230203010001300d06092a864886f70d010105050003820101005284015baba5eb092a3c681634b46b9f59a0dbb651c89ca65af730bfeb22726e048194cbd54fb4242f5ec8e514e26dd8887cbcb431f3f2eb224780b6a2204e614d705aed4bd66e153c216d35e1dc1e38e226566af74bb229a2416ea6ffb388d6f64a68386332f34f50d48b630541e2871030bd27d90a1688f46bff4e9707059cd22e56820a4a3d01f9a91b442f6adf0776d9f73533a2dcd7214305491414dbc7c734166cd833e227f9bd8a82b3d464c662c71a07703fb14de0564cad1d3851e35cc9a04ce36fde2abf8d8d9dec07752e535f35aabc3632d6d2106086477e346efebb0d4bec7afc461d7ab7f96200c2dadb2da41d09342aa2fa9ab94ab92d2053";
// this pair has one digit missing from the pubkey
String pubkeyShortByOneFingerprint = "C63AED1AC79D37C7B0474472AC6EFA6C3AB2B11A767A4F42CF360FA5496E3C50";
String pubkeyShortByOnePubkey = "3082039a30820282020900aa6887be1ec84bde300d06092a86488f70d010105050030818e310b30090603550406130255533111300f06035504080c084e657720596f726b311e301c060355040a0c15477561726469616e2050726f6a65637420546573743122302006035504030c19746573742e677561726469616e70726f6a6563742e696e666f3128302606092a864886f70d01090116197465737440677561726469616e70726f6a6563742e696e666f301e170d3134303332383230343132365a170d3431303831323230343132365a30818e310b30090603550406130255533111300f06035504080c084e657720596f726b311e301c060355040a0c15477561726469616e2050726f6a65637420546573743122302006035504030c19746573742e677561726469616e70726f6a6563742e696e666f3128302606092a864886f70d01090116197465737440677561726469616e70726f6a6563742e696e666f30820122300d06092a864886f70d01010105000382010f003082010a02820101009f4895a4a160d14e9de49dd61ac9434715c2aea25a9de75f0361e3f9bd77306cff7a8f508f9a9edc31dfb5b3aa2571e22b1711c08f0616892fa4efdf94321ec93211486b314bcf27385f670492683a0e50f5a022ede2bfc00c69b14e8c8678f313d6d280feb9c53445f087fa9d12a31392ca63d75351587e3cd2337fbf95fd7c2a9322883d74f18680165a697d4a1a4fa3bd835bd45f00561447350af4ec6b6740c0ae7950ff53c386a2efc43a280e4270912d20eb464761799fdbbae50dd0df01f9b25673499029a2e869203e7d63e7ca98826dabf856c965f472de691ddc77f6ed8db468684baf76f7f1cdf7fc3a07109ad8aea8e332a807bedbb8143bbe230203010001300d06092a864886f70d010105050003820101005284015baba5eb092a3c681634b46b9f59a0dbb651c89ca65af730bfeb22726e048194cbd54fb4242f5ec8e514e26dd8887cbcb431f3f2eb224780b6a2204e614d705aed4bd66e153c216d35e1dc1e38e226566af74bb229a2416ea6ffb388d6f64a68386332f34f50d48b630541e2871030bd27d90a1688f46bff4e9707059cd22e56820a4a3d01f9a91b442f6adf0776d9f73533a2dcd7214305491414dbc7c734166cd833e227f9bd8a82b3d464c662c71a07703fb14de0564cad1d3851e35cc9a04ce36fde2abf8d8d9dec07752e535f35aabc3632d6d2106086477e346efebb0d4bec7afc461d7ab7f96200c2dadb2da41d09342aa2fa9ab94ab92d2053";
// this pair has one digit missing from the fingerprint
String fingerprintShortByOneFingerprint = "C63AED1AC79D37C7B047442AC6EFA6C3AB2B11A767A4F42CF360FA5496E3C50";
String fingerprintShortByOnePubkey = "3082039a30820282020900aa6887be1ec84bde300d06092a864886f70d010105050030818e310b30090603550406130255533111300f06035504080c084e657720596f726b311e301c060355040a0c15477561726469616e2050726f6a65637420546573743122302006035504030c19746573742e677561726469616e70726f6a6563742e696e666f3128302606092a864886f70d01090116197465737440677561726469616e70726f6a6563742e696e666f301e170d3134303332383230343132365a170d3431303831323230343132365a30818e310b30090603550406130255533111300f06035504080c084e657720596f726b311e301c060355040a0c15477561726469616e2050726f6a65637420546573743122302006035504030c19746573742e677561726469616e70726f6a6563742e696e666f3128302606092a864886f70d01090116197465737440677561726469616e70726f6a6563742e696e666f30820122300d06092a864886f70d01010105000382010f003082010a02820101009f4895a4a160d14e9de49dd61ac9434715c2aea25a9de75f0361e3f9bd77306cff7a8f508f9a9edc31dfb5b3aa2571e22b1711c08f0616892fa4efdf94321ec93211486b314bcf27385f670492683a0e50f5a022ede2bfc00c69b14e8c8678f313d6d280feb9c53445f087fa9d12a31392ca63d75351587e3cd2337fbf95fd7c2a9322883d74f18680165a697d4a1a4fa3bd835bd45f00561447350af4ec6b6740c0ae7950ff53c386a2efc43a280e4270912d20eb464761799fdbbae50dd0df01f9b25673499029a2e869203e7d63e7ca98826dabf856c965f472de691ddc77f6ed8db468684baf76f7f1cdf7fc3a07109ad8aea8e332a807bedbb8143bbe230203010001300d06092a864886f70d010105050003820101005284015baba5eb092a3c681634b46b9f59a0dbb651c89ca65af730bfeb22726e048194cbd54fb4242f5ec8e514e26dd8887cbcb431f3f2eb224780b6a2204e614d705aed4bd66e153c216d35e1dc1e38e226566af74bb229a2416ea6ffb388d6f64a68386332f34f50d48b630541e2871030bd27d90a1688f46bff4e9707059cd22e56820a4a3d01f9a91b442f6adf0776d9f73533a2dcd7214305491414dbc7c734166cd833e227f9bd8a82b3d464c662c71a07703fb14de0564cad1d3851e35cc9a04ce36fde2abf8d8d9dec07752e535f35aabc3632d6d2106086477e346efebb0d4bec7afc461d7ab7f96200c2dadb2da41d09342aa2fa9ab94ab92d2053";
// this pair has one digit added to the pubkey
String pubkeyLongByOneFingerprint = "59050C8155DCA377F23D5A15B77D3713400CDBD8B42FBFBE0E3F38096E68CECE";
String pubkeyLongByOnePubkey = "308203c5308202ada00302010202047b7cf5493000d06092a864886f70d01010b0500308192310b30090603550406130255533111300f060355040813084e657720596f726b3111300f060355040713084e657720596f726b311d301b060355040a131454686520477561726469616e2050726f6a656374311f301d060355040b1316477561726469616e20462d44726f6964204275696c64311d301b06035504031314677561726469616e70726f6a6563742e696e666f301e170d3132313032393130323530305a170d3430303331363130323530305a308192310b30090603550406130255533111300f060355040813084e657720596f726b3111300f060355040713084e657720596f726b311d301b060355040a131454686520477561726469616e2050726f6a656374311f301d060355040b1316477561726469616e20462d44726f6964204275696c64311d301b06035504031314677561726469616e70726f6a6563742e696e666f30820122300d06092a864886f70d01010105000382010f003082010a0282010100b7f1f635fa3fce1a8042aaa960c2dc557e4ad2c082e5787488cba587fd26207cf59507919fc4dcebda5c8c0959d14146d0445593aa6c29dc639570b71712451fd5c231b0c9f5f0bec380503a1c2a3bc00048bc5db682915afa54d1ecf67b45e1e05c0934b3037a33d3a565899131f27a72c03a5de93df17a2376cc3107f03ee9d124c474dfab30d4053e8f39f292e2dcb6cc131bce12a0c5fc307985195d256bf1d7a2703d67c14bf18ed6b772bb847370b20335810e337c064fef7e2795a524c664a853cd46accb8494f865164dabfb698fa8318236432758bc40d52db00d5ce07fe2210dc06cd95298b4f09e6c9b7b7af61c1d62ea43ea36a2331e7b2d4e250203010001a321301f301d0603551d0e0416041404d763e981cf3a295b94a790d8536a783097232b300d06092a864886f70d01010b05000382010100654e6484ff032c54fed1d96d3c8e731302be9dbd7bb4fe635f2dac05b69f3ecbb5acb7c9fe405e2a066567a8f5c2beb8b199b5a4d5bb1b435cf02df026d4fb4edd9d8849078f085b00950083052d57467d65c6eebd98f037cff9b148d621cf8819c4f7dc1459bf8fc5c7d76f901495a7caf35d1e5c106e1d50610c4920c3c1b50adcfbd4ad83ce7353cdea7d856bba0419c224f89a2f3ebc203d20eb6247711ad2b55fd4737936dc42ced7a047cbbd24012079204a2883b6d55d5d5b66d9fd82fb51fca9a5db5fad9af8564cb380ff30ae8263dbbf01b46e01313f53279673daa3f893380285646b244359203e7eecde94ae141b7dfa8e6499bb8e7e0b25ab85";
// this pair has one digit added to the fingerprint
String fingerprintLongByOneFingerprint = "59050C8155DCA377F23D5A15B77D37134000CDBD8B42FBFBE0E3F38096E68CECE";
String fingerprintLongByOnePubkey = "308203c5308202ada00302010202047b7cf549300d06092a864886f70d01010b0500308192310b30090603550406130255533111300f060355040813084e657720596f726b3111300f060355040713084e657720596f726b311d301b060355040a131454686520477561726469616e2050726f6a656374311f301d060355040b1316477561726469616e20462d44726f6964204275696c64311d301b06035504031314677561726469616e70726f6a6563742e696e666f301e170d3132313032393130323530305a170d3430303331363130323530305a308192310b30090603550406130255533111300f060355040813084e657720596f726b3111300f060355040713084e657720596f726b311d301b060355040a131454686520477561726469616e2050726f6a656374311f301d060355040b1316477561726469616e20462d44726f6964204275696c64311d301b06035504031314677561726469616e70726f6a6563742e696e666f30820122300d06092a864886f70d01010105000382010f003082010a0282010100b7f1f635fa3fce1a8042aaa960c2dc557e4ad2c082e5787488cba587fd26207cf59507919fc4dcebda5c8c0959d14146d0445593aa6c29dc639570b71712451fd5c231b0c9f5f0bec380503a1c2a3bc00048bc5db682915afa54d1ecf67b45e1e05c0934b3037a33d3a565899131f27a72c03a5de93df17a2376cc3107f03ee9d124c474dfab30d4053e8f39f292e2dcb6cc131bce12a0c5fc307985195d256bf1d7a2703d67c14bf18ed6b772bb847370b20335810e337c064fef7e2795a524c664a853cd46accb8494f865164dabfb698fa8318236432758bc40d52db00d5ce07fe2210dc06cd95298b4f09e6c9b7b7af61c1d62ea43ea36a2331e7b2d4e250203010001a321301f301d0603551d0e0416041404d763e981cf3a295b94a790d8536a783097232b300d06092a864886f70d01010b05000382010100654e6484ff032c54fed1d96d3c8e731302be9dbd7bb4fe635f2dac05b69f3ecbb5acb7c9fe405e2a066567a8f5c2beb8b199b5a4d5bb1b435cf02df026d4fb4edd9d8849078f085b00950083052d57467d65c6eebd98f037cff9b148d621cf8819c4f7dc1459bf8fc5c7d76f901495a7caf35d1e5c106e1d50610c4920c3c1b50adcfbd4ad83ce7353cdea7d856bba0419c224f89a2f3ebc203d20eb6247711ad2b55fd4737936dc42ced7a047cbbd24012079204a2883b6d55d5d5b66d9fd82fb51fca9a5db5fad9af8564cb380ff30ae8263dbbf01b46e01313f53279673daa3f893380285646b244359203e7eecde94ae141b7dfa8e6499bb8e7e0b25ab85";
public void testFormatFingerprint() {
String badResult = Utils.formatFingerprint("");
// real fingerprints
String formatted = null;
formatted = Utils.formatFingerprint(fdroidFingerprint);
assertFalse(formatted.equals(badResult));
assertTrue(formatted.matches("[A-Z0-9][A-Z0-9] [A-Z0-9 ]+"));
formatted = Utils.formatFingerprint(gpRepoFingerprint);
assertFalse(formatted.equals(badResult));
assertTrue(formatted.matches("[A-Z0-9][A-Z0-9] [A-Z0-9 ]+"));
formatted = Utils.formatFingerprint(gpTest1Fingerprint);
assertFalse(formatted.equals(badResult));
assertTrue(formatted.matches("[A-Z0-9][A-Z0-9] [A-Z0-9 ]+"));
// random garbage
assertEquals(
badResult,
Utils.formatFingerprint("234k2lk3jljwlk4j2lk3jlkmqwekljrlkj34lk2jlk2j34lkjl2k3j4lk2j34lja"));
assertEquals(
badResult,
Utils.formatFingerprint("g000000000000000000000000000000000000000000000000000000000000000"));
assertEquals(
badResult,
Utils.formatFingerprint("98273498723948728934789237489273p1928731982731982739182739817238"));
// too short
assertEquals(
badResult,
Utils.formatFingerprint("C63AED1AC79D37C7B0474472AC6EFA6C3AB2B11A767A4F42CF360FA5496E3C5"));
assertEquals(
badResult,
Utils.formatFingerprint("C63AED1"));
assertEquals(
badResult,
Utils.formatFingerprint("f"));
assertEquals(
badResult,
Utils.formatFingerprint(""));
assertEquals(
badResult,
Utils.formatFingerprint(null));
// real digits but too long
assertEquals(
badResult,
Utils.formatFingerprint("43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB"));
assertEquals(
badResult,
Utils.formatFingerprint("C63AED1AC79D37C7B0474472AC6EFA6C3AB2B11A767A4F42CF360FA5496E3C50F"));
assertEquals(
badResult,
Utils.formatFingerprint("3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef"));
}
public void testCalcFingerprintString() {
// these should pass
assertEquals(fdroidFingerprint, Utils.calcFingerprint(fdroidPubkey));
assertEquals(gpRepoFingerprint, Utils.calcFingerprint(gpRepoPubkey));
assertEquals(gpTest0Fingerprint, Utils.calcFingerprint(gpTest0Pubkey));
assertEquals(gpTest1Fingerprint, Utils.calcFingerprint(gpTest1Pubkey));
// these should fail
assertFalse(gpRepoFingerprint.equals(
Utils.calcFingerprint(fdroidPubkey)));
assertFalse(gpTest0Fingerprint.equals(
Utils.calcFingerprint(fdroidPubkey)));
assertFalse(gpTest1Fingerprint.equals(
Utils.calcFingerprint(fdroidPubkey)));
assertFalse(fdroidFingerprint.equals(
Utils.calcFingerprint(gpRepoPubkey)));
assertFalse(gpTest0Fingerprint.equals(
Utils.calcFingerprint(gpRepoPubkey)));
assertFalse(gpTest1Fingerprint.equals(
Utils.calcFingerprint(gpRepoPubkey)));
assertFalse(fingerprintShortByOneFingerprint.equals(
Utils.calcFingerprint(fingerprintShortByOnePubkey)));
assertFalse(fingerprintLongByOneFingerprint.equals(
Utils.calcFingerprint(fingerprintLongByOnePubkey)));
try {
assertFalse(pubkeyShortByOneFingerprint.equals(
Utils.calcFingerprint(pubkeyShortByOnePubkey)));
} catch (ArrayIndexOutOfBoundsException e) {
assertTrue(true); // we should get this Exception!
}
try {
assertFalse(pubkeyLongByOneFingerprint.equals(
Utils.calcFingerprint(pubkeyLongByOnePubkey)));
} catch (ArrayIndexOutOfBoundsException e) {
assertTrue(true); // we should get this Exception!
}
}
public void testCalcFingerprintCertificate() {
// TODO write tests that work with a Certificate
}
}

View File

@ -0,0 +1,143 @@
package org.fdroid.fdroid.updater;
import android.content.Context;
import android.test.InstrumentationTestCase;
import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.updater.RepoUpdater.UpdateException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class SignedRepoUpdaterTest extends InstrumentationTestCase {
private Context context;
private RepoUpdater repoUpdater;
String simpleIndexPubkey = "308201ee30820157a0030201020204300d845b300d06092a864886f70d01010b0500302a3110300e060355040b1307462d44726f6964311630140603550403130d70616c6174736368696e6b656e301e170d3134303432373030303633315a170d3431303931323030303633315a302a3110300e060355040b1307462d44726f6964311630140603550403130d70616c6174736368696e6b656e30819f300d06092a864886f70d010101050003818d0030818902818100a439472e4b6d01141bfc94ecfe131c7c728fdda670bb14c57ca60bd1c38a8b8bc0879d22a0a2d0bc0d6fdd4cb98d1d607c2caefbe250a0bd0322aedeb365caf9b236992fac13e6675d3184a6c7c6f07f73410209e399a9da8d5d7512bbd870508eebacff8b57c3852457419434d34701ccbf692267cbc3f42f1c5d1e23762d790203010001a321301f301d0603551d0e041604140b1840691dab909746fde4bfe28207d1cae15786300d06092a864886f70d01010b05000381810062424c928ffd1b6fd419b44daafef01ca982e09341f7077fb865905087aeac882534b3bd679b51fdfb98892cef38b63131c567ed26c9d5d9163afc775ac98ad88c405d211d6187bde0b0d236381cc574ba06ef9080721a92ae5a103a7301b2c397eecc141cc850dd3e123813ebc41c59d31ddbcb6e984168280c53272f6a442b";
@Override
protected void setUp() {
context = getInstrumentation().getContext();
Repo repo = new Repo();
repo.pubkey = this.simpleIndexPubkey;
repoUpdater = RepoUpdater.createUpdaterFor(context, repo);
}
private InputStream getInputStreamFromAssets(String fileName) {
try {
return context.getResources().getAssets().open(fileName);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private File getTestFile(String fileName) {
File indexFile;
InputStream input = null;
OutputStream output = null;
try {
indexFile = File.createTempFile("index-", ".xml",
getInstrumentation().getTargetContext().getCacheDir());
input = getInputStreamFromAssets(fileName);
output = new FileOutputStream(indexFile);
Utils.copy(input, output);
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
Utils.closeQuietly(output);
Utils.closeQuietly(input);
}
return indexFile;
}
public void testExtractIndexFromJar() {
File simpleIndexXml = getTestFile("simpleIndex.xml");
File simpleIndexJar = getTestFile("simpleIndex.jar");
File testFile = null;
// these are supposed to succeed
try {
testFile = repoUpdater.getIndexFromFile(simpleIndexJar);
assertTrue(testFile.length() == simpleIndexXml.length());
assertEquals(FileUtils.readFileToString(testFile),
FileUtils.readFileToString(simpleIndexXml));
} catch (IOException e) {
e.printStackTrace();
fail();
} catch (UpdateException e) {
e.printStackTrace();
fail();
}
}
public void testExtractIndexFromJarWithoutSignatureJar() {
// this is supposed to fail
try {
repoUpdater.getIndexFromFile(getTestFile("simpleIndexWithoutSignature.jar"));
fail();
} catch (UpdateException e) {
// success!
}
}
public void testExtractIndexFromJarWithCorruptedManifestJar() {
// this is supposed to fail
try {
repoUpdater.getIndexFromFile(getTestFile("simpleIndexWithCorruptedManifest.jar"));
fail();
} catch (UpdateException e) {
e.printStackTrace();
fail();
} catch (SecurityException e) {
// success!
}
}
public void testExtractIndexFromJarWithCorruptedSignature() {
// this is supposed to fail
try {
repoUpdater.getIndexFromFile(getTestFile("simpleIndexWithCorruptedSignature.jar"));
fail();
} catch (UpdateException e) {
e.printStackTrace();
fail();
} catch (SecurityException e) {
// success!
}
}
public void testExtractIndexFromJarWithCorruptedCertificate() {
// this is supposed to fail
try {
repoUpdater.getIndexFromFile(getTestFile("simpleIndexWithCorruptedCertificate.jar"));
fail();
} catch (UpdateException e) {
e.printStackTrace();
fail();
} catch (SecurityException e) {
// success!
}
}
public void testExtractIndexFromJarWithCorruptedEverything() {
// this is supposed to fail
try {
repoUpdater.getIndexFromFile(getTestFile("simpleIndexWithCorruptedEverything.jar"));
fail();
} catch (UpdateException e) {
e.printStackTrace();
fail();
} catch (SecurityException e) {
// success!
}
}
}