diff --git a/assets/index.template.html b/assets/index.template.html new file mode 100644 index 000000000..fe48db91f --- /dev/null +++ b/assets/index.template.html @@ -0,0 +1,65 @@ + + + + + {{REPO_URL}} local FDroid repo + + + + + + + + +

Kerplapp Bootstrap

+
    +
  1. Find a Kerplapp Repo
  2. +
  3. Download F-Droid client
  4. +
  5. Install F-Droid client
  6. +
  7. Add Kerplapp Repo to F-Droid client
  8. +
  9. Kerplapp an App!
  10. +
+ + diff --git a/res/menu/local_repo_activity.xml b/res/menu/local_repo_activity.xml index 823701dac..462e4fe64 100644 --- a/res/menu/local_repo_activity.xml +++ b/res/menu/local_repo_activity.xml @@ -1,6 +1,11 @@ + Discovering local FDroid repos… Your local FDroid repo is accessible. Touch to setup your local repo. + Updating… + Deleting current repo… + Adding %s to repo… + Writing raw index file (index.xml)… + Linking APKs into the repo… + Copying app icons into the repo… + Finished updating local repo Fingerprint: WiFi Network: Enable WiFi diff --git a/src/org/fdroid/fdroid/FDroidApp.java b/src/org/fdroid/fdroid/FDroidApp.java index 10c51528f..1b5bfcf35 100644 --- a/src/org/fdroid/fdroid/FDroidApp.java +++ b/src/org/fdroid/fdroid/FDroidApp.java @@ -55,6 +55,7 @@ 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; @@ -83,6 +84,7 @@ public class FDroidApp extends Application { public static String ssid = ""; public static String bssid = ""; public static Repo repo = new Repo(); + public static LocalRepoManager localRepo = null; static Set selectedApps = new HashSet(); private static Messenger localRepoServiceMessenger = null; @@ -125,6 +127,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 diff --git a/src/org/fdroid/fdroid/Utils.java b/src/org/fdroid/fdroid/Utils.java index 6d0957028..8111aace3 100644 --- a/src/org/fdroid/fdroid/Utils.java +++ b/src/org/fdroid/fdroid/Utils.java @@ -20,8 +20,7 @@ package org.fdroid.fdroid; import android.content.Context; 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; @@ -30,13 +29,10 @@ import com.nostra13.universalimageloader.utils.StorageUtils; import org.fdroid.fdroid.data.Repo; -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; @@ -315,4 +311,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(" ", "-"); + } + } diff --git a/src/org/fdroid/fdroid/data/Apk.java b/src/org/fdroid/fdroid/data/Apk.java index 95ff15f65..a6d08edb4 100644 --- a/src/org/fdroid/fdroid/data/Apk.java +++ b/src/org/fdroid/fdroid/data/Apk.java @@ -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 { @@ -31,7 +32,8 @@ public class Apk extends ValueObject implements Comparable { // 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 diff --git a/src/org/fdroid/fdroid/data/App.java b/src/org/fdroid/fdroid/data/App.java index b5ffb8292..19a3cc889 100644 --- a/src/org/fdroid/fdroid/data/App.java +++ b/src/org/fdroid/fdroid/data/App.java @@ -1,20 +1,20 @@ package org.fdroid.fdroid.data; import android.content.ContentValues; -import android.content.Context; import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; import android.database.Cursor; + import org.fdroid.fdroid.AppFilter; import org.fdroid.fdroid.Utils; import java.util.Date; -import java.util.Map; +import java.util.List; public class App extends ValueObject implements Comparable { // 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 +83,9 @@ public class App extends ValueObject implements Comparable { public int installedVersionCode; + public ApplicationInfo appInfo; + public List apks; + @Override public int compareTo(App app) { return name.compareToIgnoreCase(app.name); diff --git a/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java b/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java new file mode 100644 index 000000000..240216da0 --- /dev/null +++ b/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java @@ -0,0 +1,675 @@ + +package org.fdroid.fdroid.localrepo; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.FeatureInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.AssetManager; +import android.content.res.XmlResourceParser; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +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 org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +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 apps = new HashMap(); + + 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; + } + + public void writeIndexPage(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 (symlinkOrCopyFile(apkFile, fdroidApkLink)) + fdroidClientURL = "/" + fdroidApkLink.getName(); + } catch (NameNotFoundException e) { + e.printStackTrace(); + } + + 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(); + symlinkOrCopyFile(indexHtml, fdroidDirIndex); + File repoDirIndex = new File(repoDir, "index.html"); + repoDirIndex.delete(); + 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(); + symlinkOrCopyFile(indexHtml, fdroidCAPSIndex); + File repoCAPSIndex = new File(repoCAPS, "index.html"); + repoCAPSIndex.delete(); + 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(apps.keySet())); + } + + public void copyApksToRepo(List appsToCopy) { + for (String packageName : appsToCopy) { + App app = apps.get(packageName); + + for (Apk apk : app.apks) { + File outFile = new File(repoDir, apk.apkName); + if (!symlinkOrCopyFile(apk.installedFile, outFile)) { + throw new IllegalStateException("Unable to copy APK"); + } + } + } + } + + /** + * 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 doSymLink(inFile, outFile); + } else { + return doCopyFile(inFile, outFile); + } + } + + public static boolean doSymLink(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"; + Log.i(TAG, "Running: " + command); + 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; + } + Log.i(TAG, "symlink exitcode: " + exitCode); + return exitCode == 0; + } + + public static boolean doCopyFile(File inFile, File outFile) { + InputStream inStream = null; + OutputStream outStream = null; + + try { + inStream = new FileInputStream(inFile); + outStream = new FileOutputStream(outFile); + + return doCopyStream(inStream, outStream); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + public static boolean doCopyStream(InputStream inStream, OutputStream outStream) + { + byte[] buf = new byte[1024]; + int readBytes; + try + { + while ((readBytes = inStream.read(buf)) > 0) { + outStream.write(buf, 0, readBytes); + } + inStream.close(); + outStream.close(); + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + public interface ScanListener { + public void processedApp(String packageName, int index, int total); + } + + @TargetApi(9) + public App addApp(Context context, String packageName) { + ApplicationInfo appInfo; + PackageInfo packageInfo; + try { + appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA); + packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES + | PackageManager.GET_PERMISSIONS); + } catch (NameNotFoundException e) { + e.printStackTrace(); + return null; + } + + App app = new App(); + app.name = (String) appInfo.loadLabel(pm); + app.summary = (String) appInfo.loadDescription(pm); + app.icon = getIconFile(packageName, packageInfo.versionCode).getName(); + app.id = appInfo.packageName; + if (Build.VERSION.SDK_INT > 8) { + app.added = new Date(packageInfo.firstInstallTime); + app.lastUpdated = new Date(packageInfo.lastUpdateTime); + } else { + app.added = new Date(System.currentTimeMillis()); + app.lastUpdated = app.added; + } + app.appInfo = appInfo; + app.apks = new ArrayList(); + + // TODO: use pm.getInstallerPackageName(packageName) for something + + 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 = app.added; + apk.minSdkVersion = getMinSdkVersion(context, packageName); + apk.id = app.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 featureNames = new ArrayList(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; + try { + JarFile apkJar = new JarFile(apkFile); + JarEntry aSignedEntry = (JarEntry) apkJar.getEntry("AndroidManifest.xml"); + + if (aSignedEntry == null) { + apkJar.close(); + return null; + } + + 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(); + return null; + } + + 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"); + + } catch (CertificateEncodingException e) { + return null; + } catch (IOException e) { + return null; + } + + app.apks.add(apk); + + if (!validApp(app)) + return null; + + apps.put(packageName, app); + return app; + } + + /* PackageManager doesn't give us minSdkVersion, so we have to parse it */ + public 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 void removeApp(String packageName) { + apps.remove(packageName); + } + + public List getApps() { + return new ArrayList(apps.keySet()); + } + + public boolean validApp(App app) { + if (app == null) + return false; + + if (app.name == null || app.name.equals("")) + return false; + + if (app.id == null | app.id.equals("")) + return false; + + if (app.apks == null || app.apks.size() != 1) + return false; + + File apkFile = app.apks.get(0).installedFile; + if (apkFile == null || !apkFile.canRead()) + return false; + + return true; + } + + public void copyIconsToRepo() { + for (App app : apps.values()) { + if (app.apks.size() > 0) { + Apk apk = app.apks.get(0); + copyIconToRepo(app.appInfo.loadIcon(pm), app.id, apk.vercode); + } + } + } + + /** + * 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 repo generated from apps installed on " + repoName); + repo.appendChild(repoDesc); + + SimpleDateFormat dateToStr = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + for (Entry entry : apps.entrySet()) { + String latestVersion = "0"; + String latestVerCode = "0"; + 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.name); + application.appendChild(summary); + + Element description = doc.createElement("description"); + description.setTextContent(app.name); + application.appendChild(description); + + Element desc = doc.createElement("desc"); + desc.setTextContent(app.name); + 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"); + application.appendChild(marketVersion); + + Element marketVerCode = doc.createElement("marketvercode"); + application.appendChild(marketVerCode); + + for (Apk apk : app.apks) { + Element packageNode = doc.createElement("package"); + + Element version = doc.createElement("version"); + latestVersion = apk.version; + version.setTextContent(apk.version); + packageNode.appendChild(version); + + // F-Droid unfortunately calls versionCode versioncode... + Element versioncode = doc.createElement("versioncode"); + latestVerCode = String.valueOf(apk.vercode); + versioncode.setTextContent(latestVerCode); + packageNode.appendChild(versioncode); + + Element apkname = doc.createElement("apkname"); + apkname.setTextContent(apk.apkName); + packageNode.appendChild(apkname); + + Element hash = doc.createElement("hash"); + hash.setAttribute("type", apk.hashType); + hash.setTextContent(apk.hash.toLowerCase(Locale.US)); + packageNode.appendChild(hash); + + Element sig = doc.createElement("sig"); + sig.setTextContent(apk.sig.toLowerCase(Locale.US)); + packageNode.appendChild(sig); + + Element size = doc.createElement("size"); + size.setTextContent(String.valueOf(apk.installedFile.length())); + packageNode.appendChild(size); + + Element sdkver = doc.createElement("sdkver"); + sdkver.setTextContent(String.valueOf(apk.minSdkVersion)); + packageNode.appendChild(sdkver); + + Element apkAdded = doc.createElement("added"); + apkAdded.setTextContent(dateToStr.format(apk.added)); + packageNode.appendChild(apkAdded); + + Element features = doc.createElement("features"); + if (apk.features != null) + features.setTextContent(Utils.CommaSeparatedList.str(apk.features)); + packageNode.appendChild(features); + + Element permissions = doc.createElement("permissions"); + if (apk.permissions != null) { + StringBuilder buff = new StringBuilder(); + + for (String permission : apk.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); + } + + // now mark the latest version in the feed for this particular app + marketVersion.setTextContent(latestVersion); + marketVerCode.setTextContent(latestVerCode); + } + + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + + DOMSource domSource = new DOMSource(doc); + StreamResult result = new StreamResult(xmlIndex); + + transformer.transform(domSource, result); + } +} diff --git a/src/org/fdroid/fdroid/net/WifiStateChangeService.java b/src/org/fdroid/fdroid/net/WifiStateChangeService.java index afab55923..3b646f71a 100644 --- a/src/org/fdroid/fdroid/net/WifiStateChangeService.java +++ b/src/org/fdroid/fdroid/net/WifiStateChangeService.java @@ -14,6 +14,7 @@ import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.Utils; import java.util.Locale; @@ -66,6 +67,9 @@ public class WifiStateChangeService extends Service { 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(context, FDroidApp.repo).toString()); } catch (InterruptedException e) { e.printStackTrace(); } diff --git a/src/org/fdroid/fdroid/views/LocalRepoActivity.java b/src/org/fdroid/fdroid/views/LocalRepoActivity.java index 2c3ec3713..c15248314 100644 --- a/src/org/fdroid/fdroid/views/LocalRepoActivity.java +++ b/src/org/fdroid/fdroid/views/LocalRepoActivity.java @@ -5,32 +5,25 @@ 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.*; 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.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; +import android.view.*; import android.widget.TextView; +import android.widget.Toast; 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.*; import org.fdroid.fdroid.net.WifiStateChangeService; import java.util.Locale; @@ -104,12 +97,21 @@ public class LocalRepoActivity extends Activity { 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: + setUIFromWifi(); + String[] packages = new String[2]; + packages[0] = getPackageName(); + packages[1] = "com.android.bluetooth"; + new UpdateAsyncTask(this, packages).execute(); + return true; case R.id.menu_settings: startActivityForResult(new Intent(this, PreferencesActivity.class), SET_IP_ADDRESS); return true; @@ -206,4 +208,67 @@ public class LocalRepoActivity extends Activity { // ignore orientation/keyboard change super.onConfigurationChanged(newConfig); } + + class UpdateAsyncTask extends AsyncTask { + 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() { + + @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(); + } + } }