Activity/Service for running a local repo via http://

This is a skeleton for the upcoming local repo (aka swap aka Kerplapp).
Right now, it just provides an Activity for controlling a Service which
manages a local webserver (nanohttpd).  Next, it will be wired up to the
local repo created via a dedicated Activity for managing the list of apps
included in the local repo.

refs #3204 https://dev.guardianproject.info/issues/3204
This commit is contained in:
Hans-Christoph Steiner 2014-05-02 20:26:31 -04:00
parent 2c2d8c868c
commit 5050605f72
9 changed files with 845 additions and 6 deletions

View File

@ -182,6 +182,19 @@
<activity <activity
android:name=".NfcNotEnabledActivity" android:name=".NfcNotEnabledActivity"
android:noHistory="true" /> android:noHistory="true" />
<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 <activity
android:name=".views.RepoDetailsActivity" android:name=".views.RepoDetailsActivity"
android:label="@string/menu_manage" android:label="@string/menu_manage"
@ -312,6 +325,7 @@
<service android:name=".UpdateService" /> <service android:name=".UpdateService" />
<service android:name=".net.WifiStateChangeService" /> <service android:name=".net.WifiStateChangeService" />
<service android:name=".localrepo.LocalRepoService" />
</application> </application>
</manifest> </manifest>

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,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/menu_settings"
android:icon="@android:drawable/ic_menu_preferences"
android:showAsAction="never"
android:title="@string/menu_preferences"/>
</menu>

View File

@ -150,8 +150,17 @@
<string name="category_whatsnew">What\'s New</string> <string name="category_whatsnew">What\'s New</string>
<string name="category_recentlyupdated">Recently Updated</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_title">Local FDroid Repos</string>
<string name="local_repos_scanning">Discovering local FDroid repos&#8230;</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="touch_to_configure_local_repo">Touch to setup your local repo.</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_content_description">QR Code of repo URL</string>
<!-- <!--
status_download takes four parameters: status_download takes four parameters:

View File

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

View File

@ -26,6 +26,7 @@ import android.bluetooth.BluetoothManager;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
@ -36,6 +37,7 @@ import android.net.wifi.WifiManager;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.os.Message; import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException; import android.os.RemoteException;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.util.Log; import android.util.Log;
@ -53,6 +55,7 @@ import org.fdroid.fdroid.compat.PRNGFixes;
import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.InstalledAppCacheUpdater; import org.fdroid.fdroid.data.InstalledAppCacheUpdater;
import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.localrepo.LocalRepoService;
import org.fdroid.fdroid.net.WifiStateChangeService; import org.fdroid.fdroid.net.WifiStateChangeService;
import org.thoughtcrime.ssl.pinning.PinningTrustManager; import org.thoughtcrime.ssl.pinning.PinningTrustManager;
import org.thoughtcrime.ssl.pinning.SystemKeyStore; import org.thoughtcrime.ssl.pinning.SystemKeyStore;
@ -82,6 +85,9 @@ public class FDroidApp extends Application {
public static Repo repo = new Repo(); public static Repo repo = new Repo();
static Set<String> selectedApps = new HashSet<String>(); static Set<String> selectedApps = new HashSet<String>();
private static Messenger localRepoServiceMessenger = null;
private static boolean localRepoServiceIsBound = false;
BluetoothAdapter bluetoothAdapter = null; BluetoothAdapter bluetoothAdapter = null;
private static enum Theme { private static enum Theme {
@ -279,4 +285,41 @@ public class FDroidApp extends Application {
activity.startActivity(sendBt); 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

@ -0,0 +1,146 @@
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.R;
import org.fdroid.fdroid.net.LocalHTTPD;
import org.fdroid.fdroid.net.WifiStateChangeService;
import org.fdroid.fdroid.views.LocalRepoActivity;
import java.io.IOException;
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 Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.arg1 == START) {
startWebServer();
} else if (msg.arg1 == STOP) {
stopWebServer();
} else if (msg.arg1 == RESTART) {
stopWebServer();
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 (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,209 @@
package org.fdroid.fdroid.views;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.net.wifi.WifiManager;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
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.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.ToggleButton;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.PreferencesActivity;
import org.fdroid.fdroid.QrGenAsyncTask;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
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;
/** 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));
}
@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);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
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();
}
}
@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);
}
}