Merge branch 'merge_requests/11' of https://gitlab.com/eighthave/fdroidclient
This commit is contained in:
commit
15a3d74ada
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -10,3 +10,6 @@
|
||||
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
|
||||
|
@ -33,6 +33,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" />
|
||||
@ -181,6 +182,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"
|
||||
@ -303,8 +327,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>
|
||||
|
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 58f73260ed98df878bae1051dc8e6a0bce842fcb
|
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
|
63
res/layout/local_repo_activity.xml
Normal file
63
res/layout/local_repo_activity.xml
Normal file
@ -0,0 +1,63 @@
|
||||
<?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" >
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/repoSwitch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/wifiNetwork"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:text="@string/wifi_network" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/wifiNetworkName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginLeft="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:typeface="monospace" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/instrucionsTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="20dp"
|
||||
android:layout_marginRight="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>
|
43
res/layout/qr_wizard_activity.xml
Normal file
43
res/layout/qr_wizard_activity.xml
Normal file
@ -0,0 +1,43 @@
|
||||
<?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: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>
|
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>
|
9
res/menu/select_local_apps_action_mode.xml
Normal file
9
res/menu/select_local_apps_action_mode.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
|
||||
<item
|
||||
android:id="@+id/action_update_repo"
|
||||
android:icon="@android:drawable/ic_input_add"
|
||||
android:showAsAction="ifRoom|withText"
|
||||
android:title="@string/update_repo"/>
|
||||
|
||||
</menu>
|
10
res/menu/select_local_apps_activity.xml
Normal file
10
res/menu/select_local_apps_activity.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
|
||||
<item
|
||||
android:id="@+id/action_settings"
|
||||
android:orderInCategory="100"
|
||||
android:showAsAction="never"
|
||||
android:icon="@android:drawable/ic_menu_preferences"
|
||||
android:title="@string/menu_preferences"/>
|
||||
|
||||
</menu>
|
@ -150,8 +150,33 @@
|
||||
<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="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="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:
|
||||
|
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();
|
||||
}
|
||||
}
|
1
src/fi/iki/elonen/InternalRewrite.java
Symbolic link
1
src/fi/iki/elonen/InternalRewrite.java
Symbolic link
@ -0,0 +1 @@
|
||||
../../../../extern/nanohttpd/webserver/src/main/java/fi/iki/elonen/InternalRewrite.java
|
1
src/fi/iki/elonen/NanoHTTPD.java
Symbolic link
1
src/fi/iki/elonen/NanoHTTPD.java
Symbolic link
@ -0,0 +1 @@
|
||||
../../../../extern/nanohttpd/core/src/main/java/fi/iki/elonen/NanoHTTPD.java
|
1
src/fi/iki/elonen/ServerRunner.java
Symbolic link
1
src/fi/iki/elonen/ServerRunner.java
Symbolic link
@ -0,0 +1 @@
|
||||
../../../../extern/nanohttpd/webserver/src/main/java/fi/iki/elonen/ServerRunner.java
|
1
src/fi/iki/elonen/WebServerPlugin.java
Symbolic link
1
src/fi/iki/elonen/WebServerPlugin.java
Symbolic link
@ -0,0 +1 @@
|
||||
../../../../extern/nanohttpd/webserver/src/main/java/fi/iki/elonen/WebServerPlugin.java
|
1
src/fi/iki/elonen/WebServerPluginInfo.java
Symbolic link
1
src/fi/iki/elonen/WebServerPluginInfo.java
Symbolic link
@ -0,0 +1 @@
|
||||
../../../../extern/nanohttpd/webserver/src/main/java/fi/iki/elonen/WebServerPluginInfo.java
|
@ -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,19 +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.lang.Thread;
|
||||
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 {
|
||||
@ -96,6 +125,8 @@ public class FDroidApp extends Application {
|
||||
//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
|
||||
@ -196,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)
|
||||
@ -249,4 +287,41 @@ 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) {
|
||||
context.bindService(new Intent(context, LocalRepoService.class),
|
||||
serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
localRepoServiceIsBound = true;
|
||||
}
|
||||
|
||||
public static void stopLocalRepoService(Context context) {
|
||||
if (localRepoServiceIsBound) {
|
||||
context.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));
|
||||
}
|
||||
|
73
src/org/fdroid/fdroid/QrGenAsyncTask.java
Normal file
73
src/org/fdroid/fdroid/QrGenAsyncTask.java
Normal file
@ -0,0 +1,73 @@
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -19,9 +19,11 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.res.AssetManager;
|
||||
import android.content.res.XmlResourceParser;
|
||||
import android.net.Uri;
|
||||
import android.net.wifi.WifiInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
@ -29,14 +31,13 @@ import android.util.Log;
|
||||
import com.nostra13.universalimageloader.utils.StorageUtils;
|
||||
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
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;
|
||||
@ -101,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;
|
||||
@ -150,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;
|
||||
@ -193,17 +277,16 @@ public final class Utils {
|
||||
}
|
||||
|
||||
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();
|
||||
b.appendQueryParameter("fingerprint", repo.fingerprint);
|
||||
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
|
||||
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));
|
||||
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();
|
||||
}
|
||||
@ -218,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));
|
||||
}
|
||||
|
||||
@ -234,6 +319,10 @@ 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");
|
||||
@ -310,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
|
||||
|
@ -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();
|
||||
|
@ -45,11 +45,12 @@ 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 String[] ALL = { APP_ID, VERSION_CODE, VERSION_NAME };
|
||||
public static String[] ALL = { _ID, APP_ID, VERSION_CODE, VERSION_NAME };
|
||||
|
||||
}
|
||||
|
||||
|
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);
|
||||
}
|
||||
}
|
162
src/org/fdroid/fdroid/localrepo/LocalRepoService.java
Normal file
162
src/org/fdroid/fdroid/localrepo/LocalRepoService.java
Normal file
@ -0,0 +1,162 @@
|
||||
|
||||
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";
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
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;
|
||||
}
|
||||
|
||||
}
|
283
src/org/fdroid/fdroid/views/LocalRepoActivity.java
Normal file
283
src/org/fdroid/fdroid/views/LocalRepoActivity.java
Normal file
@ -0,0 +1,283 @@
|
||||
|
||||
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.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import org.fdroid.fdroid.*;
|
||||
import org.fdroid.fdroid.net.WifiStateChangeService;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class LocalRepoActivity extends Activity {
|
||||
private static final String TAG = "LocalRepoActivity";
|
||||
private ProgressDialog repoProgress;
|
||||
|
||||
private WifiManager wifiManager;
|
||||
private ToggleButton repoSwitch;
|
||||
|
||||
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);
|
||||
setContentView(R.layout.local_repo_activity);
|
||||
|
||||
repoSwitch = (ToggleButton) 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));
|
||||
// if no local repo exists, create one with only FDroid in it
|
||||
if (!FDroidApp.localRepo.xmlIndex.exists())
|
||||
new UpdateAsyncTask(this, new String[] {
|
||||
getPackageName(),
|
||||
}).execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange);
|
||||
}
|
||||
|
||||
private BroadcastReceiver onWifiChange = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent i) {
|
||||
resetNetworkInfo();
|
||||
}
|
||||
};
|
||||
|
||||
private void resetNetworkInfo() {
|
||||
int wifiState = wifiManager.getWifiState();
|
||||
if (wifiState == WifiManager.WIFI_STATE_ENABLED) {
|
||||
setUIFromWifi();
|
||||
wireRepoSwitchToWebServer();
|
||||
} else {
|
||||
repoSwitch.setChecked(false);
|
||||
repoSwitch.setText(R.string.enable_wifi);
|
||||
repoSwitch.setTextOn(getString(R.string.enabling_wifi));
|
||||
repoSwitch.setTextOff(getString(R.string.enable_wifi));
|
||||
repoSwitch.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
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) {
|
||||
if (repoSwitch.isChecked()) {
|
||||
FDroidApp.startLocalRepoService(LocalRepoActivity.this);
|
||||
} else {
|
||||
FDroidApp.stopLocalRepoService(LocalRepoActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@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("\\?.*$", "");
|
||||
repoSwitch.setText(buttonLabel);
|
||||
repoSwitch.setTextOn(buttonLabel);
|
||||
repoSwitch.setTextOff(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);
|
||||
new QrGenAsyncTask(this, R.id.repoQrCode).execute(qrUriString);
|
||||
|
||||
TextView wifiNetworkNameTextView = (TextView) findViewById(R.id.wifiNetworkName);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
82
src/org/fdroid/fdroid/views/QrWizardDownloadActivity.java
Normal file
82
src/org/fdroid/fdroid/views/QrWizardDownloadActivity.java
Normal file
@ -0,0 +1,82 @@
|
||||
|
||||
package org.fdroid.fdroid.views;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
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);
|
||||
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;
|
||||
|
||||
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://", ""));
|
||||
}
|
||||
}
|
110
src/org/fdroid/fdroid/views/QrWizardWifiNetworkActivity.java
Normal file
110
src/org/fdroid/fdroid/views/QrWizardWifiNetworkActivity.java
Normal file
@ -0,0 +1,110 @@
|
||||
|
||||
package org.fdroid.fdroid.views;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.wifi.WifiInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
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);
|
||||
|
||||
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 += ";;";
|
||||
new QrGenAsyncTask(this, R.id.qrWizardImage).execute(qrString);
|
||||
Log.i(TAG, "qr: " + qrString);
|
||||
|
||||
TextView wifiNetworkName = (TextView) findViewById(R.id.qrWifiNetworkName);
|
||||
wifiNetworkName.setText(wifiInfo.getSSID());
|
||||
Log.i(TAG, "wifi network name: " + 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();
|
||||
}
|
||||
}
|
89
src/org/fdroid/fdroid/views/SelectLocalAppsActivity.java
Normal file
89
src/org/fdroid/fdroid/views/SelectLocalAppsActivity.java
Normal file
@ -0,0 +1,89 @@
|
||||
|
||||
package org.fdroid.fdroid.views;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
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 FragmentActivity {
|
||||
private static final String TAG = "SelectLocalAppsActivity";
|
||||
private SelectLocalAppsFragment selectLocalAppsFragment = null;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.select_local_apps_activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (selectLocalAppsFragment == null)
|
||||
selectLocalAppsFragment = (SelectLocalAppsFragment) getSupportFragmentManager().findFragmentById(
|
||||
R.id.fragment_app_list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.select_local_apps_activity, menu);
|
||||
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_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);
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
@ -364,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,147 @@
|
||||
/*
|
||||
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.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.ListFragment;
|
||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v4.widget.SimpleCursorAdapter;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ActionMode;
|
||||
import android.view.View;
|
||||
import android.widget.ListView;
|
||||
|
||||
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;
|
||||
|
||||
public class SelectLocalAppsFragment extends ListFragment implements LoaderCallbacks<Cursor> {
|
||||
|
||||
private SelectLocalAppsActivity selectLocalAppsActivity;
|
||||
private ActionMode mActionMode = null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@TargetApi(11)
|
||||
// TODO replace with appcompat-v7
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
setEmptyText(getString(R.string.no_applications_found));
|
||||
|
||||
selectLocalAppsActivity = (SelectLocalAppsActivity) getActivity();
|
||||
ListView listView = getListView();
|
||||
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
|
||||
SimpleCursorAdapter adapter = new SimpleCursorAdapter(getActivity(),
|
||||
android.R.layout.simple_list_item_activated_1,
|
||||
null,
|
||||
new String[] {
|
||||
InstalledAppProvider.DataColumns.APP_ID,
|
||||
},
|
||||
new int[] {
|
||||
android.R.id.text1,
|
||||
},
|
||||
0);
|
||||
setListAdapter(adapter);
|
||||
setListShown(false);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(11)
|
||||
// TODO replace with appcompat-v7
|
||||
@Override
|
||||
public void onListItemClick(ListView l, View v, int position, long id) {
|
||||
if (mActionMode == null)
|
||||
mActionMode = selectLocalAppsActivity
|
||||
.startActionMode(selectLocalAppsActivity.mActionModeCallback);
|
||||
Cursor cursor = (Cursor) l.getAdapter().getItem(position);
|
||||
String packageName = cursor.getString(1);
|
||||
if (FDroidApp.selectedApps.contains(packageName)) {
|
||||
FDroidApp.selectedApps.remove(packageName);
|
||||
} else {
|
||||
FDroidApp.selectedApps.add(packageName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CursorLoader onCreateLoader(int id, Bundle args) {
|
||||
CursorLoader loader = new CursorLoader(
|
||||
this.getActivity(),
|
||||
InstalledAppProvider.getContentUri(),
|
||||
InstalledAppProvider.DataColumns.ALL,
|
||||
null,
|
||||
null,
|
||||
InstalledAppProvider.DataColumns.APP_ID);
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
}
|
@ -44,7 +44,8 @@ public class SignedRepoUpdaterTest extends InstrumentationTestCase {
|
||||
InputStream input = null;
|
||||
OutputStream output = null;
|
||||
try {
|
||||
indexFile = File.createTempFile("index-", ".xml", context.getFilesDir());
|
||||
indexFile = File.createTempFile("index-", ".xml",
|
||||
getInstrumentation().getTargetContext().getCacheDir());
|
||||
input = getInputStreamFromAssets(fileName);
|
||||
output = new FileOutputStream(indexFile);
|
||||
Utils.copy(input, output);
|
||||
|
Loading…
x
Reference in New Issue
Block a user