From 953d3ed8d7dcaf211d3e470681d2a01accce1db5 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Mon, 29 Dec 2014 19:58:11 +1100
Subject: [PATCH 01/16] Added search functionality to "add apps to swap"
 screen.

This was present in the old local repo implementation, and the skeleton
code for implementing it was copied to the swap fragment. The only change
neccesary was to add a search button to the menu and make it have a
SearchView as its action view.
---
 F-Droid/res/menu/swap_next_search.xml              | 14 ++++++++++++++
 .../fdroid/views/swap/SelectAppsFragment.java      | 13 ++++++++++---
 2 files changed, 24 insertions(+), 3 deletions(-)
 create mode 100644 F-Droid/res/menu/swap_next_search.xml

diff --git a/F-Droid/res/menu/swap_next_search.xml b/F-Droid/res/menu/swap_next_search.xml
new file mode 100644
index 000000000..2c2a3f042
--- /dev/null
+++ b/F-Droid/res/menu/swap_next_search.xml
@@ -0,0 +1,14 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <item
+        android:id="@+id/action_next"
+        android:title="Next"
+        android:titleCondensed="Next"/>
+
+    <item
+        android:id="@+id/action_search"
+        android:icon="@android:drawable/ic_menu_search"
+        android:title="Search"
+        android:titleCondensed="Search"/>
+
+</menu>
\ No newline at end of file
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java
index 22d445727..294606fad 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java
@@ -10,6 +10,7 @@ import android.support.v4.content.CursorLoader;
 import android.support.v4.content.Loader;
 import android.support.v4.view.MenuItemCompat;
 import android.support.v4.widget.SimpleCursorAdapter;
+import android.support.v7.widget.SearchView;
 import android.text.TextUtils;
 import android.view.ActionMode;
 import android.view.ContextThemeWrapper;
@@ -20,7 +21,6 @@ import android.view.View;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.ListView;
-import android.widget.SearchView;
 import android.widget.TextView;
 import org.fdroid.fdroid.FDroidApp;
 import org.fdroid.fdroid.R;
@@ -36,7 +36,6 @@ public class SelectAppsFragment extends ThemeableListFragment
 
     private PackageManager packageManager;
     private Drawable defaultAppIcon;
-    private ActionMode mActionMode = null;
     private String mCurrentFilterString;
     private Set<String> previouslySelectedApps = new HashSet<String>();
 
@@ -52,10 +51,18 @@ public class SelectAppsFragment extends ThemeableListFragment
 
     @Override
     public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
-        menuInflater.inflate(R.menu.swap_next, menu);
+        menuInflater.inflate(R.menu.swap_next_search, menu);
         MenuItem nextMenuItem = menu.findItem(R.id.action_next);
         int flags = MenuItemCompat.SHOW_AS_ACTION_ALWAYS | MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT;
         MenuItemCompat.setShowAsAction(nextMenuItem, flags);
+
+        SearchView searchView = new SearchView(getActivity());
+
+        MenuItem searchMenuItem = menu.findItem(R.id.action_search);
+        MenuItemCompat.setActionView(searchMenuItem, searchView);
+        MenuItemCompat.setShowAsAction(searchMenuItem, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
+
+        searchView.setOnQueryTextListener(this);
     }
 
     @Override

From 7fb2de4bae3a2c7c7db9b9bff44f657b92bfc40e Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Mon, 29 Dec 2014 20:00:35 +1100
Subject: [PATCH 02/16] Select F-Droid correctly during swap process. Fixes
 #141.

Bug in the code which decides which apps to select for swapping.
Due to the way in which Adapters and ListViews work together to provide
"header" and "footer" functionalities for lists, there is a mismatch between
the index in our original adapter, and the actual index on the list. It is
up to us to maintain this correctly, which was not done, hence the off by
one error.
---
 .../src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java
