diff --git a/F-Droid/AndroidManifest.xml b/F-Droid/AndroidManifest.xml
index 63c9900de..be3883ec7 100644
--- a/F-Droid/AndroidManifest.xml
+++ b/F-Droid/AndroidManifest.xml
@@ -396,6 +396,17 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".FDroid" />
+
+
+
+ android:paddingTop="2dip">
+ android:paddingTop="2dip">
+
+
+
+
\ No newline at end of file
diff --git a/F-Droid/src/org/fdroid/fdroid/FDroid.java b/F-Droid/src/org/fdroid/fdroid/FDroid.java
index a532dbbc2..a04e3d4ba 100644
--- a/F-Droid/src/org/fdroid/fdroid/FDroid.java
+++ b/F-Droid/src/org/fdroid/fdroid/FDroid.java
@@ -51,6 +51,7 @@ import org.fdroid.fdroid.views.AppListFragmentPagerAdapter;
import org.fdroid.fdroid.views.ManageReposActivity;
import org.fdroid.fdroid.views.swap.ConnectSwapActivity;
import org.fdroid.fdroid.views.swap.SwapActivity;
+import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
public class FDroid extends ActionBarActivity {
@@ -257,7 +258,7 @@ public class FDroid extends ActionBarActivity {
return true;
case R.id.action_swap:
- startActivity(new Intent(this, SwapActivity.class));
+ startActivity(new Intent(this, SwapWorkflowActivity.class));
return true;
case R.id.action_search:
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
new file mode 100644
index 000000000..33f41643a
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
@@ -0,0 +1,185 @@
+package org.fdroid.fdroid.views.swap;
+
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.NonNull;
+import android.support.v4.app.FragmentActivity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.localrepo.LocalRepoManager;
+
+import java.util.Set;
+
+public class SwapWorkflowActivity extends FragmentActivity {
+
+ private ViewGroup container;
+
+ private enum State {
+ INTRO, SELECT_APPS, JOIN_WIFI
+ }
+
+ public interface InnerView {
+ /** @return True if the menu should be shown. */
+ boolean buildMenu(Menu menu, @NonNull MenuInflater inflater);
+ }
+
+ private State currentState = State.INTRO;
+ private InnerView currentView;
+ private boolean hasPreparedLocalRepo = false;
+ private UpdateAsyncTask updateSwappableAppsTask = null;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.swap_activity);
+ container = (ViewGroup) findViewById(R.id.fragment_container);
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ boolean parent = super.onPrepareOptionsMenu(menu);
+ boolean inner = currentView.buildMenu(menu, getMenuInflater());
+ return parent || inner;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ showView();
+ }
+
+ private void showView() {
+ if (currentState == State.INTRO) {
+ showIntro();
+ }
+ }
+
+ private void inflateInnerView(@LayoutRes int viewRes) {
+ container.removeAllViews();
+ View view = ((LayoutInflater)getSystemService(LAYOUT_INFLATER_SERVICE)).inflate(viewRes, container, false);
+ currentView = (InnerView)view;
+ container.addView(view);
+ supportInvalidateOptionsMenu();
+ }
+
+ private void showIntro() {
+ inflateInnerView(R.layout.swap_blank);
+ }
+
+ public void showSelectApps() {
+ inflateInnerView(R.layout.swap_select_apps);
+ }
+
+ // TODO: Pass in the selected apps, then we can figure out whether they have changed.
+ public void onAppsSelected(Set selectedApps) {
+ if (updateSwappableAppsTask == null && !hasPreparedLocalRepo) {
+ updateSwappableAppsTask = new UpdateAsyncTask(this, selectedApps);
+ updateSwappableAppsTask.execute();
+ } else {
+ showJoinWifi();
+ }
+ }
+
+ /**
+ * Once the UpdateAsyncTask has finished preparing our repository index, we can
+ * show the next screen to the user.
+ */
+ private void onLocalRepoPrepared() {
+ updateSwappableAppsTask = null;
+ hasPreparedLocalRepo = true;
+ showJoinWifi();
+ }
+
+ private void showJoinWifi() {
+ inflateInnerView(R.layout.swap_join_wifi);
+ }
+
+ class UpdateAsyncTask extends AsyncTask {
+
+ @SuppressWarnings("UnusedDeclaration")
+ private static final String TAG = "UpdateAsyncTask";
+
+ @NonNull
+ private final ProgressDialog progressDialog;
+
+ @NonNull
+ private final Set selectedApps;
+
+ @NonNull
+ private final Uri sharingUri;
+
+ @NonNull
+ private final Context context;
+
+ public UpdateAsyncTask(Context c, @NonNull Set apps) {
+ context = SwapWorkflowActivity.this.getApplicationContext();
+ selectedApps = apps;
+ progressDialog = new ProgressDialog(c);
+ progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ progressDialog.setTitle(R.string.updating);
+ sharingUri = Utils.getSharingUri(FDroidApp.repo);
+ }
+
+ @Override
+ protected void onPreExecute() {
+ progressDialog.show();
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ final LocalRepoManager lrm = LocalRepoManager.get(context);
+ publishProgress(getString(R.string.deleting_repo));
+ lrm.deleteRepo();
+ for (String app : selectedApps) {
+ publishProgress(String.format(getString(R.string.adding_apks_format), app));
+ lrm.addApp(context, app);
+ }
+ lrm.writeIndexPage(sharingUri.toString());
+ publishProgress(getString(R.string.writing_index_jar));
+ lrm.writeIndexJar();
+ publishProgress(getString(R.string.linking_apks));
+ lrm.copyApksToRepo();
+ publishProgress(getString(R.string.copying_icons));
+ // run the icon copy without progress, its not a blocker
+ new AsyncTask() {
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ lrm.copyIconsToRepo();
+ return null;
+ }
+ }.execute();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ @Override
+ protected void onProgressUpdate(String... progress) {
+ super.onProgressUpdate(progress);
+ progressDialog.setMessage(progress[0]);
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ progressDialog.dismiss();
+ Toast.makeText(context, R.string.updated_local_repo, Toast.LENGTH_SHORT).show();
+ onLocalRepoPrepared();
+ }
+ }
+
+}
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/views/SelectAppsView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/views/SelectAppsView.java
new file mode 100644
index 000000000..7a17788da
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/views/SelectAppsView.java
@@ -0,0 +1,302 @@
+package org.fdroid.fdroid.views.swap.views;
+
+import android.annotation.TargetApi;
+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.Build;
+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.v7.widget.SearchView;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.data.InstalledAppProvider;
+import org.fdroid.fdroid.localrepo.LocalRepoManager;
+import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
+
+import java.util.HashSet;
+
+public class SelectAppsView extends ListView implements
+ SwapWorkflowActivity.InnerView,
+ LoaderManager.LoaderCallbacks,
+ SearchView.OnQueryTextListener {
+
+ public SelectAppsView(Context context) {
+ super(context);
+ }
+
+ public SelectAppsView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SelectAppsView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public SelectAppsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ private SwapWorkflowActivity getActivity() {
+ return (SwapWorkflowActivity)getContext();
+ }
+
+ private static final int LOADER_INSTALLED_APPS = 253341534;
+
+ private AppListAdapter adapter;
+ private String mCurrentFilterString;
+ private final Presenter presenter = new Presenter();
+
+ public static class Presenter {
+
+ private SelectAppsView view;
+
+ public void setView(@NonNull SelectAppsView view) {
+ this.view = view;
+ }
+
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ adapter = new AppListAdapter(this, getContext(),
+ getContext().getContentResolver().query(InstalledAppProvider.getContentUri(), InstalledAppProvider.DataColumns.ALL, null, null, null));
+ setAdapter(adapter);
+ addHeaderView(inflate(getContext(), R.layout.swap_create_header, null), null, false);
+ setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+
+ // either reconnect with an existing loader or start a new one
+ getActivity().getSupportLoaderManager().initLoader(LOADER_INSTALLED_APPS, null, this);
+
+ // build list of existing apps from what is on the file system
+ if (FDroidApp.selectedApps == null) {
+ FDroidApp.selectedApps = new HashSet<>();
+ for (String filename : LocalRepoManager.get(getActivity()).repoDir.list()) {
+ if (filename.matches(".*\\.apk")) {
+ String packageName = filename.substring(0, filename.indexOf("_"));
+ FDroidApp.selectedApps.add(packageName);
+ }
+ }
+ }
+
+ setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ public void onItemClick(AdapterView> parent, View v, int position, long id) {
+ if (position > 0) {
+ // Ignore the headerView at position 0.
+ toggleAppSelected(position);
+ }
+ }
+ });
+
+ presenter.setView(this);
+ }
+
+ @Override
+ public boolean buildMenu(Menu menu, @NonNull MenuInflater inflater) {
+
+ inflater.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);
+ nextMenuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ getActivity().onAppsSelected(FDroidApp.selectedApps);
+ return true;
+ }
+ });
+
+ 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);
+ return true;
+ }
+
+ private void toggleAppSelected(int position) {
+ Cursor c = (Cursor) adapter.getItem(position - 1);
+ String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID));
+ if (FDroidApp.selectedApps.contains(packageName)) {
+ FDroidApp.selectedApps.remove(packageName);
+ } else {
+ FDroidApp.selectedApps.add(packageName);
+ }
+ }
+
+ @Override
+ public CursorLoader onCreateLoader(int id, Bundle args) {
+ Uri uri;
+ if (TextUtils.isEmpty(mCurrentFilterString)) {
+ uri = InstalledAppProvider.getContentUri();
+ } else {
+ uri = InstalledAppProvider.getSearchUri(mCurrentFilterString);
+ }
+ return new CursorLoader(
+ getActivity(),
+ uri,
+ InstalledAppProvider.DataColumns.ALL,
+ null,
+ null,
+ InstalledAppProvider.DataColumns.APPLICATION_LABEL);
+ }
+
+ @Override
+ public void onLoadFinished(Loader loader, Cursor cursor) {
+ adapter.swapCursor(cursor);
+
+ String fdroid = loader.getContext().getPackageName();
+ for (int i = 0; i < getCount() - 1; i++) {
+ Cursor c = ((Cursor) getItemAtPosition(i + 1));
+ String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID));
+ if (TextUtils.equals(packageName, fdroid)) {
+ setItemChecked(i + 1, true); // always include FDroid
+ } else {
+ for (String selected : FDroidApp.selectedApps) {
+ if (TextUtils.equals(packageName, selected)) {
+ setItemChecked(i + 1, true);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader loader) {
+ adapter.swapCursor(null);
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ String newFilter = !TextUtils.isEmpty(newText) ? newText : null;
+ if (mCurrentFilterString == null && newFilter == null) {
+ return true;
+ }
+ if (mCurrentFilterString != null && mCurrentFilterString.equals(newFilter)) {
+ return true;
+ }
+ mCurrentFilterString = newFilter;
+ getActivity().getSupportLoaderManager().restartLoader(LOADER_INSTALLED_APPS, null, this);
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ // this is not needed since we respond to every change in text
+ return true;
+ }
+
+ private class AppListAdapter extends CursorAdapter {
+
+ @SuppressWarnings("UnusedDeclaration")
+ private static final String TAG = "AppListAdapter";
+
+ @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;
+ }
+
+ 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, parent, false);
+ 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);
+
+ 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(listPosition, isChecked);
+ toggleAppSelected(listPosition);
+ }
+ });
+ }
+ }
+ }
+
+}
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/views/StartSwapView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/views/StartSwapView.java
index 97a5e7095..72560f92d 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/views/StartSwapView.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/views/StartSwapView.java
@@ -3,18 +3,18 @@ package org.fdroid.fdroid.views.swap.views;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
+import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
+import android.view.Menu;
+import android.view.MenuInflater;
import android.view.View;
import android.widget.LinearLayout;
-import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
-import org.fdroid.fdroid.views.swap.SwapActivity;
+import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
-import java.util.Set;
-
-public class StartSwapView extends LinearLayout {
+public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.InnerView {
public StartSwapView(Context context) {
super(context);
@@ -36,9 +36,9 @@ public class StartSwapView extends LinearLayout {
- private SwapActivity getActivity() {
+ private SwapWorkflowActivity getActivity() {
// TODO: Try and find a better way to get to the SwapActivity, which makes less asumptions.
- return (SwapActivity)((ContextThemeWrapper)getContext()).getBaseContext();
+ return (SwapWorkflowActivity)getContext();
}
@Override
@@ -54,4 +54,8 @@ public class StartSwapView extends LinearLayout {
}
+ @Override
+ public boolean buildMenu(Menu menu, @NonNull MenuInflater inflater) {
+ return false;
+ }
}