Merge master into super-fdroid
This commit is contained in:
commit
919f9c63b8
@ -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
4
.gitmodules
vendored
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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" />
|
||||
```
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
65
assets/index.template.html
Normal file
65
assets/index.template.html
Normal 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
1
extern/nanohttpd
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 73cb37f863c41b8f021019703d6453887b656dd6
|
BIN
libs/core-3.0.1.jar
Normal file
BIN
libs/core-3.0.1.jar
Normal file
Binary file not shown.
17
libs/core-3.0.1.jar.README
Normal file
17
libs/core-3.0.1.jar.README
Normal 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
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
/>
|
||||
|
||||
|
95
res/layout/local_repo_activity.xml
Normal file
95
res/layout/local_repo_activity.xml
Normal 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>
|
44
res/layout/qr_wizard_activity.xml
Normal file
44
res/layout/qr_wizard_activity.xml
Normal 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>
|
12
res/layout/select_local_apps_activity.xml
Normal file
12
res/layout/select_local_apps_activity.xml
Normal 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>
|
58
res/layout/select_local_apps_list_item.xml
Normal file
58
res/layout/select_local_apps_list_item.xml
Normal 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>
|
20
res/menu/local_repo_activity.xml
Normal file
20
res/menu/local_repo_activity.xml
Normal 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>
|
14
res/menu/select_local_apps_action_mode.xml
Normal file
14
res/menu/select_local_apps_action_mode.xml
Normal 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>
|
15
res/menu/select_local_apps_activity.xml
Normal file
15
res/menu/select_local_apps_activity.xml
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -2,4 +2,5 @@
|
||||
<resources>
|
||||
<color name="signed">#ffcccccc</color>
|
||||
<color name="unsigned">#ffCC0000</color>
|
||||
<color name="unverified">#ff999999</color>
|
||||
</resources>
|
@ -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>
|
||||
|
@ -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…</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…</string>
|
||||
<string name="update_repo">Update Repo</string>
|
||||
<string name="deleting_repo">Deleting current repo…</string>
|
||||
<string name="adding_apks_format">Adding %s to repo…</string>
|
||||
<string name="writing_index_xml">Writing raw index file (index.xml)…</string>
|
||||
<string name="linking_apks">Linking APKs into the repo…</string>
|
||||
<string name="copying_icons">Copying app icons into the repo…</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…</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…</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>
|
||||
|
114
src/com/google/zxing/encode/Contents.java
Executable file
114
src/com/google/zxing/encode/Contents.java
Executable 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
|
||||
};
|
||||
|
||||
}
|
248
src/com/google/zxing/encode/QRCodeEncoder.java
Executable file
248
src/com/google/zxing/encode/QRCodeEncoder.java
Executable 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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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,15 +67,26 @@ public class ManageRepo extends FragmentActivity {
|
||||
setContentView( new LinearLayout(this) );
|
||||
|
||||
listFragment = new RepoListFragment();
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.add(android.R.id.content, listFragment)
|
||||
.commit();
|
||||
fm.beginTransaction()
|
||||
.add(android.R.id.content, listFragment)
|
||||
.commit();
|
||||
}
|
||||
|
||||
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!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
75
src/org/fdroid/fdroid/QrGenAsyncTask.java
Normal file
75
src/org/fdroid/fdroid/QrGenAsyncTask.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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())
|
||||
dialog.dismiss();
|
||||
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());
|
||||
|
@ -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(" ", "-");
|
||||
}
|
||||
|
||||
}
|
||||
|
21
src/org/fdroid/fdroid/WifiStateChangeReceiver.java
Normal file
21
src/org/fdroid/fdroid/WifiStateChangeReceiver.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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,11 +250,11 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
addLastUpdatedToRepo(db, oldVersion);
|
||||
renameRepoId(db, oldVersion);
|
||||
populateRepoNames(db, oldVersion);
|
||||
|
||||
if (oldVersion < 43) createInstalledApp(db);
|
||||
addAppLabelToInstalledCache(db, oldVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Migrate repo list to new structure. (No way to change primary
|
||||
* key in sqlite - table must be recreated).
|
||||
*/
|
||||
@ -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 )
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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() {
|
||||
|
419
src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
Normal file
419
src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
Normal 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);
|
||||
}
|
||||
}
|
172
src/org/fdroid/fdroid/localrepo/LocalRepoService.java
Normal file
172
src/org/fdroid/fdroid/localrepo/LocalRepoService.java
Normal 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);
|
||||
}
|
||||
}
|
341
src/org/fdroid/fdroid/net/LocalHTTPD.java
Normal file
341
src/org/fdroid/fdroid/net/LocalHTTPD.java
Normal 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(" <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();
|
||||
}
|
||||
}
|
89
src/org/fdroid/fdroid/net/WifiStateChangeService.java
Normal file
89
src/org/fdroid/fdroid/net/WifiStateChangeService.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
330
src/org/fdroid/fdroid/views/LocalRepoActivity.java
Normal file
330
src/org/fdroid/fdroid/views/LocalRepoActivity.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
81
src/org/fdroid/fdroid/views/QrWizardDownloadActivity.java
Normal file
81
src/org/fdroid/fdroid/views/QrWizardDownloadActivity.java
Normal 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://", ""));
|
||||
}
|
||||
}
|
108
src/org/fdroid/fdroid/views/QrWizardWifiNetworkActivity.java
Normal file
108
src/org/fdroid/fdroid/views/QrWizardWifiNetworkActivity.java
Normal 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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
97
src/org/fdroid/fdroid/views/SelectLocalAppsActivity.java
Normal file
97
src/org/fdroid/fdroid/views/SelectLocalAppsActivity.java
Normal 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();
|
||||
}
|
||||
};
|
||||
}
|
@ -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
|
||||
|
@ -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,7 +364,8 @@ public class RepoListFragment extends ListFragment
|
||||
final Button addButton = alrt.getButton(DialogInterface.BUTTON_POSITIVE);
|
||||
alrt.setTitle(R.string.repo_exists);
|
||||
overwriteMessage.setVisibility(View.VISIBLE);
|
||||
newFingerprint = newFingerprint.toUpperCase(Locale.ENGLISH);
|
||||
if (newFingerprint != null)
|
||||
newFingerprint = newFingerprint.toUpperCase(Locale.ENGLISH);
|
||||
if (repo.fingerprint == null && newFingerprint != null) {
|
||||
// we're upgrading from unsigned to signed repo
|
||||
overwriteMessage.setText(R.string.repo_exists_add_fingerprint);
|
||||
|
@ -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
BIN
test/assets/simpleIndex.jar
Normal file
Binary file not shown.
6
test/assets/simpleIndex.xml
Normal file
6
test/assets/simpleIndex.xml
Normal 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>
|
BIN
test/assets/simpleIndexWithCorruptedCertificate.jar
Normal file
BIN
test/assets/simpleIndexWithCorruptedCertificate.jar
Normal file
Binary file not shown.
BIN
test/assets/simpleIndexWithCorruptedEverything.jar
Normal file
BIN
test/assets/simpleIndexWithCorruptedEverything.jar
Normal file
Binary file not shown.
BIN
test/assets/simpleIndexWithCorruptedManifest.jar
Normal file
BIN
test/assets/simpleIndexWithCorruptedManifest.jar
Normal file
Binary file not shown.
BIN
test/assets/simpleIndexWithCorruptedSignature.jar
Normal file
BIN
test/assets/simpleIndexWithCorruptedSignature.jar
Normal file
Binary file not shown.
BIN
test/assets/simpleIndexWithoutSignature.jar
Normal file
BIN
test/assets/simpleIndexWithoutSignature.jar
Normal file
Binary file not shown.
BIN
test/libs/commons-io-2.2.jar
Normal file
BIN
test/libs/commons-io-2.2.jar
Normal file
Binary file not shown.
5
test/libs/commons-io-2.2.jar.README
Normal file
5
test/libs/commons-io-2.2.jar.README
Normal 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
|
@ -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);
|
||||
|
129
test/src/org/fdroid/fdroid/UtilsTest.java
Normal file
129
test/src/org/fdroid/fdroid/UtilsTest.java
Normal 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
|
||||
}
|
||||
}
|
143
test/src/org/fdroid/fdroid/updater/SignedRepoUpdaterTest.java
Normal file
143
test/src/org/fdroid/fdroid/updater/SignedRepoUpdaterTest.java
Normal 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!
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user