index 294606fad..a8ce50bde 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java
@@ -198,7 +198,7 @@ public class SelectAppsFragment extends ThemeableListFragment
             Cursor c = ((Cursor) listView.getItemAtPosition(i + 1));
             String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID));
             if (TextUtils.equals(packageName, fdroid)) {
-                listView.setItemChecked(i, true); // always include FDroid
+                listView.setItemChecked(i + 1, true); // always include FDroid
             } else {
                 for (String selected : FDroidApp.selectedApps) {
                     if (TextUtils.equals(packageName, selected)) {

From 4711b508369a23cc698de37756654bb85c05a31d Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Tue, 30 Dec 2014 11:48:20 +1100
Subject: [PATCH 03/16] Swap works on 2.3 devices.

 * Cleaned up text alignment styles for API < 17.

API v17 has a textAlignment style, wherease previous verisons rely on
the "gravity" property. This change includes gravity="center" where there
was previously only textAlignment="center".

 * Fragments get added properly on 2.3 device.

For some reason, when adding the fragment to android.R.id.content, it
wouldn't work on my 2.3 device. This change includes a (almost) empty
activity layout with a single FrameLayout. The fragments are added to
this rather than "content", and it works better. It is not perfect - it
still adds the fargments behind the action bar, and so the action bar
appears blue. But at least they are there :)

 * Added translatable strings where constants were used before.

Not related to v2.3 support, but stil important for a stable release,
that is fully translated.
---
 F-Droid/res/layout/swap_activity.xml           | 14 ++++++++++++++
 F-Droid/res/layout/swap_blank.xml              |  4 ++--
 F-Droid/res/values/strings.xml                 |  2 ++
 F-Droid/res/values/styles.xml                  | 18 +++++++++++-------
 .../fdroid/fdroid/views/swap/SwapActivity.java |  4 +++-
 5 files changed, 32 insertions(+), 10 deletions(-)
 create mode 100644 F-Droid/res/layout/swap_activity.xml

diff --git a/F-Droid/res/layout/swap_activity.xml b/F-Droid/res/layout/swap_activity.xml
new file mode 100644
index 000000000..4f8096856
--- /dev/null
+++ b/F-Droid/res/layout/swap_activity.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+    <FrameLayout
+            android:id="@+id/fragment_container"
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent">
+
+    </FrameLayout>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/F-Droid/res/layout/swap_blank.xml b/F-Droid/res/layout/swap_blank.xml
index a865c22ab..ff4591a89 100644
--- a/F-Droid/res/layout/swap_blank.xml
+++ b/F-Droid/res/layout/swap_blank.xml
@@ -8,14 +8,14 @@
 
     <TextView
             android:id="@+id/text_description"
-            android:text="Your mobile device becomes an app store with Swap!"
+            android:text="@string/swap_introduction"
             style="@style/SwapTheme.StartSwap.MainText"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content" />
 
     <Button
             android:id="@+id/button_start_swap"
-            android:text="START A SWAP"
+            android:text="@string/swap_start"
             style="@style/SwapTheme.StartSwap.StartButton"
             android:layout_width="match_parent"
             android:layout_below="@+id/text_description"
diff --git a/F-Droid/res/values/strings.xml b/F-Droid/res/values/strings.xml
index 128dc3b9f..2ce145ddd 100644
--- a/F-Droid/res/values/strings.xml
+++ b/F-Droid/res/values/strings.xml
@@ -307,4 +307,6 @@
     <string name="open_qr_code_scanner">Open QR Code Scanner</string>
     <string name="swap_welcome">Welcome to F-Droid!</string>
     <string name="swap_confirm_connect">Do you want to get apps from %1$s now?</string>
+    <string name="swap_introduction">Your mobile device becomes an app store with Swap!</string>
+    <string name="swap_start">START A SWAP</string>
 </resources>
diff --git a/F-Droid/res/values/styles.xml b/F-Droid/res/values/styles.xml
index 3016a128e..a0522db1a 100644
--- a/F-Droid/res/values/styles.xml
+++ b/F-Droid/res/values/styles.xml
@@ -12,14 +12,14 @@
         <!-- backward-compatibility theme options go here -->
     </style>
 
-	<color name="black">#FF000000</color>
-	<color name="white">#FFFFFFFF</color>
-	<color name="red">#FFFF0000</color>
+    <color name="black">#FF000000</color>
+    <color name="white">#FFFFFFFF</color>
+    <color name="red">#FFFF0000</color>
 
-	<style name="AboutDialogLight" parent="@android:style/Theme.Dialog">
-		<item name="@android:windowBackground">@color/black</item>
-		<item name="@android:textColor">@color/white</item>
-	</style>
+    <style name="AboutDialogLight" parent="@android:style/Theme.Dialog">
+        <item name="@android:windowBackground">@color/black</item>
+        <item name="@android:textColor">@color/white</item>
+    </style>
 
     <style name="AppThemeDark" parent="AppBaseThemeDark">
         <!-- customizations that are not API-level specific go here. -->
@@ -65,6 +65,7 @@
 
     <style name="SwapTheme.AppList.SwapSuccess">
         <item name="android:textAlignment">center</item>
+        <item name="android:gravity">center</item>
         <item name="android:textSize">25.7sp</item> <!-- 46px * 96dpi / 160dpi -->
         <item name="android:paddingTop">28dp</item> <!-- 50px * 96dpi / 160dpi -->
         <item name="android:paddingBottom">20.1dp</item> <!-- 36px * 96dpi / 160dpi -->
@@ -73,6 +74,7 @@
 
     <style name="SwapTheme.AppList.SwapSuccessDetails">
         <item name="android:textAlignment">center</item>
+        <item name="android:gravity">center</item>
         <item name="android:textSize">20.1sp</item> <!-- 36px * 96dpi / 160dpi -->
         <item name="android:paddingTop">20.1dp</item> <!-- 36px * 96dpi / 160dpi -->
         <item name="android:paddingBottom">20.1dp</item> <!-- 36px * 96dpi / 160dpi -->
@@ -81,6 +83,7 @@
 
     <style name="SwapTheme.StartSwap.MainText">
         <item name="android:textAlignment">center</item>
+        <item name="android:gravity">center</item>
         <item name="android:textSize">20.1sp</item> <!-- 36px * 96dpi / 160dpi -->
         <item name="android:paddingLeft">28dp</item> <!-- 50px * 96dpi / 160dpi -->
         <item name="android:paddingRight">28dp</item> <!-- 50px * 96dpi / 160dpi -->
@@ -90,6 +93,7 @@
 
     <style name="SwapTheme.Wizard.Text">
         <item name="android:textAlignment">center</item>
+        <item name="android:gravity">center</item>
         <item name="android:textColor">#fff</item>
         <item name="android:textColorPrimary">#fff</item>
         <item name="android:textColorSecondary">#fff</item>
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
index 7bcaee90e..ba816d918 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
@@ -80,6 +80,8 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
 
         if (savedInstanceState == null) {
 
+            setContentView(R.layout.swap_activity);
+
             showFragment(new StartSwapFragment(), STATE_START_SWAP);
 
             if (FDroidApp.isLocalRepoServiceRunning()) {
@@ -133,7 +135,7 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
     private void showFragment(Fragment fragment, String name) {
         getSupportFragmentManager()
                 .beginTransaction()
-                .replace(android.R.id.content, fragment, name)
+                .replace(R.id.fragment_container, fragment, name)
                 .addToBackStack(name)
                 .commit();
     }

From 23ed692436e8b27d533c6b611ebec3c9f5801a13 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Wed, 31 Dec 2014 00:27:16 +1100
Subject: [PATCH 04/16] "Select apps for swap" screen work on API <= 10.

 * Provide CheckBox for selected items

Newer API's highlight the background using the "activated" state. Older
APIs need this to be implemented differently, so there are now checkboxes
on the left of the list view items to provide this functionality.

 * Clean up IDE warnings

Diamond operator for generics, remove unused imports and unused method.

 * Adapter class created for installed apps

Cleaned up the code to do with binding views to the adapter in this view.
Previously it made quite a few assumptions about the structure of the layout,
e.g. "layout.getParent().getParent() is a LinearLayout", which would cause
crashes if the layout changed slightly.
---
 .../select_local_apps_list_item.xml           |   1 +
 .../layout/select_local_apps_list_item.xml    |   6 +
 .../fdroid/views/swap/SelectAppsFragment.java | 149 +++++++++++-------
 .../fdroid/views/swap/WifiQrFragment.java     |  23 ++-
 4 files changed, 116 insertions(+), 63 deletions(-)

diff --git a/F-Droid/res/layout-v11/select_local_apps_list_item.xml b/F-Droid/res/layout-v11/select_local_apps_list_item.xml
index a95e22db0..5ac585492 100644
--- a/F-Droid/res/layout-v11/select_local_apps_list_item.xml
+++ b/F-Droid/res/layout-v11/select_local_apps_list_item.xml
@@ -22,6 +22,7 @@
     android:paddingTop="2dip" >
 
     <ImageView
+        android:id="@android:id/icon"
         android:layout_width="48dip"
         android:layout_height="48dip"
         android:layout_marginLeft="?attr/listPreferredItemPaddingLeft"
diff --git a/F-Droid/res/layout/select_local_apps_list_item.xml b/F-Droid/res/layout/select_local_apps_list_item.xml
index 36e5a7bf3..35920b29f 100644
--- a/F-Droid/res/layout/select_local_apps_list_item.xml
+++ b/F-Droid/res/layout/select_local_apps_list_item.xml
@@ -20,7 +20,13 @@
     android:paddingBottom="2dip"
     android:paddingTop="2dip" >
 
+    <CheckBox
+        android:id="@+id/checkbox"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+
     <ImageView
+        android:id="@android:id/icon"
         android:layout_width="48dip"
         android:layout_height="48dip"
         android:layout_marginLeft="?attr/listPreferredItemPaddingLeft"
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java
index a8ce50bde..f84a435c6 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java
@@ -1,27 +1,23 @@
 package org.fdroid.fdroid.views.swap;
 
+import android.content.Context;
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.support.v4.app.LoaderManager;
 import android.support.v4.content.CursorLoader;
 import android.support.v4.content.Loader;
 import android.support.v4.view.MenuItemCompat;
+import android.support.v4.widget.CursorAdapter;
 import android.support.v4.widget.SimpleCursorAdapter;
 import android.support.v7.widget.SearchView;
 import android.text.TextUtils;
-import android.view.ActionMode;
-import android.view.ContextThemeWrapper;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.ListView;
-import android.widget.TextView;
+import android.view.*;
+import android.widget.*;
 import org.fdroid.fdroid.FDroidApp;
 import org.fdroid.fdroid.R;
 import org.fdroid.fdroid.data.InstalledAppProvider;
@@ -34,10 +30,8 @@ import java.util.Set;
 public class SelectAppsFragment extends ThemeableListFragment
     implements LoaderManager.LoaderCallbacks<Cursor>, SearchView.OnQueryTextListener {
 
-    private PackageManager packageManager;
-    private Drawable defaultAppIcon;
     private String mCurrentFilterString;
-    private Set<String> previouslySelectedApps = new HashSet<String>();
+    private Set<String> previouslySelectedApps = new HashSet<>();
 
     public Set<String> getSelectedApps() {
         return FDroidApp.selectedApps;
@@ -103,46 +97,10 @@ public class SelectAppsFragment extends ThemeableListFragment
 
         setEmptyText(getString(R.string.no_applications_found));
 
-        packageManager = getActivity().getPackageManager();
-        defaultAppIcon = getResources().getDrawable(android.R.drawable.sym_def_app_icon);
-
         ListView listView = getListView();
         listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
-        SimpleCursorAdapter adapter = new SimpleCursorAdapter(
-                new ContextThemeWrapper(getActivity(), R.style.SwapTheme_AppList_ListItem),
-                R.layout.select_local_apps_list_item,
-                null,
-                new String[] {
-                        InstalledAppProvider.DataColumns.APPLICATION_LABEL,
-                        InstalledAppProvider.DataColumns.APP_ID,
-                },
-                new int[] {
-                        R.id.application_label,
-                        R.id.package_name,
-                });
-        adapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {
 
-            @Override
-            public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
-                if (columnIndex == cursor.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)) {
-                    String packageName = cursor.getString(columnIndex);
-                    TextView textView = (TextView) view.findViewById(R.id.package_name);
-                    textView.setText(packageName);
-                    LinearLayout ll = (LinearLayout) view.getParent().getParent();
-                    ImageView iconView = (ImageView) ll.getChildAt(0);
-                    Drawable icon;
-                    try {
-                        icon = packageManager.getApplicationIcon(packageName);
-                    } catch (PackageManager.NameNotFoundException e) {
-                        icon = defaultAppIcon;
-                    }
-                    iconView.setImageDrawable(icon);
-                    return true;
-                }
-                return false;
-            }
-        });
-        setListAdapter(adapter);
+        setListAdapter(new AppListAdapter(listView, getActivity(), null));
         setListShown(false); // start out with a progress indicator
 
         // either reconnect with an existing loader or start a new one
@@ -150,7 +108,7 @@ public class SelectAppsFragment extends ThemeableListFragment
 
         // build list of existing apps from what is on the file system
         if (FDroidApp.selectedApps == null) {
-            FDroidApp.selectedApps = new HashSet<String>();
+            FDroidApp.selectedApps = new HashSet<>();
             for (String filename : LocalRepoManager.get(getActivity()).repoDir.list()) {
                 if (filename.matches(".*\\.apk")) {
                     String packageName = filename.substring(0, filename.indexOf("_"));
@@ -190,7 +148,7 @@ public class SelectAppsFragment extends ThemeableListFragment
 
     @Override
     public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
-        ((SimpleCursorAdapter) this.getListAdapter()).swapCursor(cursor);
+        ((AppListAdapter)getListAdapter()).swapCursor(cursor);
 
         ListView listView = getListView();
         String fdroid = loader.getContext().getPackageName();
@@ -217,7 +175,7 @@ public class SelectAppsFragment extends ThemeableListFragment
 
     @Override
     public void onLoaderReset(Loader<Cursor> loader) {
-        ((SimpleCursorAdapter) this.getListAdapter()).swapCursor(null);
+        ((AppListAdapter)getListAdapter()).swapCursor(null);
     }
 
     @Override
@@ -240,10 +198,6 @@ public class SelectAppsFragment extends ThemeableListFragment
         return true;
     }
 
-    public String getCurrentFilterString() {
-        return mCurrentFilterString;
-    }
-
     @Override
     protected int getThemeStyle() {
         return R.style.SwapTheme_StartSwap;
@@ -253,4 +207,85 @@ public class SelectAppsFragment extends ThemeableListFragment
     protected int getHeaderLayout() {
         return R.layout.swap_create_header;
     }
+
+    private static class AppListAdapter extends CursorAdapter {
+
+        @Nullable
+        private LayoutInflater inflater;
+
+        @Nullable
+        private Drawable defaultAppIcon;
+
+        @NonNull
+        private final ListView listView;
+
+        public AppListAdapter(@NonNull ListView listView, @NonNull Context context, @Nullable Cursor c) {
+            super(context, c, FLAG_REGISTER_CONTENT_OBSERVER);
+            this.listView = listView;
+        }
+
+        @NonNull
+        private LayoutInflater getInflater(Context context) {
+            if (inflater == null) {
+                Context themedContext = new ContextThemeWrapper(context, R.style.SwapTheme_AppList_ListItem);
+                inflater = (LayoutInflater)themedContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+            }
+            return inflater;
+        }
+
+        @NonNull
+        private Drawable getDefaultAppIcon(Context context) {
+            if (defaultAppIcon == null) {
+                defaultAppIcon = context.getResources().getDrawable(android.R.drawable.sym_def_app_icon);
+            }
+            return defaultAppIcon;
+        }
+
+        @Override
+        public View newView(Context context, Cursor cursor, ViewGroup parent) {
+            View view = getInflater(context).inflate(R.layout.select_local_apps_list_item, null);
+            bindView(view, context, cursor);
+            return view;
+        }
+
+        @Override
+        public void bindView(final View view, final Context context, final Cursor cursor) {
+
+            TextView packageView = (TextView)view.findViewById(R.id.package_name);
+            TextView labelView = (TextView)view.findViewById(R.id.application_label);
+            ImageView iconView = (ImageView)view.findViewById(android.R.id.icon);
+
+            String packageName = cursor.getString(cursor.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID));
+            String appLabel = cursor.getString(cursor.getColumnIndex(InstalledAppProvider.DataColumns.APPLICATION_LABEL));
+
+            Drawable icon;
+            try {
+                icon = context.getPackageManager().getApplicationIcon(packageName);
+            } catch (PackageManager.NameNotFoundException e) {
+                icon = getDefaultAppIcon(context);
+            }
+
+            packageView.setText(packageName);
+            labelView.setText(appLabel);
+            iconView.setImageDrawable(icon);
+
+            // Since v11, the Android SDK provided the ability to show selected list items
+            // by highlighting their background. Prior to this, we need to handle this ourselves
+            // by adding a checkbox which can toggle selected items.
+            View checkBoxView = view.findViewById(R.id.checkbox);
+            if (checkBoxView != null) {
+                CheckBox checkBox = (CheckBox)checkBoxView;
+                checkBox.setOnCheckedChangeListener(null);
+                checkBox.setChecked(listView.isItemChecked(cursor.getPosition()));
+                final int position = cursor.getPosition();
+                checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+                    @Override
+                    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+                        listView.setItemChecked(position, isChecked);
+                    }
+                });
+            }
+        }
+    }
+
 }
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrFragment.java b/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrFragment.java
index 9453af6ad..a71db32eb 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrFragment.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrFragment.java
@@ -1,6 +1,5 @@
 package org.fdroid.fdroid.views.swap;
 
-import android.annotation.TargetApi;
 import android.app.Activity;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -23,6 +22,8 @@ import android.widget.TextView;
 import android.widget.Toast;
 import com.google.zxing.integration.android.IntentIntegrator;
 import com.google.zxing.integration.android.IntentResult;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.utils.URLEncodedUtils;
 import org.fdroid.fdroid.FDroid;
 import org.fdroid.fdroid.FDroidApp;
 import org.fdroid.fdroid.Preferences;
@@ -32,12 +33,16 @@ import org.fdroid.fdroid.Utils;
 import org.fdroid.fdroid.data.NewRepoConfig;
 import org.fdroid.fdroid.net.WifiStateChangeService;
 
+import java.net.URI;
+import java.util.List;
 import java.util.Locale;
 
 public class WifiQrFragment extends Fragment {
 
     private static final int CONNECT_TO_SWAP = 1;
 
+    private static final String TAG = "org.fdroid.fdroid.views.swap.WifiQrFragment";
+
     private BroadcastReceiver onWifiChange = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent i) {
@@ -105,7 +110,6 @@ public class WifiQrFragment extends Fragment {
                 new IntentFilter(WifiStateChangeService.BROADCAST));
     }
 
-    @TargetApi(14)
     private void setUIFromWifi() {
 
         if (TextUtils.isEmpty(FDroidApp.repo.address))
@@ -132,19 +136,26 @@ public class WifiQrFragment extends Fragment {
         }
         qrUriString += sharingUri.getPath().toUpperCase(Locale.ENGLISH);
         boolean first = true;
-        for (String parameterName : sharingUri.getQueryParameterNames()) {
-            if (!parameterName.equals("ssid")) {
+
+        // Andorid provides an API for getting the query parameters and iterating over them:
+        //   Uri.getQueryParameterNames()
+        // But it is only available on later Android versions. As such we URLEncodedUtils instead.
+        List<NameValuePair> parameters = URLEncodedUtils.parse(URI.create(sharingUri.toString()), "UTF-8");
+        for (NameValuePair parameter : parameters) {
+            if (!parameter.getName().equals("ssid")) {
                 if (first) {
                     qrUriString += "?";
                     first = false;
                 } else {
                     qrUriString += "&";
                 }
-                qrUriString += parameterName.toUpperCase(Locale.ENGLISH) + "=" +
-                    sharingUri.getQueryParameter(parameterName).toUpperCase(Locale.ENGLISH);
+                qrUriString += parameter.getName().toUpperCase(Locale.ENGLISH) + "=" +
+                    parameter.getValue().toUpperCase(Locale.ENGLISH);
             }
         }
 
+        Log.i(TAG, "Encoded swap URI in QR Code: " + qrUriString);
+
         // zxing requires >= 8
         // TODO: What about 7? I don't feel comfortable bumping the min version for this...
         // I would suggest show some alternate info, with directions for how to add a new repository manually.

From c6705e2cb967797aa2e362bf4d652bf0da7dea69 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Wed, 31 Dec 2014 23:33:49 +1100
Subject: [PATCH 05/16] Fixed all warnings from LocalRepoManager.java

 * Removed dead code
 * Added some Nullable/NonNull annotations to prevent future misuses of variables.
 * More verbose errors when an error occurs creating directories/files.
---
 F-Droid/src/org/fdroid/fdroid/Utils.java      |  2 +
 .../fdroid/localrepo/LocalRepoManager.java    | 81 +++++++++----------
 .../fdroid/net/WifiStateChangeService.java    |  1 -
 3 files changed, 42 insertions(+), 42 deletions(-)

diff --git a/F-Droid/src/org/fdroid/fdroid/Utils.java b/F-Droid/src/org/fdroid/fdroid/Utils.java
index b8470d73e..4b904b7f5 100644
--- a/F-Droid/src/org/fdroid/fdroid/Utils.java
+++ b/F-Droid/src/org/fdroid/fdroid/Utils.java
@@ -59,6 +59,8 @@ import java.util.Locale;
 
 public final class Utils {
 
+    private static final String TAG = "org.fdroid.fdroid.Utils";
+
     public static final int BUFFER_SIZE = 4096;
 
     // The date format used for storing dates (e.g. lastupdated, added) in the
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
index e867cd57e..09ead04f9 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
@@ -15,6 +15,8 @@ import android.graphics.Canvas;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import android.util.Log;
 import android.widget.Toast;
@@ -70,16 +72,13 @@ public class LocalRepoManager {
     private final SharedPreferences prefs;
     private final String fdroidPackageName;
 
-    private String ipAddressString = "UNSET";
-    private String uriString = "UNSET";
-
     private static String[] WEB_ROOT_ASSET_FILES = {
         "swap-icon.png",
         "swap-tick-done.png",
         "swap-tick-not-done.png"
     };
 
-    private Map<String, App> apps = new HashMap<String, App>();
+    private Map<String, App> apps = new HashMap<>();
 
     public final SanitizedFile xmlIndex;
     private SanitizedFile xmlIndexJar = null;
@@ -89,8 +88,10 @@ public class LocalRepoManager {
     public final SanitizedFile repoDir;
     public final SanitizedFile iconsDir;
 
+    @Nullable
     private static LocalRepoManager localRepoManager;
 
+    @NonNull
     public static LocalRepoManager get(Context context) {
         if (localRepoManager == null)
             localRepoManager = new LocalRepoManager(context);
@@ -126,10 +127,6 @@ public class LocalRepoManager {
                 Log.e(TAG, "Unable to create icons folder: " + iconsDir);
     }
 
-    public void setUriString(String uriString) {
-        this.uriString = uriString;
-    }
-
     private String writeFdroidApkToWebroot() {
         ApplicationInfo appInfo;
         String fdroidClientURL = "https://f-droid.org/FDroid.apk";
@@ -138,7 +135,7 @@ public class LocalRepoManager {
             appInfo = pm.getApplicationInfo(fdroidPackageName, PackageManager.GET_META_DATA);
             SanitizedFile apkFile = SanitizedFile.knownSanitized(appInfo.publicSourceDir);
             SanitizedFile fdroidApkLink = new SanitizedFile(webRoot, "fdroid.client.apk");
-            fdroidApkLink.delete();
+            attemptToDelete(fdroidApkLink);
             if (Utils.symlinkOrCopyFile(apkFile, fdroidApkLink))
                 fdroidClientURL = "/" + fdroidApkLink.getName();
         } catch (NameNotFoundException e) {
@@ -176,10 +173,10 @@ public class LocalRepoManager {
 
             // add in /FDROID/REPO to support bad QR Scanner apps
             File fdroidCAPS = new File(fdroidDir.getParentFile(), "FDROID");
-            fdroidCAPS.mkdir();
+            attemptToMkdir(fdroidCAPS);
 
             File repoCAPS = new File(fdroidCAPS, "REPO");
-            repoCAPS.mkdir();
+            attemptToMkdir(repoCAPS);
 
             symlinkIndexPageElsewhere("../", fdroidCAPS);
             symlinkIndexPageElsewhere("../../", repoCAPS);
@@ -190,15 +187,37 @@ public class LocalRepoManager {
         }
     }
 
+    private static void attemptToMkdir(@NonNull File dir) throws IOException {
+        if (dir.exists()) {
+            if (dir.isDirectory()) {
+                return;
+            } else {
+                throw new IOException("Can't make directory " + dir + " - it is already a file.");
+            }
+        }
+
+        if (!dir.mkdir()) {
+            throw new IOException("An error occured trying to create directory " + dir);
+        }
+    }
+
+    private static void attemptToDelete(File file) {
+            Utils.symlinkOrCopyFile(new SanitizedFile(new File(directory, symlinkPrefix), fileName), file);
+            if (!file.delete()) {
+                Log.e(TAG, "Could not delete \"" + file.getAbsolutePath() + "\".");
+            }
+        }
+    }
+
     private void symlinkIndexPageElsewhere(String symlinkPrefix, File directory) {
         SanitizedFile index = new SanitizedFile(directory, "index.html");
-        index.delete();
-        Utils.symlinkOrCopyFile(new SanitizedFile(new File(symlinkPrefix), "index.html"), index);
+        attemptToDelete(index);
+        Utils.symlinkOrCopyFile(new SanitizedFile(new File(directory, symlinkPrefix), "index.html"), index);
 
         for(String fileName : WEB_ROOT_ASSET_FILES) {
             SanitizedFile file = new SanitizedFile(directory, fileName);
-            file.delete();
-            Utils.symlinkOrCopyFile(new SanitizedFile(new File(symlinkPrefix), fileName), file);
+            attemptToDelete(file);
+            Utils.symlinkOrCopyFile(new SanitizedFile(new File(directory, symlinkPrefix), fileName), file);
         }
     }
 
@@ -208,7 +227,7 @@ public class LocalRepoManager {
                 if (file.isDirectory()) {
                     deleteContents(file);
                 } else {
-                    file.delete();
+                    attemptToDelete(file);
                 }
             }
         }
@@ -219,7 +238,7 @@ public class LocalRepoManager {
     }
 
     public void copyApksToRepo() {
-        copyApksToRepo(new ArrayList<String>(apps.keySet()));
+        copyApksToRepo(new ArrayList<>(apps.keySet()));
     }
 
     public void copyApksToRepo(List<String> appsToCopy) {
@@ -236,10 +255,6 @@ public class LocalRepoManager {
         }
     }
 
-    public interface ScanListener {
-        public void processedApp(String packageName, int index, int total);
-    }
-
     @TargetApi(9)
     public void addApp(Context context, String packageName) {
         App app;
@@ -249,15 +264,7 @@ public class LocalRepoManager {
                 return;
             PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_META_DATA);
             app.icon = getIconFile(packageName, packageInfo.versionCode).getName();
-        } catch (NameNotFoundException e) {
-            Log.e(TAG, "Error adding app to local repo: " + e.getMessage());
-            Log.e(TAG, Log.getStackTraceString(e));
-            return;
-        } catch (CertificateEncodingException e) {
-            Log.e(TAG, "Error adding app to local repo: " + e.getMessage());
-            Log.e(TAG, Log.getStackTraceString(e));
-            return;
-        } catch (IOException e) {
+        } catch (NameNotFoundException | CertificateEncodingException | IOException e) {
             Log.e(TAG, "Error adding app to local repo: " + e.getMessage());
             Log.e(TAG, Log.getStackTraceString(e));
             return;
@@ -266,12 +273,8 @@ public class LocalRepoManager {
         apps.put(packageName, app);
     }
 
-    public void removeApp(String packageName) {
-        apps.remove(packageName);
-    }
-
     public List<String> getApps() {
-        return new ArrayList<String>(apps.keySet());
+        return new ArrayList<>(apps.keySet());
     }
 
     public void copyIconsToRepo() {
@@ -290,8 +293,6 @@ public class LocalRepoManager {
 
     /**
      * 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;
@@ -319,8 +320,6 @@ public class LocalRepoManager {
         return new File(iconsDir, packageName + "_" + versionCode + ".png");
     }
 
-    // TODO this needs to be ported to < android-8
-    @TargetApi(8)
     private void writeIndexXML() throws TransformerException, ParserConfigurationException, LocalRepoKeyStore.InitException {
         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
         DocumentBuilder builder = factory.newDocumentBuilder();
@@ -338,7 +337,7 @@ public class LocalRepoManager {
         Element repo = doc.createElement("repo");
         repo.setAttribute("icon", "blah.png");
         repo.setAttribute("maxage", String.valueOf(repoMaxAge));
-        repo.setAttribute("name", repoName + " on " + ipAddressString);
+        repo.setAttribute("name", repoName + " on " + FDroidApp.ipAddressString);
         repo.setAttribute("pubkey", Hasher.hex(LocalRepoKeyStore.get(context).getCertificate()));
         long timestamp = System.currentTimeMillis() / 1000L;
         repo.setAttribute("timestamp", String.valueOf(timestamp));
@@ -512,7 +511,7 @@ public class LocalRepoManager {
         } catch (LocalRepoKeyStore.InitException e) {
             throw new IOException("Could not sign index - keystore failed to initialize");
         } finally {
-            xmlIndexJarUnsigned.delete();
+            attemptToDelete(xmlIndexJarUnsigned);
         }
 
     }
diff --git a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
index 59099ccb6..ac72dee81 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
@@ -82,7 +82,6 @@ public class WifiStateChangeService extends Service {
 
                 Context context = WifiStateChangeService.this.getApplicationContext();
                 LocalRepoManager lrm = LocalRepoManager.get(context);
-                lrm.setUriString(FDroidApp.repo.address);
                 lrm.writeIndexPage(Utils.getSharingUri(context, FDroidApp.repo).toString());
 
                 if (isCancelled())

From 5036deb61e1c72978c9e950ffd6295d42a558b48 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Fri, 2 Jan 2015 14:27:03 +1100
Subject: [PATCH 06/16] Swap apps more robust on API < 11. Only show swap repos
 sometimes.

 * Selecting apps to swap fixed

Before the checking of a list item would not actually register it to
be included in the swap. This has been rectified.

 * Added a new property to repos for "isSwap"

Repositories with this property are not shown in the Manage Repos
activity, as there is not much benefit to having this happen.

 * More robust error handling when symlinking files

Before it would check for stdout or stderr and then throw an exception.
This happened even on successful symlinks on my 2.3.3 device. As such,
I've put the error checking after the shell command has completely finished
(just in case there were any race conditions), and more importantly, checked
for the presence of the file being linked - rather than just stdout or
stderr.

 * More code cleanup

Generics <> operator, Nullable annotations, removal of dead code.
---
 .../src/org/fdroid/fdroid/UpdateService.java  | 20 ++++++++-----
 F-Droid/src/org/fdroid/fdroid/Utils.java      | 18 +++++-------
 .../src/org/fdroid/fdroid/data/DBHelper.java  | 13 +++++++--
 F-Droid/src/org/fdroid/fdroid/data/Repo.java  |  9 +++++-
 .../org/fdroid/fdroid/data/RepoProvider.java  | 24 ++++++++++++---
 .../fdroid/localrepo/LocalRepoManager.java    |  2 --
 .../fdroid/net/WifiStateChangeService.java    |  2 +-
 .../fdroid/views/LocalRepoActivity.java       |  6 ++--
 .../fdroid/views/ManageReposActivity.java     |  2 +-
 .../fdroid/views/RepoDetailsActivity.java     |  2 +-
 .../swap/ConfirmReceiveSwapFragment.java      | 17 +++++++++--
 .../fdroid/views/swap/SelectAppsFragment.java | 29 ++++++++++++++-----
 .../fdroid/views/swap/SwapActivity.java       | 13 +++++----
 .../fdroid/views/swap/SwapProcessManager.java |  6 ++++
 .../fdroid/views/swap/WifiQrFragment.java     |  6 ++--
 15 files changed, 116 insertions(+), 53 deletions(-)

diff --git a/F-Droid/src/org/fdroid/fdroid/UpdateService.java b/F-Droid/src/org/fdroid/fdroid/UpdateService.java
index ff124bbc4..2213ddb2f 100644
--- a/F-Droid/src/org/fdroid/fdroid/UpdateService.java
+++ b/F-Droid/src/org/fdroid/fdroid/UpdateService.java
@@ -346,14 +346,15 @@ public class UpdateService extends IntentService implements ProgressListener {
             List<Repo> repos = RepoProvider.Helper.all(this);
 
             // Process each repo...
-            Map<String, App> appsToUpdate = new HashMap<String, App>();
-            List<Apk> apksToUpdate = new ArrayList<Apk>();
-            List<Repo> unchangedRepos = new ArrayList<Repo>();
-            List<Repo> updatedRepos = new ArrayList<Repo>();
-            List<Repo> disabledRepos = new ArrayList<Repo>();
-            List<CharSequence> errorRepos = new ArrayList<CharSequence>();
-            ArrayList<CharSequence> repoErrors = new ArrayList<CharSequence>();
-            List<RepoUpdater.RepoUpdateRememberer> repoUpdateRememberers = new ArrayList<RepoUpdater.RepoUpdateRememberer>();
+            Map<String, App> appsToUpdate = new HashMap<>();
+            List<Apk> apksToUpdate = new ArrayList<>();
+            List<Repo> swapRepos = new ArrayList<>();
+            List<Repo> unchangedRepos = new ArrayList<>();
+            List<Repo> updatedRepos = new ArrayList<>();
+            List<Repo> disabledRepos = new ArrayList<>();
+            List<CharSequence> errorRepos = new ArrayList<>();
+            ArrayList<CharSequence> repoErrors = new ArrayList<>();
+            List<RepoUpdater.RepoUpdateRememberer> repoUpdateRememberers = new ArrayList<>();
             boolean changes = false;
             for (final Repo repo : repos) {
 
@@ -363,6 +364,9 @@ public class UpdateService extends IntentService implements ProgressListener {
                 } else if (!TextUtils.isEmpty(address) && !repo.address.equals(address)) {
                     unchangedRepos.add(repo);
                     continue;
+                } else if (TextUtils.isEmpty(address) && repo.isSwap) {
+                    swapRepos.add(repo);
+                    continue;
                 }
 
                 sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address));
diff --git a/F-Droid/src/org/fdroid/fdroid/Utils.java b/F-Droid/src/org/fdroid/fdroid/Utils.java
index 4b904b7f5..3f4472a43 100644
--- a/F-Droid/src/org/fdroid/fdroid/Utils.java
+++ b/F-Droid/src/org/fdroid/fdroid/Utils.java
@@ -232,11 +232,7 @@ public final class Utils {
                 }
                 eventType = xml.nextToken();
             }
-        } catch (NameNotFoundException e) {
-            e.printStackTrace();
-        } catch (IOException e) {
-            e.printStackTrace();
-        } catch (XmlPullParserException e) {
+        } catch (NameNotFoundException | IOException | XmlPullParserException e) {
             e.printStackTrace();
         }
         return 8; // some kind of hopeful default
@@ -284,7 +280,7 @@ public final class Utils {
         return displayFP;
     }
 
-    public static Uri getSharingUri(Context context, Repo repo) {
+    public static Uri getSharingUri(Repo repo) {
         if (TextUtils.isEmpty(repo.address))
             return Uri.parse("http://wifi-not-enabled");
         Uri uri = Uri.parse(repo.address.replaceFirst("http", "fdroidrepo"));
@@ -347,8 +343,8 @@ public final class Utils {
             digest.update(key);
             byte[] fingerprint = digest.digest();
             Formatter formatter = new Formatter(new StringBuilder());
-            for (int i = 0; i < fingerprint.length; i++) {
-                formatter.format("%02X", fingerprint[i]);
+            for (byte aFingerprint : fingerprint) {
+                formatter.format("%02X", aFingerprint);
             }
             ret = formatter.toString();
             formatter.close();
@@ -434,14 +430,14 @@ public final class Utils {
 
     public static String getBinaryHash(File apk, String algo) {
         FileInputStream fis = null;
-        BufferedInputStream bis = null;
+        BufferedInputStream bis;
         try {
             MessageDigest md = MessageDigest.getInstance(algo);
             fis = new FileInputStream(apk);
             bis = new BufferedInputStream(fis);
 
             byte[] dataBytes = new byte[524288];
-            int nread = 0;
+            int nread;
 
             while ((nread = bis.read(dataBytes)) != -1)
                 md.update(dataBytes, 0, nread);
@@ -485,7 +481,7 @@ public final class Utils {
                     listNum = -1;
                 else
                     output.append('\n');
-            } else if (opening && tag.equals("ol")) {
+            } else if (tag.equals("ol")) {
                 if (opening)
                     listNum = 1;
                 else
diff --git a/F-Droid/src/org/fdroid/fdroid/data/DBHelper.java b/F-Droid/src/org/fdroid/fdroid/data/DBHelper.java
index 6ad4950d3..01f2a8316 100644
--- a/F-Droid/src/org/fdroid/fdroid/data/DBHelper.java
+++ b/F-Droid/src/org/fdroid/fdroid/data/DBHelper.java
@@ -33,7 +33,8 @@ public class DBHelper extends SQLiteOpenHelper {
             + "priority integer not null, pubkey text, fingerprint text, "
             + "maxage integer not null default 0, "
             + "version integer not null default 0, "
-            + "lastetag text, lastUpdated string);";
+            + "lastetag text, lastUpdated string,"
+            + "isSwap integer boolean default 0);";
 
     private static final String CREATE_TABLE_APK =
             "CREATE TABLE " + TABLE_APK + " ( "
@@ -98,7 +99,7 @@ public class DBHelper extends SQLiteOpenHelper {
             + InstalledAppProvider.DataColumns.APPLICATION_LABEL + " TEXT NOT NULL "
             + " );";
 
-    private static final int DB_VERSION = 46;
+    private static final int DB_VERSION = 47;
 
     private Context context;
 
@@ -273,6 +274,7 @@ public class DBHelper extends SQLiteOpenHelper {
         populateRepoNames(db, oldVersion);
         if (oldVersion < 43) createInstalledApp(db);
         addAppLabelToInstalledCache(db, oldVersion);
+        addIsSwapToRepo(db, oldVersion);
     }
 
     /**
@@ -400,6 +402,13 @@ public class DBHelper extends SQLiteOpenHelper {
         }
     }
 
+    private void addIsSwapToRepo(SQLiteDatabase db, int oldVersion) {
+        if (oldVersion < 47 && !columnExists(db, TABLE_REPO, "isSwap")) {
+            Log.i(TAG, "Adding isSwap field to " + TABLE_REPO + " table in db.");
+            db.execSQL("alter table " + TABLE_REPO + " add column isSwap boolean default 0;");
+        }
+    }
+
     private void resetTransient(SQLiteDatabase db, int oldVersion) {
         // Before version 42, only transient info was stored in here. As of some time
         // just before 42 (F-Droid 0.60ish) it now has "ignore this version" info which
diff --git a/F-Droid/src/org/fdroid/fdroid/data/Repo.java b/F-Droid/src/org/fdroid/fdroid/data/Repo.java
index fd9fee9da..934ebaddb 100644
--- a/F-Droid/src/org/fdroid/fdroid/data/Repo.java
+++ b/F-Droid/src/org/fdroid/fdroid/data/Repo.java
@@ -30,6 +30,7 @@ public class Repo extends ValueObject {
     public int maxage; // maximum age of index that will be accepted - 0 for any
     public String lastetag; // last etag we updated from, null forces update
     public Date lastUpdated;
+    public boolean isSwap;
 
     public Repo() {
 
@@ -65,6 +66,8 @@ public class Repo extends ValueObject {
                 pubkey = cursor.getString(i);
             } else if (column.equals(RepoProvider.DataColumns.PRIORITY)) {
                 priority = cursor.getInt(i);
+            } else if (column.equals(RepoProvider.DataColumns.IS_SWAP)) {
+                isSwap = cursor.getInt(i) == 1;
             }
         }
     }
@@ -140,7 +143,7 @@ public class Repo extends ValueObject {
         }
 
         if (values.containsKey(RepoProvider.DataColumns.IN_USE)) {
-            inuse = toInt(values.getAsInteger(RepoProvider.DataColumns.FINGERPRINT)) == 1;
+            inuse = toInt(values.getAsInteger(RepoProvider.DataColumns.IN_USE)) == 1;
         }
 
         if (values.containsKey(RepoProvider.DataColumns.LAST_UPDATED)) {
@@ -173,5 +176,9 @@ public class Repo extends ValueObject {
         if (values.containsKey(RepoProvider.DataColumns.PRIORITY)) {
             priority = toInt(values.getAsInteger(RepoProvider.DataColumns.PRIORITY));
         }
+
+        if (values.containsKey(RepoProvider.DataColumns.IS_SWAP)) {
+            isSwap= toInt(values.getAsInteger(RepoProvider.DataColumns.IS_SWAP)) == 1;
+        }
     }
 }
diff --git a/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java b/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java
index 7253c64b9..1dce6b415 100644
--- a/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java
+++ b/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java
@@ -218,19 +218,24 @@ public class RepoProvider extends FDroidProvider {
         public static String LAST_ETAG    = "lastetag";
         public static String LAST_UPDATED = "lastUpdated";
         public static String VERSION      = "version";
+        public static String IS_SWAP      = "isSwap";
 
         public static String[] ALL = {
             _ID, ADDRESS, NAME, DESCRIPTION, IN_USE, PRIORITY, PUBLIC_KEY,
-            FINGERPRINT, MAX_AGE, LAST_UPDATED, LAST_ETAG, VERSION
+            FINGERPRINT, MAX_AGE, LAST_UPDATED, LAST_ETAG, VERSION, IS_SWAP
         };
     }
 
     private static final String PROVIDER_NAME = "RepoProvider";
+    private static final String PATH_ALL_EXCEPT_SWAP = "allExceptSwap";
+
+    private static final int CODE_ALL_EXCEPT_SWAP = CODE_SINGLE + 1;
 
     private static final UriMatcher matcher = new UriMatcher(-1);
 
     static {
         matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, null, CODE_LIST);
+        matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, PATH_ALL_EXCEPT_SWAP, CODE_ALL_EXCEPT_SWAP);
         matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, "#", CODE_SINGLE);
     }
 
@@ -242,6 +247,12 @@ public class RepoProvider extends FDroidProvider {
         return ContentUris.withAppendedId(getContentUri(), repoId);
     }
 
+    public static Uri allExceptSwapUri() {
+        return getContentUri().buildUpon()
+                .appendPath(PATH_ALL_EXCEPT_SWAP)
+                .build();
+    }
+
     @Override
     protected String getTableName() {
         return DBHelper.TABLE_REPO;
@@ -260,11 +271,12 @@ public class RepoProvider extends FDroidProvider {
     @Override
     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
 
+        if (TextUtils.isEmpty(sortOrder)) {
+            sortOrder = "_ID ASC";
+        }
+
         switch (matcher.match(uri)) {
             case CODE_LIST:
-                if (TextUtils.isEmpty(sortOrder)) {
-                    sortOrder = "_ID ASC";
-                }
                 break;
 
             case CODE_SINGLE:
@@ -272,6 +284,10 @@ public class RepoProvider extends FDroidProvider {
                         DataColumns._ID + " = " + uri.getLastPathSegment();
                 break;
 
+            case CODE_ALL_EXCEPT_SWAP:
+                selection = DataColumns.IS_SWAP + " = 0";
+                break;
+
             default:
                 Log.e(TAG, "Invalid URI for repo content provider: " + uri);
                 throw new UnsupportedOperationException("Invalid URI for repo content provider: " + uri);
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
index 09ead04f9..983c98632 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
@@ -1,6 +1,5 @@
 package org.fdroid.fdroid.localrepo;
 
-import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.pm.ApplicationInfo;
@@ -255,7 +254,6 @@ public class LocalRepoManager {
         }
     }
 
-    @TargetApi(9)
     public void addApp(Context context, String packageName) {
         App app;
         try {
diff --git a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
index ac72dee81..786847d1a 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
@@ -82,7 +82,7 @@ public class WifiStateChangeService extends Service {
 
                 Context context = WifiStateChangeService.this.getApplicationContext();
                 LocalRepoManager lrm = LocalRepoManager.get(context);
-                lrm.writeIndexPage(Utils.getSharingUri(context, FDroidApp.repo).toString());
+                lrm.writeIndexPage(Utils.getSharingUri(FDroidApp.repo).toString());
 
                 if (isCancelled())
                     return null;
diff --git a/F-Droid/src/org/fdroid/fdroid/views/LocalRepoActivity.java b/F-Droid/src/org/fdroid/fdroid/views/LocalRepoActivity.java
index 75ae057e2..e03dfda37 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/LocalRepoActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/LocalRepoActivity.java
@@ -246,7 +246,7 @@ public class LocalRepoActivity extends ActionBarActivity {
          * 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()
+        final String qrUriString = Utils.getSharingUri(FDroidApp.repo).toString()
                 .replaceFirst("fdroidrepo", "http")
                 .replaceAll("ssid=[^?]*", "")
                 .toUpperCase(Locale.ENGLISH);
@@ -270,7 +270,7 @@ public class LocalRepoActivity extends ActionBarActivity {
             if (nfcAdapter == null)
                 return;
             nfcAdapter.setNdefPushMessage(new NdefMessage(new NdefRecord[] {
-                    NdefRecord.createUri(Utils.getSharingUri(this, FDroidApp.repo)),
+                    NdefRecord.createUri(Utils.getSharingUri(FDroidApp.repo)),
             }), this);
         }
     }
@@ -292,7 +292,7 @@ public class LocalRepoActivity extends ActionBarActivity {
             progressDialog = new ProgressDialog(c);
             progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
             progressDialog.setTitle(R.string.updating);
-            sharingUri = Utils.getSharingUri(c, FDroidApp.repo);
+            sharingUri = Utils.getSharingUri(FDroidApp.repo);
         }
 
         @Override
diff --git a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java
index eb39e5e84..84ef99565 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java
@@ -611,7 +611,7 @@ public class ManageReposActivity extends ActionBarActivity {
 
         @Override
         public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
-            Uri uri = RepoProvider.getContentUri();
+            Uri uri = RepoProvider.allExceptSwapUri();
             Log.i(TAG, "Creating repo loader '" + uri + "'.");
             String[] projection = {
                     RepoProvider.DataColumns._ID,
diff --git a/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java b/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java
index 8968da71a..67c3b90a6 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/RepoDetailsActivity.java
@@ -66,7 +66,7 @@ public class RepoDetailsActivity extends ActionBarActivity {
 
     @TargetApi(14)
     private void setNfc() {
-        if (NfcHelper.setPushMessage(this, Utils.getSharingUri(this, repo))) {
+        if (NfcHelper.setPushMessage(this, Utils.getSharingUri(repo))) {
             findViewById(android.R.id.content).post(new Runnable() {
                 @Override
                 public void run() {
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceiveSwapFragment.java b/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceiveSwapFragment.java
index 772ee4747..7daf35690 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceiveSwapFragment.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceiveSwapFragment.java
@@ -4,7 +4,9 @@ import android.app.Activity;
 import android.content.ContentValues;
 import android.net.Uri;
 import android.os.Bundle;
+import android.support.annotation.NonNull;
 import android.support.v4.app.Fragment;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -18,6 +20,8 @@ import org.fdroid.fdroid.data.RepoProvider;
 
 public class ConfirmReceiveSwapFragment extends Fragment implements ProgressListener {
 
+    private static final String TAG = "org.fdroid.fdroid.views.swap.ConfirmReceiveSwapFragment";
+
     private NewRepoConfig newRepoConfig;
 
     @Override
@@ -64,22 +68,29 @@ public class ConfirmReceiveSwapFragment extends Fragment implements ProgressList
         UpdateService.updateRepoNow(repo.address, getActivity()).setListener(this);
     }
 
+    @NonNull
     private Repo ensureRepoExists() {
         // TODO: newRepoConfig.getUri() will include a fingerprint, which may not match with
         // the repos address in the database.
         Repo repo = RepoProvider.Helper.findByAddress(getActivity(), newRepoConfig.getUriString());
         if (repo == null) {
-            ContentValues values = new ContentValues(5);
+            ContentValues values = new ContentValues(6);
 
-             // TODO: i18n and think about most appropriate name. Although ideally, it will not be seen often,
-             // because we're whacking a pretty UI over the swap process so they don't need to "Manage repos"...
+             // TODO: i18n and think about most appropriate name. Although it wont be visible in
+             // the "Manage repos" UI after being marked as a swap repo here...
             values.put(RepoProvider.DataColumns.NAME, "Swap");
             values.put(RepoProvider.DataColumns.ADDRESS, newRepoConfig.getUriString());
             values.put(RepoProvider.DataColumns.DESCRIPTION, ""); // TODO;
             values.put(RepoProvider.DataColumns.FINGERPRINT, newRepoConfig.getFingerprint());
             values.put(RepoProvider.DataColumns.IN_USE, true);
+            values.put(RepoProvider.DataColumns.IS_SWAP, true);
             Uri uri = RepoProvider.Helper.insert(getActivity(), values);
             repo = RepoProvider.Helper.findByUri(getActivity(), uri);
+        } else if (!repo.isSwap) {
+            Log.d(TAG, "Old local repo being marked as \"Swap\" repo, so that it wont appear in the list of repositories in the future.");
+            ContentValues values = new ContentValues(1);
+            values.put(RepoProvider.DataColumns.IS_SWAP, true);
+            RepoProvider.Helper.update(getActivity(), repo, values);
         }
         return repo;
     }
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java
index f84a435c6..eb3f6a1fc 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java
@@ -13,7 +13,6 @@ import android.support.v4.content.CursorLoader;
 import android.support.v4.content.Loader;
 import android.support.v4.view.MenuItemCompat;
 import android.support.v4.widget.CursorAdapter;
-import android.support.v4.widget.SimpleCursorAdapter;
 import android.support.v7.widget.SearchView;
 import android.text.TextUtils;
 import android.view.*;
@@ -30,8 +29,13 @@ import java.util.Set;
 public class SelectAppsFragment extends ThemeableListFragment
     implements LoaderManager.LoaderCallbacks<Cursor>, SearchView.OnQueryTextListener {
 
+    @SuppressWarnings("UnusedDeclaration")
+    private static final String TAG = "org.fdroid.fdroid.views.swap.SelectAppsFragment";
+
     private String mCurrentFilterString;
-    private Set<String> previouslySelectedApps = new HashSet<>();
+
+    @NonNull
+    private final Set<String> previouslySelectedApps = new HashSet<>();
 
     public Set<String> getSelectedApps() {
         return FDroidApp.selectedApps;
@@ -120,7 +124,11 @@ public class SelectAppsFragment extends ThemeableListFragment
 
     @Override
     public void onListItemClick(ListView l, View v, int position, long id) {
-        Cursor c = (Cursor) l.getAdapter().getItem(position);
+        toggleAppSelected(position);
+    }
+
+    private void toggleAppSelected(int position) {
+        Cursor c = (Cursor) getListAdapter().getItem(position);
         String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID));
         if (FDroidApp.selectedApps.contains(packageName)) {
             FDroidApp.selectedApps.remove(packageName);
@@ -208,7 +216,10 @@ public class SelectAppsFragment extends ThemeableListFragment
         return R.layout.swap_create_header;
     }
 
-    private static class AppListAdapter extends CursorAdapter {
+    private class AppListAdapter extends CursorAdapter {
+
+        @SuppressWarnings("UnusedDeclaration")
+        private static final String TAG = "org.fdroid.fdroid.views.swap.SelectAppsFragment.AppListAdapter";
 
         @Nullable
         private LayoutInflater inflater;
@@ -276,12 +287,16 @@ public class SelectAppsFragment extends ThemeableListFragment
             if (checkBoxView != null) {
                 CheckBox checkBox = (CheckBox)checkBoxView;
                 checkBox.setOnCheckedChangeListener(null);
-                checkBox.setChecked(listView.isItemChecked(cursor.getPosition()));
-                final int position = cursor.getPosition();
+
+                final int cursorPosition = cursor.getPosition();
+                final int listPosition = cursor.getPosition() + 1; // To account for the header view.
+
+                checkBox.setChecked(listView.isItemChecked(listPosition));
                 checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                     @Override
                     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
-                        listView.setItemChecked(position, isChecked);
+                        listView.setItemChecked(listPosition, isChecked);
+                        toggleAppSelected(cursorPosition);
                     }
                 });
             }
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
index ba816d918..2f010cd9c 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
@@ -5,6 +5,7 @@ import android.content.Context;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
+import android.support.annotation.NonNull;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v7.app.ActionBarActivity;
@@ -114,7 +115,7 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
         // Even if they opted to skip the message which says "Touch devices to swap",
         // we still want to actually enable the feature, so that they could touch
         // during the wifi qr code being shown too.
-        boolean nfcMessageReady = NfcHelper.setPushMessage(this, Utils.getSharingUri(this, FDroidApp.repo));
+        boolean nfcMessageReady = NfcHelper.setPushMessage(this, Utils.getSharingUri(FDroidApp.repo));
 
         if (Preferences.get().showNfcDuringSwap() && nfcMessageReady) {
             showFragment(new NfcSwapFragment(), STATE_NFC);
@@ -200,16 +201,16 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
 
     class UpdateAsyncTask extends AsyncTask<Void, String, Void> {
         private static final String TAG = "fdroid.SwapActivity.UpdateAsyncTask";
-        private ProgressDialog progressDialog;
-        private Set<String> selectedApps;
-        private Uri sharingUri;
+        private final ProgressDialog progressDialog;
+        private final Set<String> selectedApps;
+        private final Uri sharingUri;
 
-        public UpdateAsyncTask(Context c, Set<String> apps) {
+        public UpdateAsyncTask(Context c, @NonNull Set<String> apps) {
             selectedApps = apps;
             progressDialog = new ProgressDialog(c);
             progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
             progressDialog.setTitle(R.string.updating);
-            sharingUri = Utils.getSharingUri(c, FDroidApp.repo);
+            sharingUri = Utils.getSharingUri(FDroidApp.repo);
         }
 
         @Override
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapProcessManager.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapProcessManager.java
index 0153fc578..4bd1c3c58 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapProcessManager.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapProcessManager.java
@@ -1,5 +1,11 @@
 package org.fdroid.fdroid.views.swap;
 
+/**
+ * Defines the contract between the {@link org.fdroid.fdroid.views.swap.SwapActivity}
+ * and the fragments which live in it. The fragments each have the responsibility of
+ * moving to the next stage of the process, and are entitled to stop swapping too
+ * (e.g. when a "Cancel" button is pressed).
+ */
 public interface SwapProcessManager {
     public void nextStep();
     public void stopSwapping();
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrFragment.java b/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrFragment.java
index a71db32eb..4767fb4ee 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrFragment.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrFragment.java
@@ -112,7 +112,7 @@ public class WifiQrFragment extends Fragment {
 
     private void setUIFromWifi() {
 
-        if (TextUtils.isEmpty(FDroidApp.repo.address))
+        if (TextUtils.isEmpty(FDroidApp.repo.address) || getView() == null)
             return;
 
         String scheme = Preferences.get().isLocalRepoHttpsEnabled() ? "https://" : "http://";
@@ -129,8 +129,8 @@ public class WifiQrFragment extends Fragment {
          * 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:// :-(
          */
-        Uri sharingUri = Utils.getSharingUri(getActivity(), FDroidApp.repo);
-        String qrUriString = (scheme + sharingUri.getHost()).toUpperCase(Locale.ENGLISH);
+        Uri sharingUri = Utils.getSharingUri(FDroidApp.repo);
+        String qrUriString = ( scheme + sharingUri.getHost() ).toUpperCase(Locale.ENGLISH);
         if (sharingUri.getPort() != 80) {
             qrUriString += ":" + sharingUri.getPort();
         }

From 20f17da913c650ee76940810b5352a2f958956b5 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Thu, 5 Mar 2015 16:57:45 +1100
Subject: [PATCH 07/16] Clean after merge. Refactor swap index.xml generation
 (to work with API 7).

Althought the construction of the XML document was fine witn Android 7,
the actual serialization of it was limited to 8 or higher. Try as I might,
I couldn't find a way to figure out how to serialize a DOM tree on API 7.

Turns out that the "PullParser" API is able to build and serialize XML
trees on API 7. It's a little clunkier than the DOM alternative, so I
refactored out the generation into a subclass to make it clearer what
it is doing and when.
---
 .../fdroid/localrepo/LocalRepoManager.java    | 350 +++++++++---------
 1 file changed, 174 insertions(+), 176 deletions(-)

diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
index 983c98632..e0ea8f85d 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
@@ -20,23 +20,17 @@ import android.text.TextUtils;
 import android.util.Log;
 import android.widget.Toast;
 
+import org.fdroid.fdroid.FDroidApp;
 import org.fdroid.fdroid.Hasher;
 import org.fdroid.fdroid.Preferences;
 import org.fdroid.fdroid.R;
 import org.fdroid.fdroid.Utils;
 import org.fdroid.fdroid.data.App;
 import org.fdroid.fdroid.data.SanitizedFile;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
 
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerException;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.stream.StreamResult;
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.BufferedReader;
@@ -44,13 +38,17 @@ import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
+import java.io.Writer;
 import java.security.cert.CertificateEncodingException;
+import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -68,7 +66,6 @@ public class LocalRepoManager {
     private final Context context;
     private final PackageManager pm;
     private final AssetManager assetManager;
-    private final SharedPreferences prefs;
     private final String fdroidPackageName;
 
     private static String[] WEB_ROOT_ASSET_FILES = {
@@ -101,7 +98,6 @@ public class LocalRepoManager {
         context = c.getApplicationContext();
         pm = c.getPackageManager();
         assetManager = c.getAssets();
-        prefs = PreferenceManager.getDefaultSharedPreferences(c);
         fdroidPackageName = c.getPackageName();
 
         webRoot = SanitizedFile.knownSanitized(c.getFilesDir());
@@ -201,10 +197,8 @@ public class LocalRepoManager {
     }
 
     private static void attemptToDelete(File file) {
-            Utils.symlinkOrCopyFile(new SanitizedFile(new File(directory, symlinkPrefix), fileName), file);
-            if (!file.delete()) {
-                Log.e(TAG, "Could not delete \"" + file.getAbsolutePath() + "\".");
-            }
+        if (!file.delete()) {
+            Log.e(TAG, "Could not delete \"" + file.getAbsolutePath() + "\".");
         }
     }
 
@@ -318,170 +312,12 @@ public class LocalRepoManager {
         return new File(iconsDir, packageName + "_" + versionCode + ".png");
     }
 
-    private void writeIndexXML() throws TransformerException, ParserConfigurationException, LocalRepoKeyStore.InitException {
-        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 = Preferences.get().getLocalRepoName();
-
-        Element repo = doc.createElement("repo");
-        repo.setAttribute("icon", "blah.png");
-        repo.setAttribute("maxage", String.valueOf(repoMaxAge));
-        repo.setAttribute("name", repoName + " on " + FDroidApp.ipAddressString);
-        repo.setAttribute("pubkey", Hasher.hex(LocalRepoKeyStore.get(context).getCertificate()));
-        long timestamp = System.currentTimeMillis() / 1000L;
-        repo.setAttribute("timestamp", String.valueOf(timestamp));
-        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);
-    }
-
     public void writeIndexJar() throws IOException {
         try {
-            writeIndexXML();
+            new IndexXmlBuilder(context, apps).build(new FileWriter(xmlIndex));
         } catch (Exception e) {
-            Toast.makeText(context, R.string.failed_to_create_index, Toast.LENGTH_LONG).show();
             Log.e(TAG, Log.getStackTraceString(e));
+            Toast.makeText(context, R.string.failed_to_create_index, Toast.LENGTH_LONG).show();
             return;
         }
 
@@ -513,4 +349,166 @@ public class LocalRepoManager {
         }
 
     }
+
+    /**
+     * Helper class to aid in constructing index.xml file.
+     * It uses the PullParser API, because the DOM api is only able to be serialized from
+     * API 8 upwards, but we support 7 at time of implementation.
+     */
+    public static class IndexXmlBuilder {
+
+        @NonNull
+        private final XmlSerializer serializer;
+
+        @NonNull
+        private final Map<String, App> apps;
+
+        @NonNull
+        private final Context context;
+
+        @NonNull
+        private final DateFormat dateToStr = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
+
+        public IndexXmlBuilder(@NonNull Context context, @NonNull Map<String, App> apps) throws XmlPullParserException, IOException {
+            this.context = context;
+            this.apps = apps;
+            serializer = XmlPullParserFactory.newInstance().newSerializer();
+        }
+
+        public void build(Writer output) throws IOException, LocalRepoKeyStore.InitException {
+            serializer.setOutput(output);
+            serializer.startDocument(null, null);
+            tagFdroid();
+            serializer.endDocument();
+        }
+
+        private void tagFdroid() throws IOException, LocalRepoKeyStore.InitException {
+            serializer.startTag("", "fdroid");
+            tagRepo();
+            for (Entry<String, App> entry : apps.entrySet()) {
+                tagApplication(entry.getValue());
+            }
+            serializer.endTag("", "fdroid");
+        }
+
+        private void tagRepo() throws IOException, LocalRepoKeyStore.InitException {
+
+            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+
+            // 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();
+
+            serializer.startTag("", "repo");
+
+            serializer.attribute("", "icon", "blah.png");
+            serializer.attribute("", "maxage", String.valueOf(repoMaxAge));
+            serializer.attribute("", "name", Preferences.get().getLocalRepoName() + " on " + FDroidApp.ipAddressString);
+            serializer.attribute("", "pubkey", Hasher.hex(LocalRepoKeyStore.get(context).getCertificate()));
+            long timestamp = System.currentTimeMillis() / 1000L;
+            serializer.attribute("", "timestamp", String.valueOf(timestamp));
+
+            tag("description", "A local FDroid repo generated from apps installed on " + Preferences.get().getLocalRepoName());
+
+            serializer.endTag("", "repo");
+
+        }
+
+        /**
+         * Helper function to start a tag called "name", fill it with text "text", and then
+         * end the tag in a more concise manner.
+         */
+        private void tag(String name, String text) throws IOException {
+            serializer.startTag("", name).text(text).endTag("", name);
+        }
+
+        /**
+         * Alias for {@link org.fdroid.fdroid.localrepo.LocalRepoManager.IndexXmlBuilder#tag(String, String)}
+         * That accepts a number instead of string.
+         * @see IndexXmlBuilder#tag(String, String)
+         */
+        private void tag(String name, long number) throws IOException {
+            tag(name, String.valueOf(number));
+        }
+
+        /**
+         * Alias for {@link org.fdroid.fdroid.localrepo.LocalRepoManager.IndexXmlBuilder#tag(String, String)}
+         * that accepts a date instead of a string.
+         * @see IndexXmlBuilder#tag(String, String)
+         */
+        private void tag(String name, Date date) throws IOException {
+            tag(name, dateToStr.format(date));
+        }
+
+        private void tagApplication(App app) throws IOException {
+            serializer.startTag("", "application");
+
+            tag("id", app.id);
+            tag("added", app.added);
+            tag("lastupdated", app.lastUpdated);
+            tag("name", app.name);
+            tag("summary", app.summary);
+            tag("icon", app.icon);
+            tag("desc", app.description);
+            tag("license", "Unknown");
+            tag("categories", "LocalRepo," + Preferences.get().getLocalRepoName());
+            tag("category", "LocalRepo," + Preferences.get().getLocalRepoName());
+            tag("web", "web");
+            tag("source", "source");
+            tag("tracker", "tracker");
+            tag("marketversion", app.installedApk.version);
+            tag("marketvercode", app.installedApk.vercode);
+
+            tagPackage(app);
+
+            serializer.endTag("", "application");
+        }
+
+        private void tagPackage(App app) throws IOException {
+            serializer.startTag("", "package");
+
+            tag("version", app.installedApk.version);
+            tag("versioncode", app.installedApk.vercode);
+            tag("apkname", app.installedApk.apkName);
+            tagHash(app);
+            tag("sig", app.installedApk.sig.toLowerCase(Locale.US));
+            tag("size", app.installedApk.installedFile.length());
+            tag("sdkver", app.installedApk.minSdkVersion);
+            tag("added", app.installedApk.added);
+            tagFeatures(app);
+            tagPermissions(app);
+
+            serializer.endTag("", "package");
+        }
+
+        private void tagPermissions(App app) throws IOException {
+            serializer.startTag("", "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))
+                    serializer.text(out.substring(0, out.length() - 1));
+            }
+            serializer.endTag("", "permissions");
+        }
+
+        private void tagFeatures(App app) throws IOException {
+            serializer.startTag("", "features");
+            if (app.installedApk.features != null)
+                serializer.text(Utils.CommaSeparatedList.str(app.installedApk.features));
+            serializer.endTag("", "features");
+        }
+
+        private void tagHash(App app) throws IOException {
+            serializer.startTag("", "hash");
+            serializer.attribute("", "type", app.installedApk.hashType);
+            serializer.text(app.installedApk.hash.toLowerCase(Locale.US));
+            serializer.endTag("", "hash");
+        }
+    }
+
 }

From 842ddb5e2415bd873fca05e7600928330490de46 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Thu, 5 Mar 2015 22:54:09 +1100
Subject: [PATCH 08/16] Trying to make LocalRepo stuff have less subtle side
 effects.

I had trouble wrapping my head around which point in time the fdroid/repo
directories are created, when they are populated with .html files, and
when the index.xml is put there. I did some minor cleaning up to make
it a bit easier to manage this in the future.
---
 .../fdroid/localrepo/LocalRepoManager.java    | 41 ++++++++++---------
 1 file changed, 22 insertions(+), 19 deletions(-)

diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
index e0ea8f85d..f1a238d97 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
@@ -81,7 +81,9 @@ public class LocalRepoManager {
     private SanitizedFile xmlIndexJarUnsigned = null;
     public final SanitizedFile webRoot;
     public final SanitizedFile fdroidDir;
+    public final SanitizedFile fdroidDirCaps;
     public final SanitizedFile repoDir;
+    public final SanitizedFile repoDirCaps;
     public final SanitizedFile iconsDir;
 
     @Nullable
@@ -103,7 +105,9 @@ public class LocalRepoManager {
         webRoot = SanitizedFile.knownSanitized(c.getFilesDir());
         /* /fdroid/repo is the standard path for user repos */
         fdroidDir = new SanitizedFile(webRoot, "fdroid");
+        fdroidDirCaps = new SanitizedFile(webRoot, "FDROID");
         repoDir = new SanitizedFile(fdroidDir, "repo");
+        repoDirCaps = new SanitizedFile(fdroidDirCaps, "REPO");
         iconsDir = new SanitizedFile(repoDir, "icons");
         xmlIndex = new SanitizedFile(repoDir, "index.xml");
         xmlIndexJar = new SanitizedFile(repoDir, "index.jar");
@@ -163,18 +167,15 @@ public class LocalRepoManager {
 
             // make symlinks/copies in each subdir of the repo to make sure that
             // the user will always find the bootstrap page.
-            symlinkIndexPageElsewhere("../", fdroidDir);
-            symlinkIndexPageElsewhere("../../", repoDir);
+            symlinkEntireWebRootElsewhere("../", fdroidDir);
+            symlinkEntireWebRootElsewhere("../../", repoDir);
 
             // add in /FDROID/REPO to support bad QR Scanner apps
-            File fdroidCAPS = new File(fdroidDir.getParentFile(), "FDROID");
-            attemptToMkdir(fdroidCAPS);
+            attemptToMkdir(fdroidDirCaps);
+            attemptToMkdir(repoDirCaps);
 
-            File repoCAPS = new File(fdroidCAPS, "REPO");
-            attemptToMkdir(repoCAPS);
-
-            symlinkIndexPageElsewhere("../", fdroidCAPS);
-            symlinkIndexPageElsewhere("../../", repoCAPS);
+            symlinkEntireWebRootElsewhere("../", fdroidDirCaps);
+            symlinkEntireWebRootElsewhere("../../", repoDirCaps);
 
         } catch (IOException e) {
             Log.e(TAG, "Error writing local repo index: " + e.getMessage());
@@ -192,28 +193,29 @@ public class LocalRepoManager {
         }
 
         if (!dir.mkdir()) {
-            throw new IOException("An error occured trying to create directory " + dir);
+            throw new IOException("An error occurred trying to create directory " + dir);
         }
     }
 
-    private static void attemptToDelete(File file) {
+    private static void attemptToDelete(@NonNull File file) {
         if (!file.delete()) {
             Log.e(TAG, "Could not delete \"" + file.getAbsolutePath() + "\".");
         }
     }
 
-    private void symlinkIndexPageElsewhere(String symlinkPrefix, File directory) {
-        SanitizedFile index = new SanitizedFile(directory, "index.html");
-        attemptToDelete(index);
-        Utils.symlinkOrCopyFile(new SanitizedFile(new File(directory, symlinkPrefix), "index.html"), index);
-
+    private void symlinkEntireWebRootElsewhere(String symlinkPrefix, File directory) {
+        symlinkFileElsewhere("index.html", symlinkPrefix, directory);
         for(String fileName : WEB_ROOT_ASSET_FILES) {
-            SanitizedFile file = new SanitizedFile(directory, fileName);
-            attemptToDelete(file);
-            Utils.symlinkOrCopyFile(new SanitizedFile(new File(directory, symlinkPrefix), fileName), file);
+            symlinkFileElsewhere(fileName, symlinkPrefix, directory);
         }
     }
 
+    private void symlinkFileElsewhere(String fileName, String symlinkPrefix, File directory) {
+        SanitizedFile index = new SanitizedFile(directory, fileName);
+        attemptToDelete(index);
+        Utils.symlinkOrCopyFile(new SanitizedFile(new File(directory, symlinkPrefix), fileName), index);
+    }
+
     private void deleteContents(File path) {
         if (path.exists()) {
             for (File file : path.listFiles()) {
@@ -441,6 +443,7 @@ public class LocalRepoManager {
 
         private void tagApplication(App app) throws IOException {
             serializer.startTag("", "application");
+            serializer.attribute("", "id", app.id);
 
             tag("id", app.id);
             tag("added", app.added);

From f5ce318be7b42f5a2807e493817352504a2f2932 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Sat, 15 Nov 2014 22:26:55 +1100
Subject: [PATCH 09/16] Fix for fragment not displaying on 2.3 device.

Also, app details now goes to the "Swap app list" rather than the main list of
apps after hitting the "Up" button from the ActionBar.
---
 F-Droid/AndroidManifest.xml                   |  8 +++++++
 .../fdroid/views/swap/SwapActivity.java       | 21 ++++++++++------
 .../views/swap/SwapAppListActivity.java       | 24 +++++++++++++++----
 3 files changed, 42 insertions(+), 11 deletions(-)

diff --git a/F-Droid/AndroidManifest.xml b/F-Droid/AndroidManifest.xml
index d90c2ae38..7e96eb0c7 100644
--- a/F-Droid/AndroidManifest.xml
+++ b/F-Droid/AndroidManifest.xml
@@ -329,6 +329,14 @@
             </intent-filter>
 
         </activity>
+        <activity
+            android:name=".views.swap.SwapAppListActivity$SwapAppDetails"
+            android:label="@string/app_details"
+            android:parentActivityName=".views.swap.SwapAppListActivity" >
+            <meta-data
+                android:name="android.support.PARENT_ACTIVITY"
+                android:value=".views.swap.SwapAppListActivity" />
+        </activity>
         <activity
             android:label="@string/menu_preferences"
             android:name=".PreferencesActivity"
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
index 2f010cd9c..c2a474afb 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
@@ -6,6 +6,7 @@ import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
+import android.os.Handler;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v7.app.ActionBarActivity;
@@ -83,15 +84,21 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
 
             setContentView(R.layout.swap_activity);
 
-            showFragment(new StartSwapFragment(), STATE_START_SWAP);
+            new Handler().post(new Runnable() {
+                @Override
+                public void run() {
 
-            if (FDroidApp.isLocalRepoServiceRunning()) {
-                showSelectApps();
-                showJoinWifi();
-                attemptToShowNfc();
-                showWifiQr();
-            }
+                    showFragment(new StartSwapFragment(), STATE_START_SWAP);
 
+                    if (FDroidApp.isLocalRepoServiceRunning()) {
+                        showSelectApps();
+                        showJoinWifi();
+                        attemptToShowNfc();
+                        showWifiQr();
+                    }
+
+                }
+            });
         }
 
     }
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java
index 2aa1f910f..862852ed8 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java
@@ -2,8 +2,11 @@ package org.fdroid.fdroid.views.swap;
 
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
 import android.support.annotation.Nullable;
 import android.support.v7.app.ActionBarActivity;
+
+import org.fdroid.fdroid.AppDetails;
 import org.fdroid.fdroid.R;
 import org.fdroid.fdroid.data.AppProvider;
 import org.fdroid.fdroid.views.AppListAdapter;
@@ -18,10 +21,15 @@ public class SwapAppListActivity extends ActionBarActivity {
         super.onCreate(savedInstanceState);
 
         if (savedInstanceState == null) {
-            getSupportFragmentManager()
-                    .beginTransaction()
-                    .add(android.R.id.content, new SwapAppListFragment())
-                    .commit();
+            new Handler().post(new Runnable() {
+                @Override
+                public void run() {
+                    getSupportFragmentManager()
+                        .beginTransaction()
+                        .add(android.R.id.content, new SwapAppListFragment())
+                        .commit();
+                }
+            });
         }
 
     }
@@ -56,4 +64,12 @@ public class SwapAppListActivity extends ActionBarActivity {
 
     }
 
+    /**
+     * Here so that the AndroidManifest.xml can specify the "parent" activity from this
+     * can be different form the regular AppDetails. That is - the AppDetails goes back
+     * to the main app list, but the SwapAppDetails will go back to the "Swap app list"
+     * activity.
+     */
+    public static class SwapAppDetails extends AppDetails {}
+
 }

From 85300331e05ddd55d4361da1a91cbf1e9b362076 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Wed, 18 Mar 2015 07:15:58 +1100
Subject: [PATCH 10/16] Minor cleanup after CR.

Formatting, spelling, NonNull/Nullable annotations, removing unused
imports, and adding SuppressWarning for unused logging "TAG" properties.
---
 .../src/org/fdroid/fdroid/UpdateService.java  |  5 +-
 F-Droid/src/org/fdroid/fdroid/Utils.java      |  3 +
 .../org/fdroid/fdroid/data/RepoProvider.java  |  1 +
 .../fdroid/localrepo/LocalRepoManager.java    | 76 +++++++++----------
 .../fdroid/views/swap/SwapActivity.java       |  9 +++
 .../views/swap/SwapAppListActivity.java       |  2 +
 .../fdroid/views/swap/WifiQrFragment.java     |  4 +-
 7 files changed, 58 insertions(+), 42 deletions(-)

diff --git a/F-Droid/src/org/fdroid/fdroid/UpdateService.java b/F-Droid/src/org/fdroid/fdroid/UpdateService.java
index 2213ddb2f..6520106b4 100644
--- a/F-Droid/src/org/fdroid/fdroid/UpdateService.java
+++ b/F-Droid/src/org/fdroid/fdroid/UpdateService.java
@@ -356,15 +356,16 @@ public class UpdateService extends IntentService implements ProgressListener {
             ArrayList<CharSequence> repoErrors = new ArrayList<>();
             List<RepoUpdater.RepoUpdateRememberer> repoUpdateRememberers = new ArrayList<>();
             boolean changes = false;
+            boolean singleRepoUpdate = !TextUtils.isEmpty(address);
             for (final Repo repo : repos) {
 
                 if (!repo.inuse) {
                     disabledRepos.add(repo);
                     continue;
-                } else if (!TextUtils.isEmpty(address) && !repo.address.equals(address)) {
+                } else if (singleRepoUpdate && !repo.address.equals(address)) {
                     unchangedRepos.add(repo);
                     continue;
-                } else if (TextUtils.isEmpty(address) && repo.isSwap) {
+                } else if (!singleRepoUpdate && repo.isSwap) {
                     swapRepos.add(repo);
                     continue;
                 }
diff --git a/F-Droid/src/org/fdroid/fdroid/Utils.java b/F-Droid/src/org/fdroid/fdroid/Utils.java
index 3f4472a43..164faf3d3 100644
--- a/F-Droid/src/org/fdroid/fdroid/Utils.java
+++ b/F-Droid/src/org/fdroid/fdroid/Utils.java
@@ -23,6 +23,7 @@ import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.AssetManager;
 import android.content.res.XmlResourceParser;
 import android.net.Uri;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.text.Editable;
 import android.text.Html;
@@ -59,6 +60,7 @@ import java.util.Locale;
 
 public final class Utils {
 
+    @SuppressWarnings("UnusedDeclaration")
     private static final String TAG = "org.fdroid.fdroid.Utils";
 
     public static final int BUFFER_SIZE = 4096;
@@ -280,6 +282,7 @@ public final class Utils {
         return displayFP;
     }
 
+    @NonNull
     public static Uri getSharingUri(Repo repo) {
         if (TextUtils.isEmpty(repo.address))
             return Uri.parse("http://wifi-not-enabled");
diff --git a/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java b/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java
index 1dce6b415..0497460d0 100644
--- a/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java
+++ b/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java
@@ -277,6 +277,7 @@ public class RepoProvider extends FDroidProvider {
 
         switch (matcher.match(uri)) {
             case CODE_LIST:
+                // Do nothing (don't restrict query)
                 break;
 
             case CODE_SINGLE:
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
index f1a238d97..cedd6d8a9 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
@@ -314,44 +314,6 @@ public class LocalRepoManager {
         return new File(iconsDir, packageName + "_" + versionCode + ".png");
     }
 
-    public void writeIndexJar() throws IOException {
-        try {
-            new IndexXmlBuilder(context, apps).build(new FileWriter(xmlIndex));
-        } catch (Exception e) {
-            Log.e(TAG, Log.getStackTraceString(e));
-            Toast.makeText(context, R.string.failed_to_create_index, Toast.LENGTH_LONG).show();
-            return;
-        }
-
-        BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(xmlIndexJarUnsigned));
-        JarOutputStream jo = new JarOutputStream(bo);
-
-        BufferedInputStream bi = new BufferedInputStream(new FileInputStream(xmlIndex));
-
-        JarEntry je = new JarEntry("index.xml");
-        jo.putNextEntry(je);
-
-        byte[] buf = new byte[1024];
-        int bytesRead;
-
-        while ((bytesRead = bi.read(buf)) != -1) {
-            jo.write(buf, 0, bytesRead);
-        }
-
-        bi.close();
-        jo.close();
-        bo.close();
-
-        try {
-            LocalRepoKeyStore.get(context).signZip(xmlIndexJarUnsigned, xmlIndexJar);
-        } catch (LocalRepoKeyStore.InitException e) {
-            throw new IOException("Could not sign index - keystore failed to initialize");
-        } finally {
-            attemptToDelete(xmlIndexJarUnsigned);
-        }
-
-    }
-
     /**
      * Helper class to aid in constructing index.xml file.
      * It uses the PullParser API, because the DOM api is only able to be serialized from
@@ -514,4 +476,42 @@ public class LocalRepoManager {
         }
     }
 
+    public void writeIndexJar() throws IOException {
+        try {
+            new IndexXmlBuilder(context, apps).build(new FileWriter(xmlIndex));
+        } catch (Exception e) {
+            Log.e(TAG, Log.getStackTraceString(e));
+            Toast.makeText(context, R.string.failed_to_create_index, Toast.LENGTH_LONG).show();
+            return;
+        }
+
+        BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(xmlIndexJarUnsigned));
+        JarOutputStream jo = new JarOutputStream(bo);
+
+        BufferedInputStream bi = new BufferedInputStream(new FileInputStream(xmlIndex));
+
+        JarEntry je = new JarEntry("index.xml");
+        jo.putNextEntry(je);
+
+        byte[] buf = new byte[1024];
+        int bytesRead;
+
+        while ((bytesRead = bi.read(buf)) != -1) {
+            jo.write(buf, 0, bytesRead);
+        }
+
+        bi.close();
+        jo.close();
+        bo.close();
+
+        try {
+            LocalRepoKeyStore.get(context).signZip(xmlIndexJarUnsigned, xmlIndexJar);
+        } catch (LocalRepoKeyStore.InitException e) {
+            throw new IOException("Could not sign index - keystore failed to initialize");
+        } finally {
+            attemptToDelete(xmlIndexJarUnsigned);
+        }
+
+    }
+
 }
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
index c2a474afb..6232cb17e 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapActivity.java
@@ -84,6 +84,7 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
 
             setContentView(R.layout.swap_activity);
 
+            // Necessary to run on an Android 2.3.[something] device.
             new Handler().post(new Runnable() {
                 @Override
                 public void run() {
@@ -207,9 +208,17 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
     }
 
     class UpdateAsyncTask extends AsyncTask<Void, String, Void> {
+
+        @SuppressWarnings("UnusedDeclaration")
         private static final String TAG = "fdroid.SwapActivity.UpdateAsyncTask";
+
+        @NonNull
         private final ProgressDialog progressDialog;
+
+        @NonNull
         private final Set<String> selectedApps;
+
+        @NonNull
         private final Uri sharingUri;
 
         public UpdateAsyncTask(Context c, @NonNull Set<String> apps) {
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java
index 862852ed8..f45670508 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java
@@ -21,6 +21,8 @@ public class SwapAppListActivity extends ActionBarActivity {
         super.onCreate(savedInstanceState);
 
         if (savedInstanceState == null) {
+
+            // Necessary to run on an Android 2.3.[something] device.
             new Handler().post(new Runnable() {
                 @Override
                 public void run() {
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrFragment.java b/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrFragment.java
index 4767fb4ee..543ccca1a 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrFragment.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrFragment.java
@@ -130,7 +130,7 @@ public class WifiQrFragment extends Fragment {
          * custom URI schemes, so we have to use http:// or https:// :-(
          */
         Uri sharingUri = Utils.getSharingUri(FDroidApp.repo);
-        String qrUriString = ( scheme + sharingUri.getHost() ).toUpperCase(Locale.ENGLISH);
+        String qrUriString = (scheme + sharingUri.getHost()).toUpperCase(Locale.ENGLISH);
         if (sharingUri.getPort() != 80) {
             qrUriString += ":" + sharingUri.getPort();
         }
@@ -139,7 +139,7 @@ public class WifiQrFragment extends Fragment {
 
         // Andorid provides an API for getting the query parameters and iterating over them:
         //   Uri.getQueryParameterNames()
-        // But it is only available on later Android versions. As such we URLEncodedUtils instead.
+        // But it is only available on later Android versions. As such we use URLEncodedUtils instead.
         List<NameValuePair> parameters = URLEncodedUtils.parse(URI.create(sharingUri.toString()), "UTF-8");
         for (NameValuePair parameter : parameters) {
             if (!parameter.getName().equals("ssid")) {

From da566b44cea0ddc4d105b4475f9f6700925c41a4 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Wed, 18 Mar 2015 08:09:57 +1100
Subject: [PATCH 11/16] Don't show swap apps in the main list of apps.

Achieve this by joining the fdroid_app table onto fdroid_apk and then
fdroid_repo, then checking if fdroid_repo.isSwap is 1.
---
 .../org/fdroid/fdroid/data/AppProvider.java   | 48 ++++++++++++++++++-
 .../org/fdroid/fdroid/data/QueryBuilder.java  | 15 +++++-
 .../swap/ConfirmReceiveSwapFragment.java      | 10 ++--
 .../views/swap/ConnectSwapActivity.java       |  6 ++-
 .../views/swap/SwapAppListActivity.java       | 29 ++++++++++-
 5 files changed, 99 insertions(+), 9 deletions(-)

diff --git a/F-Droid/src/org/fdroid/fdroid/data/AppProvider.java b/F-Droid/src/org/fdroid/fdroid/data/AppProvider.java
index 3e1fce0f1..e3ec1b4ba 100644
--- a/F-Droid/src/org/fdroid/fdroid/data/AppProvider.java
+++ b/F-Droid/src/org/fdroid/fdroid/data/AppProvider.java
@@ -262,7 +262,13 @@ public class AppProvider extends FDroidProvider {
 
         @Override
         protected String getRequiredTables() {
-            return DBHelper.TABLE_APP;
+            final String app  = DBHelper.TABLE_APP;
+            final String apk  = DBHelper.TABLE_APK;
+            final String repo = DBHelper.TABLE_REPO;
+
+            return app +
+                " LEFT JOIN " + apk + " ON ( " + apk + ".id = " + app + ".id ) " +
+                " LEFT JOIN " + repo + " ON ( " + apk + ".repo = " + repo + "._id )";
         }
 
         @Override
@@ -270,6 +276,11 @@ public class AppProvider extends FDroidProvider {
             return fieldCount() == 1 && categoryFieldAdded;
         }
 
+        @Override
+        protected String groupBy() {
+            return DBHelper.TABLE_APP + ".id";
+        }
+
         public void addSelection(AppQuerySelection selection) {
             addSelection(selection.getSelection());
             if (selection.naturalJoinToInstalled()) {
@@ -372,6 +383,7 @@ public class AppProvider extends FDroidProvider {
     private static final String PATH_CATEGORY = "category";
     private static final String PATH_IGNORED = "ignored";
     private static final String PATH_CALC_APP_DETAILS_FROM_INDEX = "calcDetailsFromIndex";
+    private static final String PATH_REPO = "repo";
 
     private static final int CAN_UPDATE       = CODE_SINGLE + 1;
     private static final int INSTALLED        = CAN_UPDATE + 1;
@@ -383,6 +395,7 @@ public class AppProvider extends FDroidProvider {
     private static final int CATEGORY         = NEWLY_ADDED + 1;
     private static final int IGNORED          = CATEGORY + 1;
     private static final int CALC_APP_DETAILS_FROM_INDEX = IGNORED + 1;
+    private static final int REPO             = CALC_APP_DETAILS_FROM_INDEX + 1;
 
     static {
         matcher.addURI(getAuthority(), null, CODE_LIST);
@@ -392,6 +405,7 @@ public class AppProvider extends FDroidProvider {
         matcher.addURI(getAuthority(), PATH_NEWLY_ADDED, NEWLY_ADDED);
         matcher.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY);
         matcher.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH);
+        matcher.addURI(getAuthority(), PATH_REPO + "/#", REPO);
         matcher.addURI(getAuthority(), PATH_CAN_UPDATE, CAN_UPDATE);
         matcher.addURI(getAuthority(), PATH_INSTALLED, INSTALLED);
         matcher.addURI(getAuthority(), PATH_NO_APKS, NO_APKS);
@@ -438,6 +452,13 @@ public class AppProvider extends FDroidProvider {
         return Uri.withAppendedPath(getContentUri(), PATH_CAN_UPDATE);
     }
 
+    public static Uri getRepoUri(Repo repo) {
+        return getContentUri().buildUpon()
+            .appendPath(PATH_REPO)
+            .appendPath(String.valueOf(repo.id))
+            .build();
+    }
+
     public static Uri getContentUri(List<App> apps) {
         StringBuilder builder = new StringBuilder();
         for (int i = 0; i < apps.size(); i ++) {
@@ -494,6 +515,12 @@ public class AppProvider extends FDroidProvider {
         return new AppQuerySelection(where).requireNaturalInstalledTable();
     }
 
+    private AppQuerySelection queryRepo(long repoId) {
+        String selection = " fdroid_apk.repo = ? ";
+        String[] args = { String.valueOf(repoId) };
+        return new AppQuerySelection(selection, args);
+    }
+
     private AppQuerySelection queryInstalled() {
         return new AppQuerySelection().requireNaturalInstalledTable();
     }
@@ -555,6 +582,14 @@ public class AppProvider extends FDroidProvider {
         return new AppQuerySelection(selection);
     }
 
+    private AppQuerySelection queryExcludeSwap() {
+        // fdroid_repo will have null fields if the LEFT JOIN didn't resolve, e.g. due to there
+        // being no apks for the app in the result set. In that case, we can't tell if it is from
+        // a swap repo or not.
+        String selection = " fdroid_repo.isSwap = 0 OR fdroid_repo.isSwap is null ";
+        return new AppQuerySelection(selection);
+    }
+
     private AppQuerySelection queryNewlyAdded() {
         String selection = "fdroid_app.added > ?";
         String[] args = { Utils.DATE_FORMAT.format(Preferences.get().calcMaxHistory()) };
@@ -599,11 +634,13 @@ public class AppProvider extends FDroidProvider {
     public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) {
         Query query = new Query();
         AppQuerySelection selection = new AppQuerySelection(customSelection, selectionArgs);
+        boolean includeSwap = false;
         switch (matcher.match(uri)) {
             case CODE_LIST:
                 break;
 
             case CODE_SINGLE:
+                includeSwap = true;
                 selection = selection.add(querySingle(uri.getLastPathSegment()));
                 break;
 
@@ -611,6 +648,11 @@ public class AppProvider extends FDroidProvider {
                 selection = selection.add(queryCanUpdate());
                 break;
 
+            case REPO:
+                includeSwap = true;
+                selection = selection.add(queryRepo(Long.parseLong(uri.getLastPathSegment())));
+                break;
+
             case INSTALLED:
                 selection = selection.add(queryInstalled());
                 break;
@@ -650,6 +692,10 @@ public class AppProvider extends FDroidProvider {
                 throw new UnsupportedOperationException("Invalid URI for app content provider: " + uri);
         }
 
+        if (!includeSwap) {
+            selection = selection.add(queryExcludeSwap());
+        }
+
         if (AppProvider.DataColumns.NAME.equals(sortOrder)) {
             sortOrder = " lower( fdroid_app." + sortOrder + " ) ";
         }
diff --git a/F-Droid/src/org/fdroid/fdroid/data/QueryBuilder.java b/F-Droid/src/org/fdroid/fdroid/data/QueryBuilder.java
index 63cb4dd37..d74086262 100644
--- a/F-Droid/src/org/fdroid/fdroid/data/QueryBuilder.java
+++ b/F-Droid/src/org/fdroid/fdroid/data/QueryBuilder.java
@@ -28,6 +28,10 @@ abstract class QueryBuilder {
         return false;
     }
 
+    protected String groupBy() {
+        return null;
+    }
+
     protected void appendField(String field) {
         appendField(field, null, null);
     }
@@ -85,6 +89,10 @@ abstract class QueryBuilder {
             .append(')');
     }
 
+    private String distinctSql() {
+        return isDistinct() ? " DISTINCT " : "";
+    }
+
     private String fieldsSql() {
         StringBuilder sb = new StringBuilder();
         for (int i = 0; i < fields.size(); i ++) {
@@ -104,12 +112,15 @@ abstract class QueryBuilder {
         return orderBy != null ? " ORDER BY " + orderBy : "";
     }
 
+    private String groupBySql() {
+        return groupBy() != null ? " GROUP BY " + groupBy() : "";
+    }
+
     private String tablesSql() {
         return tables.toString();
     }
 
     public String toString() {
-        String distinct = isDistinct() ? " DISTINCT " : "";
-        return "SELECT " + distinct + fieldsSql() + " FROM " + tablesSql() + whereSql() + orderBySql();
+        return "SELECT " + distinctSql() + fieldsSql() + " FROM " + tablesSql() + whereSql() + groupBySql() + orderBySql();
     }
 }
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceiveSwapFragment.java b/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceiveSwapFragment.java
index 7daf35690..9f0401cf1 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceiveSwapFragment.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceiveSwapFragment.java
@@ -5,6 +5,7 @@ import android.content.ContentValues;
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.support.v4.app.Fragment;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -24,6 +25,9 @@ public class ConfirmReceiveSwapFragment extends Fragment implements ProgressList
 
     private NewRepoConfig newRepoConfig;
 
+    @Nullable
+    private Repo repo;
+
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 
@@ -64,8 +68,8 @@ public class ConfirmReceiveSwapFragment extends Fragment implements ProgressList
     }
 
     private void confirm() {
-        Repo repo = ensureRepoExists();
-        UpdateService.updateRepoNow(repo.address, getActivity()).setListener(this);
+        this.repo = ensureRepoExists();
+        UpdateService.updateRepoNow(this.repo.address, getActivity()).setListener(this);
     }
 
     @NonNull
@@ -104,7 +108,7 @@ public class ConfirmReceiveSwapFragment extends Fragment implements ProgressList
 
         if (event.type.equals(UpdateService.EVENT_COMPLETE_AND_SAME) ||
                 event.type.equals(UpdateService.EVENT_COMPLETE_WITH_CHANGES)) {
-            ((ConnectSwapActivity)getActivity()).onRepoUpdated();
+            ((ConnectSwapActivity)getActivity()).onRepoUpdated(repo);
             /*Intent intent = new Intent();
             intent.putExtra("category", newRepoConfig.getHost()); // TODO: Load repo from database to get proper name. This is what the category we want to select will be called.
             getActivity().setResult(Activity.RESULT_OK, intent);
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java
index 56231bef5..60f065ec7 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java
@@ -5,10 +5,11 @@ import android.os.Bundle;
 import android.support.v4.app.FragmentActivity;
 import android.support.v4.app.FragmentManager;
 
+import org.fdroid.fdroid.data.Repo;
+
 public class ConnectSwapActivity extends FragmentActivity {
 
     private static final String STATE_CONFIRM = "startSwap";
-    private static final String STATE_APP_LIST = "swapAppList";
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -42,9 +43,10 @@ public class ConnectSwapActivity extends FragmentActivity {
         return lastFragment.getName();
     }
 
-    public void onRepoUpdated() {
+    public void onRepoUpdated(Repo repo) {
 
         Intent intent = new Intent(this, SwapAppListActivity.class);
+        intent.putExtra(SwapAppListActivity.EXTRA_REPO_ADDRESS, repo.address);
         startActivity(intent);
 
     }
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java
index f45670508..fd1ed73a3 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java
@@ -1,5 +1,6 @@
 package org.fdroid.fdroid.views.swap;
 
+import android.app.Activity;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
@@ -9,12 +10,18 @@ import android.support.v7.app.ActionBarActivity;
 import org.fdroid.fdroid.AppDetails;
 import org.fdroid.fdroid.R;
 import org.fdroid.fdroid.data.AppProvider;
+import org.fdroid.fdroid.data.Repo;
+import org.fdroid.fdroid.data.RepoProvider;
 import org.fdroid.fdroid.views.AppListAdapter;
 import org.fdroid.fdroid.views.AvailableAppListAdapter;
 import org.fdroid.fdroid.views.fragments.AppListFragment;
 
 public class SwapAppListActivity extends ActionBarActivity {
 
+    public static String EXTRA_REPO_ADDRESS = "repoAddress";
+
+    private Repo repo;
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
 
@@ -36,8 +43,28 @@ public class SwapAppListActivity extends ActionBarActivity {
 
     }
 
+    @Override
+    protected void onResume() {
+        super.onResume();
+
+        String repoAddress = getIntent().getStringExtra(EXTRA_REPO_ADDRESS);
+        repo = RepoProvider.Helper.findByAddress(this, repoAddress);
+    }
+
+    public Repo getRepo() {
+        return repo;
+    }
+
     public static class SwapAppListFragment extends AppListFragment {
 
+        private Repo repo;
+
+        @Override
+        public void onAttach(Activity activity) {
+            super.onAttach(activity);
+            repo = ((SwapAppListActivity)activity).getRepo();
+        }
+
         @Override
         protected int getHeaderLayout() {
             return R.layout.swap_success_header;
@@ -61,7 +88,7 @@ public class SwapAppListActivity extends ActionBarActivity {
 
         @Override
         protected Uri getDataUri() {
-            return AppProvider.getCategoryUri("LocalRepo");
+            return AppProvider.getRepoUri(repo);
         }
 
     }

From 9dfa18aeadfa6d0e3220a98f554dae653ee1817b Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Mon, 23 Mar 2015 23:15:27 +1100
Subject: [PATCH 12/16] Make F-Droid tests runnable from Gradle.

This also makes AndroidStudio integration work better, which makes
running and debugging tests much nicer than the CLI.

Also cleaned up imports in one test, and made the symlink tests not
fail on older devices below API 19.
---
 F-Droid/build.gradle                              | 15 ++++++++++++++-
 .../src/org/fdroid/fdroid/FileCompatTest.java     |  9 +++++++--
 .../fdroid/updater/SignedRepoUpdaterTest.java     |  6 ------
 3 files changed, 21 insertions(+), 9 deletions(-)

diff --git a/F-Droid/build.gradle b/F-Droid/build.gradle
index e24437876..4ea25a9c6 100644
--- a/F-Droid/build.gradle
+++ b/F-Droid/build.gradle
@@ -39,6 +39,8 @@ if ( !hasProperty( 'sourceDeps' ) ) {
 
         // Upstream doesn't have a binary on mavenCentral.
         compile(name: 'zipsigner')
+
+        androidTestCompile 'commons-io:commons-io:2.2'
     }
 
 } else {
@@ -76,6 +78,8 @@ if ( !hasProperty( 'sourceDeps' ) ) {
         compile 'com.android.support:support-v4:20.0.+',
                 'com.android.support:appcompat-v7:20.0.+',
                 'com.android.support:support-annotations:20.0.+'
+
+        androidTestCompile 'commons-io:commons-io:2.2'
     }
 
 }
@@ -135,7 +139,16 @@ android {
             assets.srcDirs = ['assets']
         }
 
-        instrumentTest.setRoot('test')
+        androidTest.setRoot('test')
+        androidTest {
+            manifest.srcFile 'test/AndroidManifest.xml'
+            java.srcDirs = ['test/src']
+            resources.srcDirs = ['test/src']
+            aidl.srcDirs = ['test/src']
+            renderscript.srcDirs = ['test/src']
+            res.srcDirs = ['test/res']
+            assets.srcDirs = ['test/assets']
+        }
     }
 
     buildTypes {
diff --git a/F-Droid/test/src/org/fdroid/fdroid/FileCompatTest.java b/F-Droid/test/src/org/fdroid/fdroid/FileCompatTest.java
index 4f1c48110..1ab615b3c 100644
--- a/F-Droid/test/src/org/fdroid/fdroid/FileCompatTest.java
+++ b/F-Droid/test/src/org/fdroid/fdroid/FileCompatTest.java
@@ -25,8 +25,13 @@ public class FileCompatTest extends InstrumentationTestCase {
     }
 
     public void tearDown() {
-        assertTrue("Can't delete " + sourceFile.getAbsolutePath() + ".", sourceFile.delete());
-        assertTrue("Can't delete " + destFile.getAbsolutePath() + ".", destFile.delete());
+        if (sourceFile.exists()) {
+            assertTrue("Can't delete " + sourceFile.getAbsolutePath() + ".", sourceFile.delete());
+        }
+
+        if (destFile.exists()) {
+            assertTrue("Can't delete " + destFile.getAbsolutePath() + ".", destFile.delete());
+        }
     }
 
     public void testSymlinkRuntime() {
diff --git a/F-Droid/test/src/org/fdroid/fdroid/updater/SignedRepoUpdaterTest.java b/F-Droid/test/src/org/fdroid/fdroid/updater/SignedRepoUpdaterTest.java
index e11f9639c..e94a3faa1 100644
--- a/F-Droid/test/src/org/fdroid/fdroid/updater/SignedRepoUpdaterTest.java
+++ b/F-Droid/test/src/org/fdroid/fdroid/updater/SignedRepoUpdaterTest.java
@@ -3,21 +3,15 @@ package org.fdroid.fdroid.updater;
 
 import android.annotation.TargetApi;
 import android.content.Context;
-import android.os.Environment;
 import android.test.InstrumentationTestCase;
-import android.util.Log;
 
 import org.apache.commons.io.FileUtils;
 import org.fdroid.fdroid.TestUtils;
-import org.fdroid.fdroid.Utils;
 import org.fdroid.fdroid.data.Repo;
 import org.fdroid.fdroid.updater.RepoUpdater.UpdateException;
 
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
 
 @TargetApi(8)
 public class SignedRepoUpdaterTest extends InstrumentationTestCase {

From a16bc22c4aa27b0e698d36c63688bdfe3cc334f5 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Tue, 24 Mar 2015 19:55:29 +1100
Subject: [PATCH 13/16] Fixed broken + commented out tests.

They were all due to the addition of "application label" to the
installed app cache. This commit adds a mock ApplicationInfo
to the mock package manager and also specifies the label while
inserting into the test content provider.
---
 .../test/src/mock/MockApplicationInfo.java    | 19 +++++++++++++++++
 .../mock/MockInstallablePackageManager.java   |  8 ++++++-
 .../org/fdroid/fdroid/AppProviderTest.java    | 19 +++++++++--------
 .../fdroid/InstalledAppProviderTest.java      | 10 ++++-----
 .../test/src/org/fdroid/fdroid/TestUtils.java | 21 +++++++++----------
 5 files changed, 51 insertions(+), 26 deletions(-)
 create mode 100644 F-Droid/test/src/mock/MockApplicationInfo.java

diff --git a/F-Droid/test/src/mock/MockApplicationInfo.java b/F-Droid/test/src/mock/MockApplicationInfo.java
new file mode 100644
index 000000000..abd6c2e22
--- /dev/null
+++ b/F-Droid/test/src/mock/MockApplicationInfo.java
@@ -0,0 +1,19 @@
+package mock;
+
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+
+public class MockApplicationInfo extends ApplicationInfo {
+
+    private final PackageInfo info;
+
+    public MockApplicationInfo(PackageInfo info) {
+        this.info = info;
+    }
+
+    @Override
+    public CharSequence loadLabel(PackageManager pm) {
+        return "Mock app: " + info.packageName;
+    }
+}
diff --git a/F-Droid/test/src/mock/MockInstallablePackageManager.java b/F-Droid/test/src/mock/MockInstallablePackageManager.java
index f291c1517..7fc1a0758 100644
--- a/F-Droid/test/src/mock/MockInstallablePackageManager.java
+++ b/F-Droid/test/src/mock/MockInstallablePackageManager.java
@@ -1,5 +1,6 @@
 package mock;
 
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.test.mock.MockPackageManager;
 
@@ -9,7 +10,7 @@ import java.util.List;
 
 public class MockInstallablePackageManager extends MockPackageManager {
 
-    private List<PackageInfo> info = new ArrayList<PackageInfo>();
+    private List<PackageInfo> info = new ArrayList<>();
 
     @Override
     public List<PackageInfo> getInstalledPackages(int flags) {
@@ -30,6 +31,11 @@ public class MockInstallablePackageManager extends MockPackageManager {
         }
     }
 
+    @Override
+    public ApplicationInfo getApplicationInfo(String packageName, int flags) throws NameNotFoundException {
+        return new MockApplicationInfo(getPackageInfo(packageName));
+    }
+
     public PackageInfo getPackageInfo(String id) {
         for (PackageInfo i : info) {
             if (i.packageName.equals(id)) {
diff --git a/F-Droid/test/src/org/fdroid/fdroid/AppProviderTest.java b/F-Droid/test/src/org/fdroid/fdroid/AppProviderTest.java
index 644dbca7d..a769d5c83 100644
--- a/F-Droid/test/src/org/fdroid/fdroid/AppProviderTest.java
+++ b/F-Droid/test/src/org/fdroid/fdroid/AppProviderTest.java
@@ -1,14 +1,18 @@
 package org.fdroid.fdroid;
 
+import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.res.Resources;
 import android.database.Cursor;
 
 import mock.MockCategoryResources;
+import mock.MockContextSwappableComponents;
+import mock.MockInstallablePackageManager;
 
 import org.fdroid.fdroid.data.ApkProvider;
 import org.fdroid.fdroid.data.App;
 import org.fdroid.fdroid.data.AppProvider;
+import org.fdroid.fdroid.data.InstalledAppCacheUpdater;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -43,7 +47,6 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
      * the AppProvider used to stumble across this bug when asking for installed apps,
      * and the device had over 1000 apps installed.
      */
-/* TODO fix me
     public void testMaxSqliteParams() {
 
         MockInstallablePackageManager pm = new MockInstallablePackageManager();
@@ -74,7 +77,7 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
 
         assertResultCount(3, AppProvider.getInstalledUri());
     }
-*/
+
     public void testCantFindApp() {
         assertNull(AppProvider.Helper.findById(getMockContentResolver(), "com.example.doesnt-exist"));
     }
@@ -92,7 +95,7 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
         App app = new App();
         app.id = "org.fdroid.fdroid";
 
-        List<App> apps = new ArrayList<App>(1);
+        List<App> apps = new ArrayList<>(1);
         apps.add(app);
 
         assertValidUri(AppProvider.getContentUri(app));
@@ -105,7 +108,6 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
         assertNotNull(cursor);
     }
 
-/* TODO fix me
     private void insertApps(int count) {
         for (int i = 0; i < count; i ++) {
             insertApp("com.example.test." + i, "Test app " + i);
@@ -122,7 +124,7 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
         values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, ignoreVercode);
         insertApp(id, "App: " + id, values);
 
-        TestUtils.installAndBroadcast(getMockContext(), packageManager, id, installedVercode, "v" + installedVercode);
+        TestUtils.installAndBroadcast(getSwappableContext(), packageManager, id, installedVercode, "v" + installedVercode);
     }
 
     public void testCanUpdate() {
@@ -173,7 +175,7 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
 
         Cursor canUpdateCursor = r.query(AppProvider.getCanUpdateUri(), AppProvider.DataColumns.ALL, null, null, null);
         canUpdateCursor.moveToFirst();
-        List<String> canUpdateIds = new ArrayList<String>(canUpdateCursor.getCount());
+        List<String> canUpdateIds = new ArrayList<>(canUpdateCursor.getCount());
         while (!canUpdateCursor.isAfterLast()) {
             canUpdateIds.add(new App(canUpdateCursor).id);
             canUpdateCursor.moveToNext();
@@ -224,7 +226,7 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
     }
 
     private void assertContainsOnlyIds(List<App> actualApps, String[] expectedIds) {
-        List<String> actualIds = new ArrayList<String>(actualApps.size());
+        List<String> actualIds = new ArrayList<>(actualApps.size());
         for (App app : actualApps) {
             actualIds.add(app.id);
         }
@@ -241,12 +243,11 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
         assertResultCount(0, AppProvider.getInstalledUri());
 
         for (int i = 10; i < 20; i ++) {
-            TestUtils.installAndBroadcast(getMockContext(), pm, "com.example.test." + i, i, "v1");
+            TestUtils.installAndBroadcast(getSwappableContext(), pm, "com.example.test." + i, i, "v1");
         }
 
         assertResultCount(10, AppProvider.getInstalledUri());
     }
-*/
 
     public void testInsert() {
 
diff --git a/F-Droid/test/src/org/fdroid/fdroid/InstalledAppProviderTest.java b/F-Droid/test/src/org/fdroid/fdroid/InstalledAppProviderTest.java
index e266d303d..bc1a176ce 100644
--- a/F-Droid/test/src/org/fdroid/fdroid/InstalledAppProviderTest.java
+++ b/F-Droid/test/src/org/fdroid/fdroid/InstalledAppProviderTest.java
@@ -37,7 +37,6 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro
         assertValidUri(InstalledAppProvider.getAppUri("blah"));
     }
 
-/* TODO fix me
     public void testInsert() {
 
         assertResultCount(0, InstalledAppProvider.getContentUri());
@@ -134,7 +133,7 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro
         assertIsInstalledVersionInDb("com.example.toKeep", 1, "v0.1");
 
     }
-*/
+
     @Override
     protected String[] getMinimalProjection() {
         return new String[] {
@@ -153,6 +152,7 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro
         if (appId != null) {
             values.put(InstalledAppProvider.DataColumns.APP_ID, appId);
         }
+        values.put(InstalledAppProvider.DataColumns.APPLICATION_LABEL, "Mock app: " + appId);
         values.put(InstalledAppProvider.DataColumns.VERSION_CODE, versionCode);
         values.put(InstalledAppProvider.DataColumns.VERSION_NAME, versionNumber);
         return values;
@@ -164,15 +164,15 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro
     }
 
     private void removeAndBroadcast(String appId) {
-        TestUtils.removeAndBroadcast(getMockContext(), getPackageManager(), appId);
+        TestUtils.removeAndBroadcast(getSwappableContext(), getPackageManager(), appId);
     }
 
     private void upgradeAndBroadcast(String appId, int versionCode, String versionName) {
-        TestUtils.upgradeAndBroadcast(getMockContext(), getPackageManager(), appId, versionCode, versionName);
+        TestUtils.upgradeAndBroadcast(getSwappableContext(), getPackageManager(), appId, versionCode, versionName);
     }
 
     private void installAndBroadcast(String appId, int versionCode, String versionName) {
-        TestUtils.installAndBroadcast(getMockContext(), getPackageManager(), appId, versionCode, versionName);
+        TestUtils.installAndBroadcast(getSwappableContext(), getPackageManager(), appId, versionCode, versionName);
     }
 
 }
diff --git a/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java b/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java
index 1f6b65c45..d33dcc4db 100644
--- a/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java
+++ b/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java
@@ -6,6 +6,8 @@ import android.net.Uri;
 import android.os.Environment;
 import android.util.Log;
 import junit.framework.AssertionFailedError;
+
+import mock.MockContextSwappableComponents;
 import mock.MockInstallablePackageManager;
 import org.fdroid.fdroid.data.ApkProvider;
 import org.fdroid.fdroid.data.AppProvider;
@@ -134,17 +136,12 @@ public class TestUtils {
      * Will tell {@code pm} that we are installing {@code appId}, and then alert the
      * {@link org.fdroid.fdroid.PackageAddedReceiver}. This will in turn update the
      * "installed apps" table in the database.
-     *
-     * Note: in order for this to work, the {@link AppProviderTest#getSwappableContext()}
-     * will need to be aware of the package manager that we have passed in. Therefore,
-     * you will have to have called
-     * {@link mock.MockContextSwappableComponents#setPackageManager(android.content.pm.PackageManager)}
-     * on the {@link AppProviderTest#getSwappableContext()} before invoking this method.
      */
     public static void installAndBroadcast(
-            Context context,  MockInstallablePackageManager pm,
+            MockContextSwappableComponents context,  MockInstallablePackageManager pm,
             String appId, int versionCode, String versionName) {
 
+        context.setPackageManager(pm);
         pm.install(appId, versionCode, versionName);
         Intent installIntent = new Intent(Intent.ACTION_PACKAGE_ADDED);
         installIntent.setData(Uri.parse("package:" + appId));
@@ -153,15 +150,16 @@ public class TestUtils {
     }
 
     /**
-     * @see org.fdroid.fdroid.TestUtils#installAndBroadcast(android.content.Context context, mock.MockInstallablePackageManager, String, int, String)
+     * @see org.fdroid.fdroid.TestUtils#installAndBroadcast(mock.MockContextSwappableComponents, mock.MockInstallablePackageManager, String, int, String)
      */
     public static void upgradeAndBroadcast(
-            Context context, MockInstallablePackageManager pm,
+            MockContextSwappableComponents context, MockInstallablePackageManager pm,
             String appId, int versionCode, String versionName) {
         /*
         removeAndBroadcast(context, pm, appId);
         installAndBroadcast(context, pm, appId, versionCode, versionName);
         */
+        context.setPackageManager(pm);
         pm.install(appId, versionCode, versionName);
         Intent installIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED);
         installIntent.setData(Uri.parse("package:" + appId));
@@ -170,10 +168,11 @@ public class TestUtils {
     }
 
     /**
-     * @see org.fdroid.fdroid.TestUtils#installAndBroadcast(android.content.Context context, mock.MockInstallablePackageManager, String, int, String)
+     * @see org.fdroid.fdroid.TestUtils#installAndBroadcast(mock.MockContextSwappableComponents, mock.MockInstallablePackageManager, String, int, String)
      */
-    public static void removeAndBroadcast(Context context, MockInstallablePackageManager pm, String appId) {
+    public static void removeAndBroadcast(MockContextSwappableComponents context, MockInstallablePackageManager pm, String appId) {
 
+        context.setPackageManager(pm);
         pm.remove(appId);
         Intent installIntent = new Intent(Intent.ACTION_PACKAGE_REMOVED);
         installIntent.setData(Uri.parse("package:" + appId));

From 2a481f6889246cb3fc9114687ff5084a355cebb5 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Fri, 27 Mar 2015 07:36:24 +1100
Subject: [PATCH 14/16] Correctly navigate "up" to the swap list.

When viewing app details from a swap list, we need to return to the
swap list when pressing "up". Previously it would go to the main list
of apps, and only return to the swap list when pressing "back".

Now, a subclass of AppDetails is used when in swap mode, which knows
how to navigate up to the correct task.
---
 F-Droid/src/org/fdroid/fdroid/AppDetails.java |  6 ++-
 .../org/fdroid/fdroid/data/AppProvider.java   | 14 ++++--
 .../org/fdroid/fdroid/data/RepoProvider.java  |  2 +-
 .../views/fragments/AppListFragment.java      | 18 +++++--
 .../views/swap/ConnectSwapActivity.java       |  4 +-
 .../views/swap/SwapAppListActivity.java       | 49 ++++++++++++++++---
 6 files changed, 72 insertions(+), 21 deletions(-)

diff --git a/F-Droid/src/org/fdroid/fdroid/AppDetails.java b/F-Droid/src/org/fdroid/fdroid/AppDetails.java
index f97e7908b..abf543b57 100644
--- a/F-Droid/src/org/fdroid/fdroid/AppDetails.java
+++ b/F-Droid/src/org/fdroid/fdroid/AppDetails.java
@@ -768,13 +768,17 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
         startActivity(intent);
     }
 
+    protected void navigateUp() {
+        NavUtils.navigateUpFromSameTask(this);
+    }
+
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
 
         switch (item.getItemId()) {
 
         case android.R.id.home:
-            NavUtils.navigateUpFromSameTask(this);
+            navigateUp();
             return true;
 
         case LAUNCH:
diff --git a/F-Droid/src/org/fdroid/fdroid/data/AppProvider.java b/F-Droid/src/org/fdroid/fdroid/data/AppProvider.java
index e3ec1b4ba..189220433 100644
--- a/F-Droid/src/org/fdroid/fdroid/data/AppProvider.java
+++ b/F-Droid/src/org/fdroid/fdroid/data/AppProvider.java
@@ -634,31 +634,36 @@ public class AppProvider extends FDroidProvider {
     public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) {
         Query query = new Query();
         AppQuerySelection selection = new AppQuerySelection(customSelection, selectionArgs);
-        boolean includeSwap = false;
+
+        // Queries which are for the main list of apps should not include swap apps.
+        boolean includeSwap = true;
+
         switch (matcher.match(uri)) {
             case CODE_LIST:
+                includeSwap = false;
                 break;
 
             case CODE_SINGLE:
-                includeSwap = true;
                 selection = selection.add(querySingle(uri.getLastPathSegment()));
                 break;
 
             case CAN_UPDATE:
                 selection = selection.add(queryCanUpdate());
+                includeSwap = false;
                 break;
 
             case REPO:
-                includeSwap = true;
                 selection = selection.add(queryRepo(Long.parseLong(uri.getLastPathSegment())));
                 break;
 
             case INSTALLED:
                 selection = selection.add(queryInstalled());
+                includeSwap = false;
                 break;
 
             case SEARCH:
                 selection = selection.add(querySearch(uri.getLastPathSegment()));
+                includeSwap = false;
                 break;
 
             case NO_APKS:
@@ -675,16 +680,19 @@ public class AppProvider extends FDroidProvider {
 
             case CATEGORY:
                 selection = selection.add(queryCategory(uri.getLastPathSegment()));
+                includeSwap = false;
                 break;
 
             case RECENTLY_UPDATED:
                 sortOrder = " fdroid_app.lastUpdated DESC";
                 selection = selection.add(queryRecentlyUpdated());
+                includeSwap = false;
                 break;
 
             case NEWLY_ADDED:
                 sortOrder = " fdroid_app.added DESC";
                 selection = selection.add(queryNewlyAdded());
+                includeSwap = false;
                 break;
 
             default:
diff --git a/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java b/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java
index 0497460d0..a4f250c03 100644
--- a/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java
+++ b/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java
@@ -286,7 +286,7 @@ public class RepoProvider extends FDroidProvider {
                 break;
 
             case CODE_ALL_EXCEPT_SWAP:
-                selection = DataColumns.IS_SWAP + " = 0";
+                selection = DataColumns.IS_SWAP + " = 0 OR " + DataColumns.IS_SWAP + " IS NULL ";
                 break;
 
             default:
diff --git a/F-Droid/src/org/fdroid/fdroid/views/fragments/AppListFragment.java b/F-Droid/src/org/fdroid/fdroid/views/fragments/AppListFragment.java
index e7d33c607..a34d5994c 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/fragments/AppListFragment.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/fragments/AppListFragment.java
@@ -142,11 +142,19 @@ abstract public class AppListFragment extends ThemeableListFragment implements
 
     @Override
     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-        final App app = new App((Cursor)getListView().getItemAtPosition(position));
-        Intent intent = new Intent(getActivity(), AppDetails.class);
-        intent.putExtra(AppDetails.EXTRA_APPID, app.id);
-        intent.putExtra(AppDetails.EXTRA_FROM, getFromTitle());
-        startActivityForResult(intent, FDroid.REQUEST_APPDETAILS);
+        // Cursor is null in the swap list when touching the first item.
+        Cursor cursor = (Cursor)getListView().getItemAtPosition(position);
+        if (cursor != null) {
+            final App app = new App(cursor);
+            Intent intent = getAppDetailsIntent();
+            intent.putExtra(AppDetails.EXTRA_APPID, app.id);
+            intent.putExtra(AppDetails.EXTRA_FROM, getFromTitle());
+            startActivityForResult(intent, FDroid.REQUEST_APPDETAILS);
+        }
+    }
+
+    protected Intent getAppDetailsIntent() {
+        return new Intent(getActivity(), AppDetails.class);
     }
 
     @Override
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java
index 60f065ec7..946cd3e77 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java
@@ -44,10 +44,8 @@ public class ConnectSwapActivity extends FragmentActivity {
     }
 
     public void onRepoUpdated(Repo repo) {
-
         Intent intent = new Intent(this, SwapAppListActivity.class);
-        intent.putExtra(SwapAppListActivity.EXTRA_REPO_ADDRESS, repo.address);
+        intent.putExtra(SwapAppListActivity.EXTRA_REPO_ID, repo.getId());
         startActivity(intent);
-
     }
 }
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java
index fd1ed73a3..bc935d73f 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java
@@ -1,11 +1,14 @@
 package org.fdroid.fdroid.views.swap;
 
 import android.app.Activity;
+import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
+import android.support.v4.app.NavUtils;
 import android.support.annotation.Nullable;
 import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
 
 import org.fdroid.fdroid.AppDetails;
 import org.fdroid.fdroid.R;
@@ -18,7 +21,9 @@ import org.fdroid.fdroid.views.fragments.AppListFragment;
 
 public class SwapAppListActivity extends ActionBarActivity {
 
-    public static String EXTRA_REPO_ADDRESS = "repoAddress";
+    private static final String TAG = "fdroid.SwapAppListActivity";
+
+    public static String EXTRA_REPO_ID = "repoId";
 
     private Repo repo;
 
@@ -47,8 +52,12 @@ public class SwapAppListActivity extends ActionBarActivity {
     protected void onResume() {
         super.onResume();
 
-        String repoAddress = getIntent().getStringExtra(EXTRA_REPO_ADDRESS);
-        repo = RepoProvider.Helper.findByAddress(this, repoAddress);
+        long repoAddress = getIntent().getLongExtra(EXTRA_REPO_ID, -1);
+        repo = RepoProvider.Helper.findById(this, repoAddress);
+        if (repo == null) {
+            Log.e(TAG, "Couldn't show swap app list for repo " + repoAddress);
+            finish();
+        }
     }
 
     public Repo getRepo() {
@@ -91,14 +100,38 @@ public class SwapAppListActivity extends ActionBarActivity {
             return AppProvider.getRepoUri(repo);
         }
 
+        protected Intent getAppDetailsIntent() {
+            Intent intent = new Intent(getActivity(), SwapAppDetails.class);
+            intent.putExtra(EXTRA_REPO_ID, repo.getId());
+            return intent;
+        }
+
     }
 
     /**
-     * Here so that the AndroidManifest.xml can specify the "parent" activity from this
-     * can be different form the regular AppDetails. That is - the AppDetails goes back
-     * to the main app list, but the SwapAppDetails will go back to the "Swap app list"
-     * activity.
+     * Only difference from base class is that it navigates up to a different task.
+     * It will go to the {@link org.fdroid.fdroid.views.swap.SwapAppListActivity}
+     * whereas the baseclass will go back to the main list of apps. Need to juggle
+     * the repoId in order to be able to return to an appropriately configured swap
+     * list (see {@link org.fdroid.fdroid.views.swap.SwapAppListActivity.SwapAppListFragment#getAppDetailsIntent()}).
      */
-    public static class SwapAppDetails extends AppDetails {}
+    public static class SwapAppDetails extends AppDetails {
+
+        private long repoId;
+
+        @Override
+        protected void onResume() {
+            super.onResume();
+            repoId = getIntent().getLongExtra(EXTRA_REPO_ID, -1);
+        }
+
+        @Override
+        protected void navigateUp() {
+            Intent parentIntent = NavUtils.getParentActivityIntent(this);
+            parentIntent.putExtra(EXTRA_REPO_ID, repoId);
+            NavUtils.navigateUpTo(this, parentIntent);
+        }
+
+    }
 
 }

From a2be7d9013c9bcf696763ae7a0d0011c0f0d7896 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Wed, 1 Apr 2015 15:39:13 +1100
Subject: [PATCH 15/16] Fix update count, breakage from rebase, and broken
 tests.

The update count was broken because I added the join onto the
apk table, and in the process, forced a GROUP BY on the AppProvider
queries. This group by made the COUNT(*) actually count the number
of apks for each app, not the total rows.
---
 F-Droid/src/org/fdroid/fdroid/Utils.java            | 4 +---
 F-Droid/src/org/fdroid/fdroid/data/AppProvider.java | 7 +++++--
 F-Droid/test/src/org/fdroid/fdroid/TestUtils.java   | 7 +++++--
 3 files changed, 11 insertions(+), 7 deletions(-)

diff --git a/F-Droid/src/org/fdroid/fdroid/Utils.java b/F-Droid/src/org/fdroid/fdroid/Utils.java
index 164faf3d3..9b24dfb71 100644
--- a/F-Droid/src/org/fdroid/fdroid/Utils.java
+++ b/F-Droid/src/org/fdroid/fdroid/Utils.java
@@ -61,7 +61,7 @@ import java.util.Locale;
 public final class Utils {
 
     @SuppressWarnings("UnusedDeclaration")
-    private static final String TAG = "org.fdroid.fdroid.Utils";
+    private static final String TAG = "fdroid.Utils";
 
     public static final int BUFFER_SIZE = 4096;
 
@@ -75,8 +75,6 @@ public final class Utils {
     public static final SimpleDateFormat LOG_DATE_FORMAT =
             new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
 
-    private static final String TAG = "fdroid.Utils";
-
     public static String getIconsDir(Context context) {
         DisplayMetrics metrics = context.getResources().getDisplayMetrics();
         String iconsDir;
diff --git a/F-Droid/src/org/fdroid/fdroid/data/AppProvider.java b/F-Droid/src/org/fdroid/fdroid/data/AppProvider.java
index 189220433..69bf31424 100644
--- a/F-Droid/src/org/fdroid/fdroid/data/AppProvider.java
+++ b/F-Droid/src/org/fdroid/fdroid/data/AppProvider.java
@@ -259,6 +259,7 @@ public class AppProvider extends FDroidProvider {
         private boolean isSuggestedApkTableAdded = false;
         private boolean requiresInstalledTable = false;
         private boolean categoryFieldAdded = false;
+        private boolean countFieldAppended = false;
 
         @Override
         protected String getRequiredTables() {
@@ -278,7 +279,8 @@ public class AppProvider extends FDroidProvider {
 
         @Override
         protected String groupBy() {
-            return DBHelper.TABLE_APP + ".id";
+            // If the count field has been requested, then we want to group all rows together.
+            return countFieldAppended ? null : DBHelper.TABLE_APP + ".id";
         }
 
         public void addSelection(AppQuerySelection selection) {
@@ -329,7 +331,8 @@ public class AppProvider extends FDroidProvider {
         }
 
         private void appendCountField() {
-            appendField("COUNT(*) AS " + DataColumns._COUNT);
+            countFieldAppended = true;
+            appendField("COUNT( DISTINCT fdroid_app.id ) AS " + DataColumns._COUNT);
         }
 
         private void addSuggestedApkVersionField() {
diff --git a/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java b/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java
index d33dcc4db..2bb7e89ca 100644
--- a/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java
+++ b/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java
@@ -11,6 +11,9 @@ import mock.MockContextSwappableComponents;
 import mock.MockInstallablePackageManager;
 import org.fdroid.fdroid.data.ApkProvider;
 import org.fdroid.fdroid.data.AppProvider;
+import org.fdroid.fdroid.receiver.PackageAddedReceiver;
+import org.fdroid.fdroid.receiver.PackageRemovedReceiver;
+import org.fdroid.fdroid.receiver.PackageUpgradedReceiver;
 
 import java.io.*;
 import java.util.ArrayList;
@@ -19,7 +22,7 @@ import java.util.List;
 
 public class TestUtils {
 
-    private static final String TAG = "org.fdroid.fdroid.TestUtils";
+    private static final String TAG = "fdroid.TestUtils";
 
     public static <T extends Comparable> void assertContainsOnly(List<T> actualList, T[] expectedArray) {
         List<T> expectedList = new ArrayList<T>(expectedArray.length);
@@ -134,7 +137,7 @@ public class TestUtils {
 
     /**
      * Will tell {@code pm} that we are installing {@code appId}, and then alert the
-     * {@link org.fdroid.fdroid.PackageAddedReceiver}. This will in turn update the
+     * {@link org.fdroid.fdroid.receiver.PackageAddedReceiver}. This will in turn update the
      * "installed apps" table in the database.
      */
     public static void installAndBroadcast(

From e3b73ff49a87fbd4168a586a6971cd603e8de861 Mon Sep 17 00:00:00 2001
From: Peter Serwylo <peter@serwylo.com>
Date: Wed, 1 Apr 2015 16:46:22 +1100
Subject: [PATCH 16/16] Update changelog to specify swap stuff is fixed.

---
 CHANGELOG.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c74bf2eea..bc8ff924d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@
 
 * Handle amazon.com and play.google.com app links
 
+* Misc fixes to the "swap" workflow (especially on Android 2.3 devices)
+
 ### 0.83 (2015-03-26)
 
 * Fix possible crashes when installing or uninstalling apps