This commit is contained in:
Daniel Martí 2014-05-07 20:32:22 +02:00
commit 15a3d74ada
40 changed files with 2916 additions and 50 deletions

3
.gitmodules vendored
View File

@ -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

View File

@ -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>

View File

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

1
extern/nanohttpd vendored Submodule

@ -0,0 +1 @@
Subproject commit 58f73260ed98df878bae1051dc8e6a0bce842fcb

BIN
libs/core-3.0.1.jar Normal file

Binary file not shown.

View File

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

View File

@ -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>

View 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>

View File

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

View File

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

View File

@ -0,0 +1,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>

View 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>

View File

@ -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&#8230;</string>
<string name="local_repo_running">Your local FDroid repo is accessible.</string>
<string name="setup_repo">Setup Local Repo</string>
<string name="touch_to_configure_local_repo">Touch to setup your local repo.</string>
<string name="updating">Updating&#8230;</string>
<string name="update_repo">Update Repo</string>
<string name="deleting_repo">Deleting current repo&#8230;</string>
<string name="adding_apks_format">Adding %s to repo&#8230;</string>
<string name="writing_index_xml">Writing raw index file (index.xml)&#8230;</string>
<string name="linking_apks">Linking APKs into the repo&#8230;</string>
<string name="copying_icons">Copying app icons into the repo&#8230;</string>
<string name="updated_local_repo">Finished updating local repo</string>
<string name="no_applications_found">No applications found</string>
<string name="icon">icon</string>
<string name="fingerprint">Fingerprint:</string>
<string name="wifi_network">WiFi Network:</string>
<string name="enable_wifi">Enable WiFi</string>
<string name="enabling_wifi">Enabling WiFi&#8230;</string>
<string name="same_wifi_instructions">To connect to other people\'s devices, make sure both devices are on the same WiFi network. Then either type the URL above into F-Droid, or scan this QR Code:</string>
<string name="qr_code">QR Code</string>
<string name="next">Next</string>
<string name="qr_content_description">QR Code of repo URL</string>
<string name="qr_wizard_wifi_network_instructions">Scan this QR Code to connect to the same WiFi network as this device.</string>
<string name="qr_wizard_download_instructions">Scan this QR Code to connect to the website for getting started.</string>
<string name="send_fdroid_via_wifi">Send FDroid via WiFi&#8230;</string>
<!--
status_download takes four parameters:

View File

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

View File

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

View File

@ -0,0 +1 @@
../../../../extern/nanohttpd/webserver/src/main/java/fi/iki/elonen/InternalRewrite.java

View File

@ -0,0 +1 @@
../../../../extern/nanohttpd/core/src/main/java/fi/iki/elonen/NanoHTTPD.java

View File

@ -0,0 +1 @@
../../../../extern/nanohttpd/webserver/src/main/java/fi/iki/elonen/ServerRunner.java

View File

@ -0,0 +1 @@
../../../../extern/nanohttpd/webserver/src/main/java/fi/iki/elonen/WebServerPlugin.java

View File

@ -0,0 +1 @@
../../../../extern/nanohttpd/webserver/src/main/java/fi/iki/elonen/WebServerPluginInfo.java

View File

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

View File

@ -23,14 +23,22 @@ import android.app.Activity;
import android.app.Application;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;
@ -46,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();
}
}
}
}

View File

@ -22,8 +22,7 @@ import java.util.ArrayList;
import java.util.Arrays;
public class FDroidCertPins {
public static final String[] DEFAULT_PINS =
{
public static final String[] DEFAULT_PINS = {
/*
* SubjectDN: CN=f-droid.org, OU=PositiveSSL, OU=Domain Control Validated
* IssuerDN: CN=PositiveSSL CA 2, O=COMODO CA Limited, L=Salford, ST=Greater Manchester, C=GB
@ -31,22 +30,12 @@ public class FDroidCertPins {
* SPKI Pin: 638F93856E1F5EDFCBD40C46D4160CFF21B0713A
*/
"638F93856E1F5EDFCBD40C46D4160CFF21B0713A",
/*
* SubjectDN: CN=guardianproject.info, OU=Gandi Standard SSL, OU=Domain Control Validated
* IssuerDN: CN=Gandi Standard SSL CA, O=GANDI SAS, C=FR
* Fingerprint: 187C2573E924DFCBFF2A781A2F99D71C6E031828
* SPKI Pin: EB6BBC6C6BAEEA20CB0F3357720D86E0F3A526F4
*/
"EB6BBC6C6BAEEA20CB0F3357720D86E0F3A526F4",
};
public static ArrayList<String> PINLIST = null;
public static String[] getPinList()
{
if(PINLIST == null)
{
public static String[] getPinList() {
if (PINLIST == null) {
PINLIST = new ArrayList<String>();
PINLIST.addAll(Arrays.asList(DEFAULT_PINS));
}

View File

@ -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);
}
}

View File

@ -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(" ", "-");
}
}

View File

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

View File

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

View File

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

View File

@ -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 };
}

View File

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

View File

@ -0,0 +1,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);
}
}

View File

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

View File

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

View File

@ -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();
}
}
}

View 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://", ""));
}
}

View 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();
}
}

View 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();
}
};
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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);