-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/menu/swap_skip.xml b/res/menu/swap_skip.xml
new file mode 100644
index 000000000..8483ff835
--- /dev/null
+++ b/res/menu/swap_skip.xml
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
index d2777848b..5157887e3 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -3,4 +3,16 @@
#ffcccccc
#ffCC0000
#ff999999
+
+ #27aae1
+ #ff98cce1
+ #1c6bbc
+ #ff6ca8d5
+ #ff27aae1
+ #ff2ed7eb
+ #ff21488c
+ #ff3096b9
+ #fbb040
+ #00a14b
+
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 25c1d3af4..3aa91b7ef 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -167,10 +167,10 @@
Local Repo
Local FDroid Repos
Discovering local FDroid repos…
- Your local FDroid repo is accessible.
+ F-Droid is ready to swap
waiting for IP address…
Setup Local Repo
- Touch to setup your local repo.
+ Touch to view details and allow others to swap your apps.
Touch to turn on your local repo.
Touch to turn off your local repo.
Updating…
@@ -192,6 +192,7 @@
To connect to other people\'s devices, make sure both devices are on the same WiFi network. Then either type the URL above into F-Droid, or scan this QR Code:
QR Code
Next
+ Skip
QR Code of repo URL
Scan this QR Code to connect to the same WiFi network as this device.
Scan this QR Code to connect to the website for getting started.
@@ -295,4 +296,16 @@
This option is only available when F-Droid is installed as a system-app.
F-Droid is an installable catalogue of FOSS (Free and Open Source Software) applications for the Android platform. The client makes it easy to browse, install, and keep track of updates on your device.
+ If your friend has F-Droid and NFC turned on touch your phones together.
+ Join the same Wifi as your friend
+ Use Bluetooth instead
+ Learn more about Wifi
+ Swap apps
+ Swap apps
+ No network yet
+ (Tap to open available networks)
+ It\'s not working
+ Open QR Code Scanner
+ Welcome to F-Droid!
+ Do you ant to get apps from %1$s now?
diff --git a/res/values/styles.xml b/res/values/styles.xml
index dfd911baa..1d34a71d8 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -33,4 +33,146 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/com/google/zxing/integration/android/IntentIntegrator.java b/src/com/google/zxing/integration/android/IntentIntegrator.java
new file mode 100644
index 000000000..52ba797c8
--- /dev/null
+++ b/src/com/google/zxing/integration/android/IntentIntegrator.java
@@ -0,0 +1,506 @@
+/*
+ * Copyright 2009 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.integration.android;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A utility class which helps ease integration with Barcode Scanner via {@link Intent}s. This is a simple
+ * way to invoke barcode scanning and receive the result, without any need to integrate, modify, or learn the
+ * project's source code.
+ *
+ * Initiating a barcode scan
+ *
+ * To integrate, create an instance of {@code IntentIntegrator} and call {@link #initiateScan()} and wait
+ * for the result in your app.
+ *
+ * It does require that the Barcode Scanner (or work-alike) application is installed. The
+ * {@link #initiateScan()} method will prompt the user to download the application, if needed.
+ *
+ * There are a few steps to using this integration. First, your {@link Activity} must implement
+ * the method {@link Activity#onActivityResult(int, int, Intent)} and include a line of code like this:
+ *
+ * {@code
+ * public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ * IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
+ * if (scanResult != null) {
+ * // handle scan result
+ * }
+ * // else continue with any other code you need in the method
+ * ...
+ * }
+ * }
+ *
+ * This is where you will handle a scan result.
+ *
+ * Second, just call this in response to a user action somewhere to begin the scan process:
+ *
+ * {@code
+ * IntentIntegrator integrator = new IntentIntegrator(yourActivity);
+ * integrator.initiateScan();
+ * }
+ *
+ * Note that {@link #initiateScan()} returns an {@link AlertDialog} which is non-null if the
+ * user was prompted to download the application. This lets the calling app potentially manage the dialog.
+ * In particular, ideally, the app dismisses the dialog if it's still active in its {@link Activity#onPause()}
+ * method.
+ *
+ * You can use {@link #setTitle(String)} to customize the title of this download prompt dialog (or, use
+ * {@link #setTitleByID(int)} to set the title by string resource ID.) Likewise, the prompt message, and
+ * yes/no button labels can be changed.
+ *
+ * Finally, you can use {@link #addExtra(String, Object)} to add more parameters to the Intent used
+ * to invoke the scanner. This can be used to set additional options not directly exposed by this
+ * simplified API.
+ *
+ * By default, this will only allow applications that are known to respond to this intent correctly
+ * do so. The apps that are allowed to response can be set with {@link #setTargetApplications(List)}.
+ * For example, set to {@link #TARGET_BARCODE_SCANNER_ONLY} to only target the Barcode Scanner app itself.
+ *
+ * Sharing text via barcode
+ *
+ * To share text, encoded as a QR Code on-screen, similarly, see {@link #shareText(CharSequence)}.
+ *
+ * Some code, particularly download integration, was contributed from the Anobiit application.
+ *
+ * Enabling experimental barcode formats
+ *
+ * Some formats are not enabled by default even when scanning with {@link #ALL_CODE_TYPES}, such as
+ * PDF417. Use {@link #initiateScan(java.util.Collection)} with
+ * a collection containing the names of formats to scan for explicitly, like "PDF_417", to use such
+ * formats.
+ *
+ * @author Sean Owen
+ * @author Fred Lin
+ * @author Isaac Potoczny-Jones
+ * @author Brad Drehmer
+ * @author gcstang
+ */
+public class IntentIntegrator {
+
+ public static final int REQUEST_CODE = 0x0000c0de; // Only use bottom 16 bits
+ private static final String TAG = IntentIntegrator.class.getSimpleName();
+
+ public static final String DEFAULT_TITLE = "Install Barcode Scanner?";
+ public static final String DEFAULT_MESSAGE =
+ "This application requires Barcode Scanner. Would you like to install it?";
+ public static final String DEFAULT_YES = "Yes";
+ public static final String DEFAULT_NO = "No";
+
+ private static final String BS_PACKAGE = "com.google.zxing.client.android";
+ private static final String BSPLUS_PACKAGE = "com.srowen.bs.android";
+
+ // supported barcode formats
+ public static final Collection PRODUCT_CODE_TYPES = list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "RSS_14");
+ public static final Collection ONE_D_CODE_TYPES =
+ list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "CODE_39", "CODE_93", "CODE_128",
+ "ITF", "RSS_14", "RSS_EXPANDED");
+ public static final Collection QR_CODE_TYPES = Collections.singleton("QR_CODE");
+ public static final Collection DATA_MATRIX_TYPES = Collections.singleton("DATA_MATRIX");
+
+ public static final Collection ALL_CODE_TYPES = null;
+
+ public static final List TARGET_BARCODE_SCANNER_ONLY = Collections.singletonList(BS_PACKAGE);
+ public static final List TARGET_ALL_KNOWN = list(
+ BSPLUS_PACKAGE, // Barcode Scanner+
+ BSPLUS_PACKAGE + ".simple", // Barcode Scanner+ Simple
+ BS_PACKAGE // Barcode Scanner
+ // What else supports this intent?
+ );
+
+ private final Activity activity;
+ private final Fragment fragment;
+
+ private String title;
+ private String message;
+ private String buttonYes;
+ private String buttonNo;
+ private List targetApplications;
+ private final Map moreExtras = new HashMap(3);
+
+ /**
+ * @param activity {@link Activity} invoking the integration
+ */
+ public IntentIntegrator(Activity activity) {
+ this.activity = activity;
+ this.fragment = null;
+ initializeConfiguration();
+ }
+
+ /**
+ * @param fragment {@link Fragment} invoking the integration.
+ * {@link #startActivityForResult(Intent, int)} will be called on the {@link Fragment} instead
+ * of an {@link Activity}
+ */
+ public IntentIntegrator(Fragment fragment) {
+ this.activity = fragment.getActivity();
+ this.fragment = fragment;
+ initializeConfiguration();
+ }
+
+ private void initializeConfiguration() {
+ title = DEFAULT_TITLE;
+ message = DEFAULT_MESSAGE;
+ buttonYes = DEFAULT_YES;
+ buttonNo = DEFAULT_NO;
+ targetApplications = TARGET_ALL_KNOWN;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public void setTitleByID(int titleID) {
+ title = activity.getString(titleID);
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public void setMessageByID(int messageID) {
+ message = activity.getString(messageID);
+ }
+
+ public String getButtonYes() {
+ return buttonYes;
+ }
+
+ public void setButtonYes(String buttonYes) {
+ this.buttonYes = buttonYes;
+ }
+
+ public void setButtonYesByID(int buttonYesID) {
+ buttonYes = activity.getString(buttonYesID);
+ }
+
+ public String getButtonNo() {
+ return buttonNo;
+ }
+
+ public void setButtonNo(String buttonNo) {
+ this.buttonNo = buttonNo;
+ }
+
+ public void setButtonNoByID(int buttonNoID) {
+ buttonNo = activity.getString(buttonNoID);
+ }
+
+ public Collection getTargetApplications() {
+ return targetApplications;
+ }
+
+ public final void setTargetApplications(List targetApplications) {
+ if (targetApplications.isEmpty()) {
+ throw new IllegalArgumentException("No target applications");
+ }
+ this.targetApplications = targetApplications;
+ }
+
+ public void setSingleTargetApplication(String targetApplication) {
+ this.targetApplications = Collections.singletonList(targetApplication);
+ }
+
+ public Map getMoreExtras() {
+ return moreExtras;
+ }
+
+ public final void addExtra(String key, Object value) {
+ moreExtras.put(key, value);
+ }
+
+ /**
+ * Initiates a scan for all known barcode types with the default camera.
+ *
+ * @return the {@link AlertDialog} that was shown to the user prompting them to download the app
+ * if a prompt was needed, or null otherwise.
+ */
+ public final AlertDialog initiateScan() {
+ return initiateScan(ALL_CODE_TYPES, -1);
+ }
+
+ /**
+ * Initiates a scan for all known barcode types with the specified camera.
+ *
+ * @param cameraId camera ID of the camera to use. A negative value means "no preference".
+ * @return the {@link AlertDialog} that was shown to the user prompting them to download the app
+ * if a prompt was needed, or null otherwise.
+ */
+ public final AlertDialog initiateScan(int cameraId) {
+ return initiateScan(ALL_CODE_TYPES, cameraId);
+ }
+
+ /**
+ * Initiates a scan, using the default camera, only for a certain set of barcode types, given as strings corresponding
+ * to their names in ZXing's {@code BarcodeFormat} class like "UPC_A". You can supply constants
+ * like {@link #PRODUCT_CODE_TYPES} for example.
+ *
+ * @param desiredBarcodeFormats names of {@code BarcodeFormat}s to scan for
+ * @return the {@link AlertDialog} that was shown to the user prompting them to download the app
+ * if a prompt was needed, or null otherwise.
+ */
+ public final AlertDialog initiateScan(Collection desiredBarcodeFormats) {
+ return initiateScan(desiredBarcodeFormats, -1);
+ }
+
+ /**
+ * Initiates a scan, using the specified camera, only for a certain set of barcode types, given as strings corresponding
+ * to their names in ZXing's {@code BarcodeFormat} class like "UPC_A". You can supply constants
+ * like {@link #PRODUCT_CODE_TYPES} for example.
+ *
+ * @param desiredBarcodeFormats names of {@code BarcodeFormat}s to scan for
+ * @param cameraId camera ID of the camera to use. A negative value means "no preference".
+ * @return the {@link AlertDialog} that was shown to the user prompting them to download the app
+ * if a prompt was needed, or null otherwise
+ */
+ public final AlertDialog initiateScan(Collection desiredBarcodeFormats, int cameraId) {
+ Intent intentScan = new Intent(BS_PACKAGE + ".SCAN");
+ intentScan.addCategory(Intent.CATEGORY_DEFAULT);
+
+ // check which types of codes to scan for
+ if (desiredBarcodeFormats != null) {
+ // set the desired barcode types
+ StringBuilder joinedByComma = new StringBuilder();
+ for (String format : desiredBarcodeFormats) {
+ if (joinedByComma.length() > 0) {
+ joinedByComma.append(',');
+ }
+ joinedByComma.append(format);
+ }
+ intentScan.putExtra("SCAN_FORMATS", joinedByComma.toString());
+ }
+
+ // check requested camera ID
+ if (cameraId >= 0) {
+ intentScan.putExtra("SCAN_CAMERA_ID", cameraId);
+ }
+
+ String targetAppPackage = findTargetAppPackage(intentScan);
+ if (targetAppPackage == null) {
+ return showDownloadDialog();
+ }
+ intentScan.setPackage(targetAppPackage);
+ intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ attachMoreExtras(intentScan);
+ startActivityForResult(intentScan, REQUEST_CODE);
+ return null;
+ }
+
+ /**
+ * Start an activity. This method is defined to allow different methods of activity starting for
+ * newer versions of Android and for compatibility library.
+ *
+ * @param intent Intent to start.
+ * @param code Request code for the activity
+ * @see android.app.Activity#startActivityForResult(Intent, int)
+ * @see android.app.Fragment#startActivityForResult(Intent, int)
+ */
+ protected void startActivityForResult(Intent intent, int code) {
+ if (fragment == null) {
+ activity.startActivityForResult(intent, code);
+ } else {
+ fragment.startActivityForResult(intent, code);
+ }
+ }
+
+ private String findTargetAppPackage(Intent intent) {
+ PackageManager pm = activity.getPackageManager();
+ List availableApps = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (availableApps != null) {
+ for (String targetApp : targetApplications) {
+ if (contains(availableApps, targetApp)) {
+ return targetApp;
+ }
+ }
+ }
+ return null;
+ }
+
+ private static boolean contains(Iterable availableApps, String targetApp) {
+ for (ResolveInfo availableApp : availableApps) {
+ String packageName = availableApp.activityInfo.packageName;
+ if (targetApp.equals(packageName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private AlertDialog showDownloadDialog() {
+ AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity);
+ downloadDialog.setTitle(title);
+ downloadDialog.setMessage(message);
+ downloadDialog.setPositiveButton(buttonYes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ String packageName;
+ if (targetApplications.contains(BS_PACKAGE)) {
+ // Prefer to suggest download of BS if it's anywhere in the list
+ packageName = BS_PACKAGE;
+ } else {
+ // Otherwise, first option:
+ packageName = targetApplications.get(0);
+ }
+ Uri uri = Uri.parse("market://details?id=" + packageName);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ try {
+ if (fragment == null) {
+ activity.startActivity(intent);
+ } else {
+ fragment.startActivity(intent);
+ }
+ } catch (ActivityNotFoundException anfe) {
+ // Hmm, market is not installed
+ Log.w(TAG, "Google Play is not installed; cannot install " + packageName);
+ }
+ }
+ });
+ downloadDialog.setNegativeButton(buttonNo, null);
+ downloadDialog.setCancelable(true);
+ return downloadDialog.show();
+ }
+
+
+ /**
+ * Call this from your {@link Activity}'s
+ * {@link Activity#onActivityResult(int, int, Intent)} method.
+ *
+ * @param requestCode request code from {@code onActivityResult()}
+ * @param resultCode result code from {@code onActivityResult()}
+ * @param intent {@link Intent} from {@code onActivityResult()}
+ * @return null if the event handled here was not related to this class, or
+ * else an {@link IntentResult} containing the result of the scan. If the user cancelled scanning,
+ * the fields will be null.
+ */
+ public static IntentResult parseActivityResult(int requestCode, int resultCode, Intent intent) {
+ if (requestCode == REQUEST_CODE) {
+ if (resultCode == Activity.RESULT_OK) {
+ String contents = intent.getStringExtra("SCAN_RESULT");
+ String formatName = intent.getStringExtra("SCAN_RESULT_FORMAT");
+ byte[] rawBytes = intent.getByteArrayExtra("SCAN_RESULT_BYTES");
+ int intentOrientation = intent.getIntExtra("SCAN_RESULT_ORIENTATION", Integer.MIN_VALUE);
+ Integer orientation = intentOrientation == Integer.MIN_VALUE ? null : intentOrientation;
+ String errorCorrectionLevel = intent.getStringExtra("SCAN_RESULT_ERROR_CORRECTION_LEVEL");
+ return new IntentResult(contents,
+ formatName,
+ rawBytes,
+ orientation,
+ errorCorrectionLevel);
+ }
+ return new IntentResult();
+ }
+ return null;
+ }
+
+
+ /**
+ * Defaults to type "TEXT_TYPE".
+ *
+ * @param text the text string to encode as a barcode
+ * @return the {@link AlertDialog} that was shown to the user prompting them to download the app
+ * if a prompt was needed, or null otherwise
+ * @see #shareText(CharSequence, CharSequence)
+ */
+ public final AlertDialog shareText(CharSequence text) {
+ return shareText(text, "TEXT_TYPE");
+ }
+
+ /**
+ * Shares the given text by encoding it as a barcode, such that another user can
+ * scan the text off the screen of the device.
+ *
+ * @param text the text string to encode as a barcode
+ * @param type type of data to encode. See {@code com.google.zxing.client.android.Contents.Type} constants.
+ * @return the {@link AlertDialog} that was shown to the user prompting them to download the app
+ * if a prompt was needed, or null otherwise
+ */
+ public final AlertDialog shareText(CharSequence text, CharSequence type) {
+ Intent intent = new Intent();
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ intent.setAction(BS_PACKAGE + ".ENCODE");
+ intent.putExtra("ENCODE_TYPE", type);
+ intent.putExtra("ENCODE_DATA", text);
+ String targetAppPackage = findTargetAppPackage(intent);
+ if (targetAppPackage == null) {
+ return showDownloadDialog();
+ }
+ intent.setPackage(targetAppPackage);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ attachMoreExtras(intent);
+ if (fragment == null) {
+ activity.startActivity(intent);
+ } else {
+ fragment.startActivity(intent);
+ }
+ return null;
+ }
+
+ private static List list(String... values) {
+ return Collections.unmodifiableList(Arrays.asList(values));
+ }
+
+ private void attachMoreExtras(Intent intent) {
+ for (Map.Entry entry : moreExtras.entrySet()) {
+ String key = entry.getKey();
+ Object value = entry.getValue();
+ // Kind of hacky
+ if (value instanceof Integer) {
+ intent.putExtra(key, (Integer) value);
+ } else if (value instanceof Long) {
+ intent.putExtra(key, (Long) value);
+ } else if (value instanceof Boolean) {
+ intent.putExtra(key, (Boolean) value);
+ } else if (value instanceof Double) {
+ intent.putExtra(key, (Double) value);
+ } else if (value instanceof Float) {
+ intent.putExtra(key, (Float) value);
+ } else if (value instanceof Bundle) {
+ intent.putExtra(key, (Bundle) value);
+ } else {
+ intent.putExtra(key, value.toString());
+ }
+ }
+ }
+
+}
diff --git a/src/com/google/zxing/integration/android/IntentResult.java b/src/com/google/zxing/integration/android/IntentResult.java
new file mode 100644
index 000000000..2469af92c
--- /dev/null
+++ b/src/com/google/zxing/integration/android/IntentResult.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2009 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.integration.android;
+
+/**
+ * Encapsulates the result of a barcode scan invoked through {@link IntentIntegrator}.
+ *
+ * @author Sean Owen
+ */
+public final class IntentResult {
+
+ private final String contents;
+ private final String formatName;
+ private final byte[] rawBytes;
+ private final Integer orientation;
+ private final String errorCorrectionLevel;
+
+ IntentResult() {
+ this(null, null, null, null, null);
+ }
+
+ IntentResult(String contents,
+ String formatName,
+ byte[] rawBytes,
+ Integer orientation,
+ String errorCorrectionLevel) {
+ this.contents = contents;
+ this.formatName = formatName;
+ this.rawBytes = rawBytes;
+ this.orientation = orientation;
+ this.errorCorrectionLevel = errorCorrectionLevel;
+ }
+
+ /**
+ * @return raw content of barcode
+ */
+ public String getContents() {
+ return contents;
+ }
+
+ /**
+ * @return name of format, like "QR_CODE", "UPC_A". See {@code BarcodeFormat} for more format names.
+ */
+ public String getFormatName() {
+ return formatName;
+ }
+
+ /**
+ * @return raw bytes of the barcode content, if applicable, or null otherwise
+ */
+ public byte[] getRawBytes() {
+ return rawBytes;
+ }
+
+ /**
+ * @return rotation of the image, in degrees, which resulted in a successful scan. May be null.
+ */
+ public Integer getOrientation() {
+ return orientation;
+ }
+
+ /**
+ * @return name of the error correction level used in the barcode, if applicable
+ */
+ public String getErrorCorrectionLevel() {
+ return errorCorrectionLevel;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder dialogText = new StringBuilder(100);
+ dialogText.append("Format: ").append(formatName).append('\n');
+ dialogText.append("Contents: ").append(contents).append('\n');
+ int rawBytesLength = rawBytes == null ? 0 : rawBytes.length;
+ dialogText.append("Raw bytes: (").append(rawBytesLength).append(" bytes)\n");
+ dialogText.append("Orientation: ").append(orientation).append('\n');
+ dialogText.append("EC level: ").append(errorCorrectionLevel).append('\n');
+ return dialogText.toString();
+ }
+
+}
diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java
index 76c5c2ee5..c7f4b464a 100644
--- a/src/org/fdroid/fdroid/AppDetails.java
+++ b/src/org/fdroid/fdroid/AppDetails.java
@@ -1327,10 +1327,10 @@ public class AppDetails extends ActionBarActivity implements ProgressListener, A
TextView statusView = (TextView) view.findViewById(R.id.status);
if (getApp().isInstalled()) {
statusView.setText(getString(R.string.details_installed, getApp().installedVersionName));
- NfcBeamManager.setAndroidBeam(getActivity(), getApp().id);
+ NfcHelper.setAndroidBeam(getActivity(), getApp().id);
} else {
statusView.setText(getString(R.string.details_notinstalled));
- NfcBeamManager.disableAndroidBeam(getActivity());
+ NfcHelper.disableAndroidBeam(getActivity());
}
}
diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java
index 18d610e52..d59ed4363 100644
--- a/src/org/fdroid/fdroid/FDroid.java
+++ b/src/org/fdroid/fdroid/FDroid.java
@@ -41,12 +41,15 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
-
+import android.widget.Toast;
import org.fdroid.fdroid.compat.TabManager;
import org.fdroid.fdroid.data.AppProvider;
+import org.fdroid.fdroid.data.NewRepoConfig;
import org.fdroid.fdroid.views.AppListFragmentPagerAdapter;
import org.fdroid.fdroid.views.LocalRepoActivity;
import org.fdroid.fdroid.views.ManageReposActivity;
+import org.fdroid.fdroid.views.swap.ConnectSwapActivity;
+import org.fdroid.fdroid.views.swap.SwapActivity;
public class FDroid extends ActionBarActivity {
@@ -54,9 +57,12 @@ public class FDroid extends ActionBarActivity {
public static final int REQUEST_MANAGEREPOS = 1;
public static final int REQUEST_PREFS = 2;
public static final int REQUEST_ENABLE_BLUETOOTH = 3;
+ public static final int REQUEST_SWAP = 4;
public static final String EXTRA_TAB_UPDATE = "extraTab";
+ public static final String ACTION_ADD_REPO = "org.fdroid.fdroid.FDroid.ACTION_ADD_REPO";
+
private FDroidApp fdroidApp = null;
private ViewPager viewPager;
@@ -106,7 +112,26 @@ public class FDroid extends ActionBarActivity {
protected void onResume() {
super.onResume();
// AppDetails and RepoDetailsActivity set different NFC actions, so reset here
- NfcBeamManager.setAndroidBeam(this, getApplication().getPackageName());
+ NfcHelper.setAndroidBeam(this, getApplication().getPackageName());
+ checkForAddRepoIntent();
+ }
+
+ private void checkForAddRepoIntent() {
+ // Don't handle the intent after coming back to this view (e.g. after hitting the back button)
+ // http://stackoverflow.com/a/14820849
+ if (!getIntent().hasExtra("handled")) {
+ NewRepoConfig parser = new NewRepoConfig(this, getIntent());
+ if (parser.isValidRepo()) {
+ getIntent().putExtra("handled", true);
+ if (parser.isFromSwap()) {
+ startActivityForResult(new Intent(ACTION_ADD_REPO, getIntent().getData(), this, ConnectSwapActivity.class), REQUEST_SWAP);
+ } else {
+ startActivity(new Intent(ACTION_ADD_REPO, getIntent().getData(), this, ManageReposActivity.class));
+ }
+ } else if (parser.getErrorMessage() != null) {
+ Toast.makeText(this, parser.getErrorMessage(), Toast.LENGTH_LONG).show();
+ }
+ }
}
@Override
@@ -145,8 +170,8 @@ public class FDroid extends ActionBarActivity {
startActivityForResult(prefs, REQUEST_PREFS);
return true;
- case R.id.action_local_repo:
- startActivity(new Intent(this, LocalRepoActivity.class));
+ case R.id.action_swap:
+ startActivity(new Intent(this, SwapActivity.class));
return true;
case R.id.action_search:
diff --git a/src/org/fdroid/fdroid/FDroidApp.java b/src/org/fdroid/fdroid/FDroidApp.java
index 249e32d94..e16a5728c 100644
--- a/src/org/fdroid/fdroid/FDroidApp.java
+++ b/src/org/fdroid/fdroid/FDroidApp.java
@@ -41,13 +41,11 @@ import android.os.Messenger;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.widget.Toast;
-
import com.nostra13.universalimageloader.cache.disc.impl.LimitedAgeDiscCache;
import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.nostra13.universalimageloader.utils.StorageUtils;
-
import org.fdroid.fdroid.Preferences.ChangeListener;
import org.fdroid.fdroid.compat.PRNGFixes;
import org.fdroid.fdroid.data.AppProvider;
@@ -57,6 +55,9 @@ import org.fdroid.fdroid.localrepo.LocalRepoService;
import org.fdroid.fdroid.net.IconDownloader;
import org.fdroid.fdroid.net.WifiStateChangeService;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
import java.io.File;
import java.util.Set;
@@ -73,6 +74,8 @@ public class FDroidApp extends Application {
private static Messenger localRepoServiceMessenger = null;
private static boolean localRepoServiceIsBound = false;
+ private static final String TAG = "org.fdroid.fdroid.FDroidApp";
+
BluetoothAdapter bluetoothAdapter = null;
public static enum Theme {
@@ -266,8 +269,7 @@ public class FDroidApp extends Application {
if (!localRepoServiceIsBound) {
Context app = context.getApplicationContext();
Intent service = new Intent(app, LocalRepoService.class);
- localRepoServiceIsBound = app.bindService(service, serviceConnection,
- Context.BIND_AUTO_CREATE);
+ localRepoServiceIsBound = app.bindService(service, serviceConnection, Context.BIND_AUTO_CREATE);
if (localRepoServiceIsBound)
app.startService(service);
}
@@ -285,8 +287,7 @@ public class FDroidApp extends Application {
public static void restartLocalRepoService() {
if (localRepoServiceMessenger != null) {
try {
- Message msg = Message.obtain(null,
- LocalRepoService.RESTART, LocalRepoService.RESTART, 0);
+ Message msg = Message.obtain(null, LocalRepoService.RESTART, LocalRepoService.RESTART, 0);
localRepoServiceMessenger.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
@@ -294,7 +295,7 @@ public class FDroidApp extends Application {
}
}
- public static boolean isLocalRepoServiceRunnig() {
+ public static boolean isLocalRepoServiceRunning() {
return localRepoServiceIsBound;
}
}
diff --git a/src/org/fdroid/fdroid/NfcBeamManager.java b/src/org/fdroid/fdroid/NfcHelper.java
similarity index 57%
rename from src/org/fdroid/fdroid/NfcBeamManager.java
rename to src/org/fdroid/fdroid/NfcHelper.java
index 488fe7d67..b5e7e3f87 100644
--- a/src/org/fdroid/fdroid/NfcBeamManager.java
+++ b/src/org/fdroid/fdroid/NfcHelper.java
@@ -3,21 +3,44 @@ package org.fdroid.fdroid;
import android.annotation.TargetApi;
import android.app.Activity;
+import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.os.Build;
-@TargetApi(16)
-public class NfcBeamManager {
+public class NfcHelper {
+ @TargetApi(14)
+ private static NfcAdapter getAdapter(Context context) {
+ if (Build.VERSION.SDK_INT < 14)
+ return null;
+
+ return NfcAdapter.getDefaultAdapter(context.getApplicationContext());
+ }
+
+ @TargetApi(14)
+ public static boolean setPushMessage(Activity activity, Uri toShare) {
+ NfcAdapter adapter = getAdapter(activity);
+ if (adapter != null) {
+ adapter.setNdefPushMessage(new NdefMessage(new NdefRecord[]{
+ NdefRecord.createUri(toShare),
+ }), activity);
+ return true;
+ }
+ return false;
+ }
+
+ @TargetApi(16)
static void setAndroidBeam(Activity activity, String packageName) {
if (Build.VERSION.SDK_INT < 16)
return;
PackageManager pm = activity.getPackageManager();
- NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(activity);
+ NfcAdapter nfcAdapter = getAdapter(activity);
if (nfcAdapter != null) {
ApplicationInfo appInfo;
try {
@@ -32,10 +55,11 @@ public class NfcBeamManager {
}
}
+ @TargetApi(16)
static void disableAndroidBeam(Activity activity) {
if (Build.VERSION.SDK_INT < 16)
return;
- NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(activity);
+ NfcAdapter nfcAdapter = getAdapter(activity);
if (nfcAdapter != null)
nfcAdapter.setBeamPushUris(null, activity);
}
diff --git a/src/org/fdroid/fdroid/Preferences.java b/src/org/fdroid/fdroid/Preferences.java
index 448db5031..f0a4354a1 100644
--- a/src/org/fdroid/fdroid/Preferences.java
+++ b/src/org/fdroid/fdroid/Preferences.java
@@ -56,6 +56,7 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
public static final String PREF_ENABLE_PROXY = "enableProxy";
public static final String PREF_PROXY_HOST = "proxyHost";
public static final String PREF_PROXY_PORT = "proxyPort";
+ public static final String PREF_SHOW_NFC_DURING_SWAP = "showNfcDuringSwap";
private static final boolean DEFAULT_COMPACT_LAYOUT = false;
private static final boolean DEFAULT_ROOTED = true;
@@ -70,6 +71,7 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
private static final boolean DEFAULT_ENABLE_PROXY = false;
public static final String DEFAULT_PROXY_HOST = "127.0.0.1";
public static final int DEFAULT_PROXY_PORT = 8118;
+ public static final boolean DEFAULT_SHOW_NFC_DURING_SWAP = true;
private boolean compactLayout = DEFAULT_COMPACT_LAYOUT;
private boolean filterAppsRequiringRoot = DEFAULT_ROOTED;
@@ -115,6 +117,14 @@ public class Preferences implements SharedPreferences.OnSharedPreferenceChangeLi
return preferences.getBoolean(PREF_PERMISSIONS, DEFAULT_PERMISSIONS);
}
+ public boolean showNfcDuringSwap() {
+ return preferences.getBoolean(PREF_SHOW_NFC_DURING_SWAP, DEFAULT_SHOW_NFC_DURING_SWAP);
+ }
+
+ public void setShowNfcDuringSwap(boolean show) {
+ preferences.edit().putBoolean(PREF_SHOW_NFC_DURING_SWAP, show).commit();
+ }
+
public boolean expertMode() {
return preferences.getBoolean(PREF_EXPERT, DEFAULT_EXPERT);
}
diff --git a/src/org/fdroid/fdroid/QrGenAsyncTask.java b/src/org/fdroid/fdroid/QrGenAsyncTask.java
index ebfc0fc19..e3a167e1f 100644
--- a/src/org/fdroid/fdroid/QrGenAsyncTask.java
+++ b/src/org/fdroid/fdroid/QrGenAsyncTask.java
@@ -70,6 +70,11 @@ public class QrGenAsyncTask extends AsyncTask {
@Override
protected void onPostExecute(Void v) {
ImageView qrCodeImageView = (ImageView) activity.findViewById(viewId);
- qrCodeImageView.setImageBitmap(qrBitmap);
+
+ // If the generation takes too long for whatever reason, then this view, and indeed the entire
+ // activity may not be around any more.
+ if (qrCodeImageView != null) {
+ qrCodeImageView.setImageBitmap(qrBitmap);
+ }
}
}
diff --git a/src/org/fdroid/fdroid/Utils.java b/src/org/fdroid/fdroid/Utils.java
index f7d77dd24..fd42e4c37 100644
--- a/src/org/fdroid/fdroid/Utils.java
+++ b/src/org/fdroid/fdroid/Utils.java
@@ -28,10 +28,6 @@ import android.text.Html;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ListAdapter;
-import android.widget.ListView;
import com.nostra13.universalimageloader.utils.StorageUtils;
import org.fdroid.fdroid.data.Repo;
import org.xml.sax.XMLReader;
@@ -297,6 +293,7 @@ public final class Utils {
return Uri.parse("http://wifi-not-enabled");
Uri uri = Uri.parse(repo.address.replaceFirst("http", "fdroidrepo"));
Uri.Builder b = uri.buildUpon();
+ b.appendQueryParameter("swap", "1");
if (!TextUtils.isEmpty(repo.fingerprint))
b.appendQueryParameter("fingerprint", repo.fingerprint);
if (!TextUtils.isEmpty(FDroidApp.bssid)) {
diff --git a/src/org/fdroid/fdroid/compat/ContextCompat.java b/src/org/fdroid/fdroid/compat/ContextCompat.java
deleted file mode 100644
index f8e5104dd..000000000
--- a/src/org/fdroid/fdroid/compat/ContextCompat.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package org.fdroid.fdroid.compat;
-
-import java.io.File;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Environment;
-
-public abstract class ContextCompat extends Compatibility {
-
- public static ContextCompat create(Context context) {
- if (hasApi(8)) {
- return new FroyoContextCompatImpl(context);
- } else {
- return new OldContextCompatImpl(context);
- }
- }
-
- protected final Context context;
-
- public ContextCompat(Context context) {
- this.context = context;
- }
-
- /**
- * @see android.content.Context#getExternalCacheDir()
- */
- public abstract File getExternalCacheDir();
-
-}
-
-class OldContextCompatImpl extends ContextCompat {
-
- public OldContextCompatImpl(Context context) {
- super(context);
- }
-
- @Override
- public File getExternalCacheDir() {
- File file = new File(Environment.getExternalStorageDirectory(),
- "Android/data/org.fdroid.fdroid/cache");
- if (!file.exists())
- file.mkdirs();
- return file;
- }
-
-}
-
-@TargetApi(8)
-class FroyoContextCompatImpl extends ContextCompat {
-
- public FroyoContextCompatImpl(Context context) {
- super(context);
- }
-
- @Override
- public File getExternalCacheDir() {
- return context.getExternalCacheDir();
- }
-
-}
diff --git a/src/org/fdroid/fdroid/data/NewRepoConfig.java b/src/org/fdroid/fdroid/data/NewRepoConfig.java
index 080c97b4a..4e0eb7795 100644
--- a/src/org/fdroid/fdroid/data/NewRepoConfig.java
+++ b/src/org/fdroid/fdroid/data/NewRepoConfig.java
@@ -6,7 +6,6 @@ import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
-
import org.fdroid.fdroid.R;
import java.util.Arrays;
@@ -14,8 +13,10 @@ import java.util.Locale;
public class NewRepoConfig {
+ private static final String TAG = "org.fdroid.fdroid.data.NewRepoConfig";
+
private String errorMessage;
- private boolean isValidRepo;
+ private boolean isValidRepo = false;
private String uriString;
private Uri uri;
@@ -25,22 +26,32 @@ public class NewRepoConfig {
private String fingerprint;
private String bssid;
private String ssid;
+ private boolean fromSwap;
+
+ public NewRepoConfig(Context context, String uri) {
+ init(context, uri != null ? Uri.parse(uri) : null);
+ }
public NewRepoConfig(Context context, Intent intent) {
+ init(context, intent.getData());
+ }
+
+ private void init(Context context, Uri incomingUri) {
/* an URL from a click, NFC, QRCode scan, etc */
- uri = intent.getData();
+ uri = incomingUri;
if (uri == null) {
isValidRepo = false;
return;
}
+ Log.d(TAG, "Parsing incoming intent looking for repo: " + incomingUri);
+
// scheme and host should only ever be pure ASCII aka Locale.ENGLISH
- scheme = intent.getScheme();
+ scheme = uri.getScheme();
host = uri.getHost();
port = uri.getPort();
if (TextUtils.isEmpty(scheme) || TextUtils.isEmpty(host)) {
- errorMessage = String.format(context.getString(R.string.malformed_repo_uri),
- uri);
+ errorMessage = String.format(context.getString(R.string.malformed_repo_uri), uri);
isValidRepo = false;
return;
}
@@ -69,13 +80,16 @@ public class NewRepoConfig {
fingerprint = uri.getQueryParameter("fingerprint");
bssid = uri.getQueryParameter("bssid");
ssid = uri.getQueryParameter("ssid");
+ fromSwap = uri.getQueryParameter("swap") != null;
- Log.i("RepoListFragment", "onCreate " + fingerprint);
- if (Arrays.asList("fdroidrepos", "fdroidrepo", "https", "http").contains(scheme)) {
- uriString = sanitizeRepoUri(uri);
+ if (!Arrays.asList("fdroidrepos", "fdroidrepo", "https", "http").contains(scheme)) {
+ isValidRepo = false;
+ return;
}
- this.isValidRepo = true;
+ uriString = sanitizeRepoUri(uri);
+ isValidRepo = true;
+
}
public String getBssid() {
@@ -94,6 +108,10 @@ public class NewRepoConfig {
return uriString;
}
+ public Uri getUri() {
+ return uri;
+ }
+
public String getHost() {
return host;
}
@@ -110,6 +128,10 @@ public class NewRepoConfig {
return isValidRepo;
}
+ public boolean isFromSwap() {
+ return fromSwap;
+ }
+
/*
* The port starts out as 8888, but if there is a conflict, it will be
* incremented until there is a free port found.
diff --git a/src/org/fdroid/fdroid/data/RepoProvider.java b/src/org/fdroid/fdroid/data/RepoProvider.java
index 78c9329db..8b103779c 100644
--- a/src/org/fdroid/fdroid/data/RepoProvider.java
+++ b/src/org/fdroid/fdroid/data/RepoProvider.java
@@ -20,6 +20,12 @@ public class RepoProvider extends FDroidProvider {
private Helper() {}
+ public static Repo findByUri(Context context, Uri uri) {
+ ContentResolver resolver = context.getContentResolver();
+ Cursor cursor = resolver.query(uri, DataColumns.ALL, null, null, null);
+ return cursorToRepo(cursor);
+ }
+
public static Repo findById(Context context, long repoId) {
return findById(context, repoId, DataColumns.ALL);
}
@@ -29,15 +35,7 @@ public class RepoProvider extends FDroidProvider {
ContentResolver resolver = context.getContentResolver();
Uri uri = RepoProvider.getContentUri(repoId);
Cursor cursor = resolver.query(uri, projection, null, null, null);
- Repo repo = null;
- if (cursor != null) {
- if (cursor.getCount() > 0) {
- cursor.moveToFirst();
- repo = new Repo(cursor);
- }
- cursor.close();
- }
- return repo;
+ return cursorToRepo(cursor);
}
public static Repo findByAddress(Context context, String address) {
@@ -90,6 +88,18 @@ public class RepoProvider extends FDroidProvider {
return repos;
}
+ private static Repo cursorToRepo(Cursor cursor) {
+ Repo repo = null;
+ if (cursor != null) {
+ if (cursor.getCount() > 0) {
+ cursor.moveToFirst();
+ repo = new Repo(cursor);
+ }
+ cursor.close();
+ }
+ return repo;
+ }
+
public static void update(Context context, Repo repo,
ContentValues values) {
ContentResolver resolver = context.getContentResolver();
@@ -152,11 +162,11 @@ public class RepoProvider extends FDroidProvider {
* resolver, but I thought I'd put it here in the interests of having
* each of the CRUD methods available in the helper class.
*/
- public static void insert(Context context,
+ public static Uri insert(Context context,
ContentValues values) {
ContentResolver resolver = context.getContentResolver();
Uri uri = RepoProvider.getContentUri();
- resolver.insert(uri, values);
+ return resolver.insert(uri, values);
}
public static void remove(Context context, long repoId) {
diff --git a/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java b/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
index f0ff3fe42..cdd9faa97 100644
--- a/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
+++ b/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java
@@ -28,6 +28,14 @@ import org.fdroid.fdroid.data.App;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
+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;
@@ -50,15 +58,6 @@ import java.util.Map.Entry;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
-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;
-
public class LocalRepoManager {
private static final String TAG = "LocalRepoManager";
@@ -74,6 +73,12 @@ public class LocalRepoManager {
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 apps = new HashMap();
public final File xmlIndex;
@@ -125,7 +130,7 @@ public class LocalRepoManager {
this.uriString = uriString;
}
- private String writeFdroidApkToWebroot(String repoAddress) {
+ private String writeFdroidApkToWebroot() {
ApplicationInfo appInfo;
String fdroidClientURL = "https://f-droid.org/FDroid.apk";
@@ -143,7 +148,7 @@ public class LocalRepoManager {
}
public void writeIndexPage(String repoAddress) {
- final String fdroidClientURL = writeFdroidApkToWebroot(repoAddress);
+ final String fdroidClientURL = writeFdroidApkToWebroot();
try {
File indexHtml = new File(webRoot, "index.html");
BufferedReader in = new BufferedReader(
@@ -159,30 +164,43 @@ public class LocalRepoManager {
}
in.close();
out.close();
+
+ for (String file : WEB_ROOT_ASSET_FILES) {
+ Utils.copy(assetManager.open(file), new FileOutputStream(new File(webRoot, file)));
+ }
+
// make symlinks/copies in each subdir of the repo to make sure that
// the user will always find the bootstrap page.
- File fdroidDirIndex = new File(fdroidDir, "index.html");
- fdroidDirIndex.delete();
- Utils.symlinkOrCopyFile(new File("../index.html"), fdroidDirIndex);
- File repoDirIndex = new File(repoDir, "index.html");
- repoDirIndex.delete();
- Utils.symlinkOrCopyFile(new File("../../index.html"), repoDirIndex);
+ symlinkIndexPageElsewhere("../", fdroidDir);
+ symlinkIndexPageElsewhere("../../", repoDir);
+
// add in /FDROID/REPO to support bad QR Scanner apps
File fdroidCAPS = new File(fdroidDir.getParentFile(), "FDROID");
fdroidCAPS.mkdir();
+
File repoCAPS = new File(fdroidCAPS, "REPO");
repoCAPS.mkdir();
- File fdroidCAPSIndex = new File(fdroidCAPS, "index.html");
- fdroidCAPSIndex.delete();
- Utils.symlinkOrCopyFile(new File("../index.html"), fdroidCAPSIndex);
- File repoCAPSIndex = new File(repoCAPS, "index.html");
- repoCAPSIndex.delete();
- Utils.symlinkOrCopyFile(new File("../../index.html"), repoCAPSIndex);
+
+ symlinkIndexPageElsewhere("../", fdroidCAPS);
+ symlinkIndexPageElsewhere("../../", repoCAPS);
+
} catch (IOException e) {
e.printStackTrace();
}
}
+ private void symlinkIndexPageElsewhere(String symlinkPrefix, File directory) {
+ File index = new File(directory, "index.html");
+ index.delete();
+ Utils.symlinkOrCopyFile(new File(symlinkPrefix + "index.html"), index);
+
+ for(String fileName : WEB_ROOT_ASSET_FILES) {
+ File file = new File(directory, fileName);
+ file.delete();
+ Utils.symlinkOrCopyFile(new File(symlinkPrefix + fileName), file);
+ }
+ }
+
private void deleteContents(File path) {
if (path.exists()) {
for (File file : path.listFiles()) {
@@ -225,7 +243,7 @@ public class LocalRepoManager {
public void addApp(Context context, String packageName) {
App app;
try {
- app = new App(context, pm, packageName);
+ app = new App(context.getApplicationContext(), pm, packageName);
if (!app.isValid())
return;
PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_META_DATA);
diff --git a/src/org/fdroid/fdroid/localrepo/LocalRepoService.java b/src/org/fdroid/fdroid/localrepo/LocalRepoService.java
index d05826b83..a8ed9f59b 100644
--- a/src/org/fdroid/fdroid/localrepo/LocalRepoService.java
+++ b/src/org/fdroid/fdroid/localrepo/LocalRepoService.java
@@ -2,27 +2,38 @@
package org.fdroid.fdroid.localrepo;
import android.annotation.SuppressLint;
-import android.app.*;
-import android.content.*;
-import android.os.*;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
-
-import org.fdroid.fdroid.*;
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Preferences.ChangeListener;
+import org.fdroid.fdroid.R;
import org.fdroid.fdroid.net.LocalHTTPD;
import org.fdroid.fdroid.net.WifiStateChangeService;
-import org.fdroid.fdroid.views.LocalRepoActivity;
+import org.fdroid.fdroid.views.swap.SwapActivity;
+import javax.jmdns.JmDNS;
+import javax.jmdns.ServiceInfo;
import java.io.IOException;
import java.net.BindException;
import java.util.HashMap;
import java.util.Random;
-import javax.jmdns.JmDNS;
-import javax.jmdns.ServiceInfo;
-
public class LocalRepoService extends Service {
private static final String TAG = "LocalRepoService";
@@ -47,25 +58,35 @@ public class LocalRepoService extends Service {
final Messenger messenger = new Messenger(new StartStopHandler(this));
+ /**
+ * This is most likely going to be created on the UI thread, hence all of
+ * the message handling will take place on a new thread to prevent blocking
+ * the UI.
+ */
static class StartStopHandler extends Handler {
- private static LocalRepoService service;
+
+ private final LocalRepoService service;
public StartStopHandler(LocalRepoService service) {
- StartStopHandler.service = service;
+ this.service = service;
}
@Override
- public void handleMessage(Message msg) {
- if (msg.arg1 == START) {
- service.startNetworkServices();
- } else if (msg.arg1 == STOP) {
- service.stopNetworkServices();
- } else if (msg.arg1 == RESTART) {
- service.stopNetworkServices();
- service.startNetworkServices();
- } else {
- Log.e(TAG, "unsupported msg.arg1, ignored");
- }
+ public void handleMessage(final Message msg) {
+ new Thread() {
+ public void run() {
+ if (msg.arg1 == START) {
+ service.startNetworkServices();
+ } else if (msg.arg1 == STOP) {
+ service.stopNetworkServices();
+ } else if (msg.arg1 == RESTART) {
+ service.stopNetworkServices();
+ service.startNetworkServices();
+ } else {
+ Log.e(TAG, "Unsupported msg.arg1 (" + msg.arg1 + "), ignored");
+ }
+ }
+ }.start();
}
}
@@ -105,21 +126,24 @@ public class LocalRepoService extends Service {
}
};
- @Override
- public void onCreate() {
+ private void showNotification() {
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
// launch LocalRepoActivity if the user selects this notification
- Intent intent = new Intent(this, LocalRepoActivity.class);
+ Intent intent = new Intent(this, SwapActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
- PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent,
- PendingIntent.FLAG_CANCEL_CURRENT);
+ PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
notification = new NotificationCompat.Builder(this)
.setContentTitle(getText(R.string.local_repo_running))
.setContentText(getText(R.string.touch_to_configure_local_repo))
- .setSmallIcon(android.R.drawable.ic_dialog_info)
+ .setSmallIcon(R.drawable.ic_swap)
.setContentIntent(contentIntent)
.build();
startForeground(NOTIFICATION, notification);
+ }
+
+ @Override
+ public void onCreate() {
+ showNotification();
startNetworkServices();
Preferences.get().registerLocalRepoBonjourListeners(localRepoBonjourChangeListener);
@@ -136,7 +160,12 @@ public class LocalRepoService extends Service {
@Override
public void onDestroy() {
- stopNetworkServices();
+ new Thread() {
+ public void run() {
+ stopNetworkServices();
+ }
+ }.start();
+
notificationManager.cancel(NOTIFICATION);
LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange);
Preferences.get().unregisterLocalRepoBonjourListeners(localRepoBonjourChangeListener);
@@ -148,6 +177,7 @@ public class LocalRepoService extends Service {
}
private void startNetworkServices() {
+ Log.d(TAG, "Starting local repo network services");
startWebServer();
if (Preferences.get().isLocalRepoBonjourEnabled())
registerMDNSService();
@@ -155,8 +185,13 @@ public class LocalRepoService extends Service {
}
private void stopNetworkServices() {
+ Log.d(TAG, "Stopping local repo network services");
Preferences.get().unregisterLocalRepoHttpsListeners(localRepoHttpsChangeListener);
+
+ Log.d(TAG, "Unregistering MDNS service...");
unregisterMDNSService();
+
+ Log.d(TAG, "Stopping web server...");
stopWebServer();
}
diff --git a/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java b/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java
index 57fdeb373..c6c9a4e48 100644
--- a/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java
+++ b/src/org/fdroid/fdroid/updater/SignedRepoUpdater.java
@@ -15,6 +15,8 @@ import java.util.jar.JarFile;
public class SignedRepoUpdater extends RepoUpdater {
+ private static final String TAG = "org.fdroid.fdroid.updater.SignedRepoUpdater";
+
public SignedRepoUpdater(Context ctx, Repo repo) {
super(ctx, repo);
}
@@ -25,15 +27,22 @@ public class SignedRepoUpdater extends RepoUpdater {
throw new UpdateException(repo, "No signature found in index");
}
- Log.d("FDroid", "Index has " + certs.length + " signature(s)");
+ Log.d(TAG, "Index has " + certs.length + " signature(s)");
boolean match = false;
for (Certificate cert : certs) {
String certdata = Hasher.hex(cert);
- if (repo.pubkey == null && repo.fingerprint.equals(Utils.calcFingerprint(cert))) {
- repo.pubkey = certdata;
- usePubkeyInJar = true;
+ if (repo.pubkey == null && repo.fingerprint != null) {
+ String certFingerprint = Utils.calcFingerprint(cert);
+ Log.d(TAG, "No public key for repo " + repo.address + " yet, but it does have a fingerprint, so comparing them.");
+ Log.d(TAG, "Repo fingerprint: " + repo.fingerprint);
+ Log.d(TAG, "Cert fingerprint: " + certFingerprint);
+ if (repo.fingerprint.equalsIgnoreCase(certFingerprint)) {
+ repo.pubkey = certdata;
+ usePubkeyInJar = true;
+ }
}
if (repo.pubkey != null && repo.pubkey.equals(certdata)) {
+ Log.d(TAG, "Checking repo public key against cert found in jar.");
match = true;
break;
}
@@ -105,7 +114,7 @@ public class SignedRepoUpdater extends RepoUpdater {
protected File getIndexFromFile(File downloadedFile) throws
UpdateException {
Date updateTime = new Date(System.currentTimeMillis());
- Log.d("FDroid", "Getting signed index from " + repo.address + " at " +
+ Log.d(TAG, "Getting signed index from " + repo.address + " at " +
Utils.LOG_DATE_FORMAT.format(updateTime));
File indexJar = downloadedFile;
diff --git a/src/org/fdroid/fdroid/views/LocalRepoActivity.java b/src/org/fdroid/fdroid/views/LocalRepoActivity.java
index 67ef20e07..0005245f3 100644
--- a/src/org/fdroid/fdroid/views/LocalRepoActivity.java
+++ b/src/org/fdroid/fdroid/views/LocalRepoActivity.java
@@ -74,7 +74,7 @@ public class LocalRepoActivity extends ActionBarActivity {
public void onResume() {
super.onResume();
resetNetworkInfo();
- setRepoSwitchChecked(FDroidApp.isLocalRepoServiceRunnig());
+ setRepoSwitchChecked(FDroidApp.isLocalRepoServiceRunning());
LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
new IntentFilter(WifiStateChangeService.BROADCAST));
diff --git a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java
index 6516e106f..426cd25e1 100644
--- a/src/org/fdroid/fdroid/views/RepoDetailsActivity.java
+++ b/src/org/fdroid/fdroid/views/RepoDetailsActivity.java
@@ -5,7 +5,6 @@ import android.annotation.TargetApi;
import android.content.Intent;
import android.net.Uri;
import android.nfc.NdefMessage;
-import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.os.Build;
import android.os.Bundle;
@@ -17,6 +16,7 @@ import android.view.MenuItem;
import android.widget.LinearLayout;
import android.widget.Toast;
import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.NfcHelper;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
@@ -67,27 +67,18 @@ public class RepoDetailsActivity extends ActionBarActivity {
@TargetApi(14)
private void setNfc() {
- if (Build.VERSION.SDK_INT < 14)
- return;
- NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
- if (nfcAdapter == null) {
- return;
+ if (NfcHelper.setPushMessage(this, Utils.getSharingUri(this, repo))) {
+ findViewById(android.R.id.content).post(new Runnable() {
+ @Override
+ public void run() {
+ onNewIntent(getIntent());
+ }
+ });
}
- nfcAdapter.setNdefPushMessage(new NdefMessage(new NdefRecord[] {
- NdefRecord.createUri(Utils.getSharingUri(this, repo)),
- }), this);
- findViewById(android.R.id.content).post(new Runnable() {
- @Override
- public void run() {
- Log.i(TAG, "Runnable.run()");
- onNewIntent(getIntent());
- }
- });
}
@Override
public void onResume() {
- Log.i(TAG, "onResume");
super.onResume();
// FDroid.java and AppDetails set different NFC actions, so reset here
setNfc();
@@ -96,9 +87,6 @@ public class RepoDetailsActivity extends ActionBarActivity {
@Override
public void onNewIntent(Intent i) {
- Log.i(TAG, "onNewIntent");
- Log.i(TAG, "action: " + i.getAction());
- Log.i(TAG, "data: " + i.getData());
// onResume gets called after this to handle the intent
setIntent(i);
}
@@ -108,7 +96,6 @@ public class RepoDetailsActivity extends ActionBarActivity {
if (Build.VERSION.SDK_INT < 9)
return;
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(i.getAction())) {
- Log.i(TAG, "ACTION_NDEF_DISCOVERED");
Parcelable[] rawMsgs =
i.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
NdefMessage msg = (NdefMessage) rawMsgs[0];
diff --git a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java
index c0e1f894a..f0453ee5e 100644
--- a/src/org/fdroid/fdroid/views/fragments/AppListFragment.java
+++ b/src/org/fdroid/fdroid/views/fragments/AppListFragment.java
@@ -6,22 +6,21 @@ import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
-import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
-
-import com.nostra13.universalimageloader.core.ImageLoader;
-import com.nostra13.universalimageloader.core.listener.PauseOnScrollListener;
-import org.fdroid.fdroid.*;
+import org.fdroid.fdroid.AppDetails;
+import org.fdroid.fdroid.FDroid;
+import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.views.AppListAdapter;
-abstract public class AppListFragment extends ListFragment implements
+abstract public class AppListFragment extends ThemeableListFragment implements
AdapterView.OnItemClickListener,
Preferences.ChangeListener,
LoaderManager.LoaderCallbacks {
diff --git a/src/org/fdroid/fdroid/views/fragments/ThemeableListFragment.java b/src/org/fdroid/fdroid/views/fragments/ThemeableListFragment.java
new file mode 100644
index 000000000..0a846ca67
--- /dev/null
+++ b/src/org/fdroid/fdroid/views/fragments/ThemeableListFragment.java
@@ -0,0 +1,65 @@
+package org.fdroid.fdroid.views.fragments;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.ListFragment;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+import org.fdroid.fdroid.R;
+
+public abstract class ThemeableListFragment extends ListFragment {
+
+ protected int getThemeStyle() {
+ return 0;
+ }
+
+ protected int getHeaderLayout() {
+ return 0;
+ }
+
+ protected View getHeaderView(LayoutInflater inflater, ViewGroup container) {
+ if (getHeaderLayout() > 0) {
+ return inflater.inflate(getHeaderLayout(), null, false);
+ } else {
+ return null;
+ }
+ }
+
+ private LayoutInflater getThemedInflater(Context context)
+ {
+ Context c = (getThemeStyle() == 0) ? context : new ContextThemeWrapper(context, getThemeStyle());
+ return (LayoutInflater)c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ /**
+ * Normally we'd just let the baseclass ListFrament.onCreateView() from the support library do its magic.
+ * However, it doesn't allow us to theme it. That is, it always passes getActivity() into the constructor
+ * of widgets. We are more interested in a ContextThemeWrapper, so that the widgets get appropriately
+ * themed. In order to get it working, we need to work around android bug 21742 as well
+ * (https://code.google.com/p/android/issues/detail?id=21742).
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ LayoutInflater themedInflater = getThemedInflater(inflater.getContext());
+
+ View view = themedInflater.inflate(R.layout.list_content, container, false);
+
+ View headerView = getHeaderView(themedInflater, container);
+ if (headerView != null) {
+ ListView listView = (ListView) view.findViewById(android.R.id.list);
+ listView.addHeaderView(headerView);
+ }
+
+ // Workaround for https://code.google.com/p/android/issues/detail?id=21742
+ view.findViewById(android.R.id.empty).setId(0x00ff0001);
+ view.findViewById(R.id.progressContainer).setId(0x00ff0002);
+ view.findViewById(android.R.id.progress).setId(0x00ff0003);
+
+ return view;
+ }
+
+}
diff --git a/src/org/fdroid/fdroid/views/swap/ConfirmReceiveSwapFragment.java b/src/org/fdroid/fdroid/views/swap/ConfirmReceiveSwapFragment.java
new file mode 100644
index 000000000..772ee4747
--- /dev/null
+++ b/src/org/fdroid/fdroid/views/swap/ConfirmReceiveSwapFragment.java
@@ -0,0 +1,107 @@
+package org.fdroid.fdroid.views.swap;
+
+import android.app.Activity;
+import android.content.ContentValues;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import org.fdroid.fdroid.ProgressListener;
+import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.UpdateService;
+import org.fdroid.fdroid.data.NewRepoConfig;
+import org.fdroid.fdroid.data.Repo;
+import org.fdroid.fdroid.data.RepoProvider;
+
+public class ConfirmReceiveSwapFragment extends Fragment implements ProgressListener {
+
+ private NewRepoConfig newRepoConfig;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ View view = inflater.inflate(R.layout.swap_confirm_receive, container, false);
+
+ view.findViewById(R.id.no_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ });
+
+ view.findViewById(R.id.yes_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ confirm();
+ }
+ });
+
+ return view;
+ }
+
+ private void finish() {
+ getActivity().setResult(Activity.RESULT_OK);
+ getActivity().finish();
+ }
+
+ public void onResume() {
+ super.onResume();
+ newRepoConfig = new NewRepoConfig(getActivity(), getActivity().getIntent());
+ if (newRepoConfig.isValidRepo()) {
+ ((TextView) getView().findViewById(R.id.text_description)).setText(
+ getString(R.string.swap_confirm_connect, newRepoConfig.getHost())
+ );
+ } else {
+ // TODO: Show error message on screen (not in popup).
+ }
+ }
+
+ private void confirm() {
+ Repo repo = ensureRepoExists();
+ UpdateService.updateRepoNow(repo.address, getActivity()).setListener(this);
+ }
+
+ 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);
+
+ // 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"...
+ 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);
+ Uri uri = RepoProvider.Helper.insert(getActivity(), values);
+ repo = RepoProvider.Helper.findByUri(getActivity(), uri);
+ }
+ return repo;
+ }
+
+ @Override
+ public void onProgress(Event event) {
+ // TODO: Show progress, but we can worry about that later.
+ // Might be nice to have it nicely embedded in the UI, rather than as
+ // an additional dialog. E.g. White text on blue, letting the user
+ // know what we are up to.
+
+ if (event.type.equals(UpdateService.EVENT_COMPLETE_AND_SAME) ||
+ event.type.equals(UpdateService.EVENT_COMPLETE_WITH_CHANGES)) {
+ ((ConnectSwapActivity)getActivity()).onRepoUpdated();
+ /*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);
+ finish();*/
+ } else if (event.type.equals(UpdateService.EVENT_ERROR)) {
+ // TODO: Show message on this screen (with a big "okay" button that goes back to F-Droid activity)
+ // rather than finishing directly.
+ finish();
+ }
+ }
+}
diff --git a/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java b/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java
new file mode 100644
index 000000000..56231bef5
--- /dev/null
+++ b/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java
@@ -0,0 +1,51 @@
+package org.fdroid.fdroid.views.swap;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+
+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) {
+
+ super.onCreate(savedInstanceState);
+
+ if (savedInstanceState == null) {
+
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(android.R.id.content, new ConfirmReceiveSwapFragment(), STATE_CONFIRM)
+ .addToBackStack(STATE_CONFIRM)
+ .commit();
+
+ }
+
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (currentState().equals(STATE_CONFIRM)) {
+ finish();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ private String currentState() {
+ int index = getSupportFragmentManager().getBackStackEntryCount() - 1;
+ FragmentManager.BackStackEntry lastFragment = getSupportFragmentManager().getBackStackEntryAt(index);
+ return lastFragment.getName();
+ }
+
+ public void onRepoUpdated() {
+
+ Intent intent = new Intent(this, SwapAppListActivity.class);
+ startActivity(intent);
+
+ }
+}
diff --git a/src/org/fdroid/fdroid/views/swap/JoinWifiFragment.java b/src/org/fdroid/fdroid/views/swap/JoinWifiFragment.java
new file mode 100644
index 000000000..f09750f9f
--- /dev/null
+++ b/src/org/fdroid/fdroid/views/swap/JoinWifiFragment.java
@@ -0,0 +1,86 @@
+package org.fdroid.fdroid.views.swap;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.wifi.WifiManager;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.content.LocalBroadcastManager;
+import android.support.v4.view.MenuItemCompat;
+import android.text.TextUtils;
+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.TextView;
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.net.WifiStateChangeService;
+
+public class JoinWifiFragment extends Fragment {
+
+ private BroadcastReceiver onWifiChange = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ refreshWifiState();
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.swap_next, 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);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View joinWifiView = inflater.inflate(R.layout.swap_join_wifi, container, false);
+ joinWifiView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ openAvailableNetworks();
+ }
+ });
+ return joinWifiView;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ // TODO: Listen for "Connecting..." state and reflect that in the view too.
+ LocalBroadcastManager.getInstance(activity).registerReceiver(
+ onWifiChange,
+ new IntentFilter(WifiStateChangeService.BROADCAST));
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ refreshWifiState();
+ }
+
+ private void refreshWifiState() {
+ if (getView() != null) {
+ TextView ssidView = (TextView) getView().findViewById(R.id.wifi_ssid);
+ String text = TextUtils.isEmpty(FDroidApp.ssid) ? getString(R.string.swap_no_wifi_network) : FDroidApp.ssid;
+ ssidView.setText(text);
+ }
+ }
+
+ private void openAvailableNetworks() {
+ startActivity(new Intent(WifiManager.ACTION_PICK_WIFI_NETWORK));
+ }
+}
diff --git a/src/org/fdroid/fdroid/views/swap/NfcSwapFragment.java b/src/org/fdroid/fdroid/views/swap/NfcSwapFragment.java
new file mode 100644
index 000000000..01167d534
--- /dev/null
+++ b/src/org/fdroid/fdroid/views/swap/NfcSwapFragment.java
@@ -0,0 +1,46 @@
+package org.fdroid.fdroid.views.swap;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.view.MenuItemCompat;
+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.CheckBox;
+import android.widget.CompoundButton;
+import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.R;
+
+public class NfcSwapFragment extends Fragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.swap_skip, 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);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.swap_nfc, container, false);
+ CheckBox dontShowAgain = (CheckBox)view.findViewById(R.id.checkbox_dont_show);
+ dontShowAgain.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ Preferences.get().setShowNfcDuringSwap(!isChecked);
+ }
+ });
+ return view;
+ }
+
+}
diff --git a/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java b/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java
new file mode 100644
index 000000000..22d445727
--- /dev/null
+++ b/src/org/fdroid/fdroid/views/swap/SelectAppsFragment.java
@@ -0,0 +1,249 @@
+package org.fdroid.fdroid.views.swap;
+
+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.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.SimpleCursorAdapter;
+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.SearchView;
+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.fragments.ThemeableListFragment;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class SelectAppsFragment extends ThemeableListFragment
+ implements LoaderManager.LoaderCallbacks, SearchView.OnQueryTextListener {
+
+ private PackageManager packageManager;
+ private Drawable defaultAppIcon;
+ private ActionMode mActionMode = null;
+ private String mCurrentFilterString;
+ private Set previouslySelectedApps = new HashSet();
+
+ public Set getSelectedApps() {
+ return FDroidApp.selectedApps;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.swap_next, 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);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ previouslySelectedApps.clear();
+ if (FDroidApp.selectedApps != null) {
+ previouslySelectedApps.addAll(FDroidApp.selectedApps);
+ }
+ }
+
+ public boolean hasSelectionChanged() {
+
+ Set currentlySelected = getSelectedApps();
+ if (currentlySelected.size() != previouslySelectedApps.size()) {
+ return true;
+ }
+
+ for (String current : currentlySelected) {
+ boolean found = false;
+ for (String previous : previouslySelectedApps) {
+ if (current.equals(previous)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ 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);
+ setListShown(false); // start out with a progress indicator
+
+ // either reconnect with an existing loader or start a new one
+ getLoaderManager().initLoader(0, null, this);
+
+ // build list of existing apps from what is on the file system
+ if (FDroidApp.selectedApps == null) {
+ FDroidApp.selectedApps = new HashSet();
+ for (String filename : LocalRepoManager.get(getActivity()).repoDir.list()) {
+ if (filename.matches(".*\\.apk")) {
+ String packageName = filename.substring(0, filename.indexOf("_"));
+ FDroidApp.selectedApps.add(packageName);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ Cursor c = (Cursor) l.getAdapter().getItem(position);
+ 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 baseUri;
+ if (TextUtils.isEmpty(mCurrentFilterString)) {
+ baseUri = InstalledAppProvider.getContentUri();
+ } else {
+ baseUri = InstalledAppProvider.getSearchUri(mCurrentFilterString);
+ }
+ return new CursorLoader(
+ this.getActivity(),
+ baseUri,
+ InstalledAppProvider.DataColumns.ALL,
+ null,
+ null,
+ InstalledAppProvider.DataColumns.APPLICATION_LABEL);
+ }
+
+ @Override
+ public void onLoadFinished(Loader loader, Cursor cursor) {
+ ((SimpleCursorAdapter) this.getListAdapter()).swapCursor(cursor);
+
+ ListView listView = getListView();
+ String fdroid = loader.getContext().getPackageName();
+ for (int i = 0; i < listView.getCount() - 1; i++) {
+ 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
+ } else {
+ for (String selected : FDroidApp.selectedApps) {
+ if (TextUtils.equals(packageName, selected)) {
+ listView.setItemChecked(i + 1, true);
+ }
+ }
+ }
+ }
+
+ if (isResumed()) {
+ setListShown(true);
+ } else {
+ setListShownNoAnimation(true);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader loader) {
+ ((SimpleCursorAdapter) this.getListAdapter()).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;
+ getLoaderManager().restartLoader(0, null, this);
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ // this is not needed since we respond to every change in text
+ return true;
+ }
+
+ public String getCurrentFilterString() {
+ return mCurrentFilterString;
+ }
+
+ @Override
+ protected int getThemeStyle() {
+ return R.style.SwapTheme_StartSwap;
+ }
+
+ @Override
+ protected int getHeaderLayout() {
+ return R.layout.swap_create_header;
+ }
+}
diff --git a/src/org/fdroid/fdroid/views/swap/StartSwapFragment.java b/src/org/fdroid/fdroid/views/swap/StartSwapFragment.java
new file mode 100644
index 000000000..1eb5cb883
--- /dev/null
+++ b/src/org/fdroid/fdroid/views/swap/StartSwapFragment.java
@@ -0,0 +1,38 @@
+package org.fdroid.fdroid.views.swap;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import org.fdroid.fdroid.R;
+
+public class StartSwapFragment extends Fragment {
+
+ private SwapProcessManager manager;
+
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ manager = (SwapProcessManager)activity;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ LayoutInflater themedInflater = (LayoutInflater)new ContextThemeWrapper(inflater.getContext(), R.style.SwapTheme_StartSwap).getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ View view = themedInflater.inflate(R.layout.swap_blank, container, false);
+ view.findViewById(R.id.button_start_swap).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ manager.nextStep();
+ }
+ });
+
+ return view;
+ }
+
+}
diff --git a/src/org/fdroid/fdroid/views/swap/SwapActivity.java b/src/org/fdroid/fdroid/views/swap/SwapActivity.java
new file mode 100644
index 000000000..651d1c466
--- /dev/null
+++ b/src/org/fdroid/fdroid/views/swap/SwapActivity.java
@@ -0,0 +1,263 @@
+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.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v7.app.ActionBarActivity;
+import android.view.MenuItem;
+import android.widget.Toast;
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.NfcHelper;
+import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.localrepo.LocalRepoManager;
+
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class SwapActivity extends ActionBarActivity implements SwapProcessManager {
+
+ private static final String STATE_START_SWAP = "startSwap";
+ private static final String STATE_SELECT_APPS = "selectApps";
+ private static final String STATE_JOIN_WIFI = "joinWifi";
+ private static final String STATE_NFC = "nfc";
+ private static final String STATE_WIFI_QR = "wifiQr";
+
+ private Timer shutdownLocalRepoTimer;
+ private UpdateAsyncTask updateSwappableAppsTask = null;
+ private boolean hasPreparedLocalRepo = false;
+
+ @Override
+ public void onBackPressed() {
+ if (currentState().equals(STATE_START_SWAP)) {
+ finish();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ private String currentState() {
+ FragmentManager.BackStackEntry lastFragment = getSupportFragmentManager().getBackStackEntryAt(getSupportFragmentManager().getBackStackEntryCount() - 1);
+ return lastFragment.getName();
+ }
+
+ public void nextStep() {
+ String current = currentState();
+ if (current.equals(STATE_START_SWAP)) {
+ showSelectApps();
+ } else if (current.equals(STATE_SELECT_APPS)) {
+ prepareLocalRepo();
+ } else if (current.equals(STATE_JOIN_WIFI)) {
+ ensureLocalRepoRunning();
+ if (!attemptToShowNfc()) {
+ showWifiQr();
+ }
+ } else if (current.equals(STATE_NFC)) {
+ showWifiQr();
+ } else if (current.equals(STATE_WIFI_QR)) {
+ }
+ supportInvalidateOptionsMenu();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.action_next) {
+ nextStep();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+
+ if (savedInstanceState == null) {
+
+ showFragment(new StartSwapFragment(), STATE_START_SWAP);
+
+ if (FDroidApp.isLocalRepoServiceRunning()) {
+ showSelectApps();
+ showJoinWifi();
+ attemptToShowNfc();
+ showWifiQr();
+ }
+
+ }
+
+ }
+
+ private void showSelectApps() {
+
+ showFragment(new SelectAppsFragment(), STATE_SELECT_APPS);
+
+ }
+
+ private void showJoinWifi() {
+
+ showFragment(new JoinWifiFragment(), STATE_JOIN_WIFI);
+
+ }
+
+ private boolean attemptToShowNfc() {
+ // TODO: What if NFC is disabled? Hook up with NfcNotEnabledActivity? Or maybe only if they
+ // click a relevant button?
+
+ // 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));
+
+ if (Preferences.get().showNfcDuringSwap() && nfcMessageReady) {
+ showFragment(new NfcSwapFragment(), STATE_NFC);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void showBluetooth() {
+
+ }
+
+ private void showWifiQr() {
+ showFragment(new WifiQrFragment(), STATE_WIFI_QR);
+ }
+
+ private void showFragment(Fragment fragment, String name) {
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(android.R.id.content, fragment, name)
+ .addToBackStack(name)
+ .commit();
+ }
+
+ private void prepareLocalRepo() {
+ SelectAppsFragment fragment = (SelectAppsFragment)getSupportFragmentManager().findFragmentByTag(STATE_SELECT_APPS);
+ boolean needsUpdating = !hasPreparedLocalRepo || fragment.hasSelectionChanged();
+ if (updateSwappableAppsTask == null && needsUpdating) {
+ updateSwappableAppsTask = new UpdateAsyncTask(this, fragment.getSelectedApps());
+ 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 ensureLocalRepoRunning() {
+ if (!FDroidApp.isLocalRepoServiceRunning()) {
+ FDroidApp.startLocalRepoService(this);
+ initLocalRepoTimer(900000); // 15 mins
+ }
+ }
+
+ private void initLocalRepoTimer(long timeoutMilliseconds) {
+
+ // reset the timer if viewing this Activity again
+ if (shutdownLocalRepoTimer != null)
+ shutdownLocalRepoTimer.cancel();
+
+ // automatically turn off after 15 minutes
+ shutdownLocalRepoTimer = new Timer();
+ shutdownLocalRepoTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ FDroidApp.stopLocalRepoService(SwapActivity.this);
+ }
+ }, timeoutMilliseconds);
+
+ }
+
+ @Override
+ public void stopSwapping() {
+ if (FDroidApp.isLocalRepoServiceRunning()) {
+ if (shutdownLocalRepoTimer != null) {
+ shutdownLocalRepoTimer.cancel();
+ }
+ FDroidApp.stopLocalRepoService(SwapActivity.this);
+ }
+ finish();
+ }
+
+ class UpdateAsyncTask extends AsyncTask {
+ private static final String TAG = "UpdateAsyncTask";
+ private ProgressDialog progressDialog;
+ private Set selectedApps;
+ private Uri sharingUri;
+
+ public UpdateAsyncTask(Context c, Set apps) {
+ selectedApps = apps;
+ progressDialog = new ProgressDialog(c);
+ progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ progressDialog.setTitle(R.string.updating);
+ sharingUri = Utils.getSharingUri(c, FDroidApp.repo);
+ }
+
+ @Override
+ protected void onPreExecute() {
+ progressDialog.show();
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ final LocalRepoManager lrm = LocalRepoManager.get(SwapActivity.this);
+ publishProgress(getString(R.string.deleting_repo));
+ lrm.deleteRepo();
+ for (String app : selectedApps) {
+ publishProgress(String.format(getString(R.string.adding_apks_format), app));
+ lrm.addApp(SwapActivity.this, 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(SwapActivity.this, R.string.updated_local_repo, Toast.LENGTH_SHORT).show();
+ onLocalRepoPrepared();
+ }
+ }
+
+}
diff --git a/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java b/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java
new file mode 100644
index 000000000..c852ef2b7
--- /dev/null
+++ b/src/org/fdroid/fdroid/views/swap/SwapAppListActivity.java
@@ -0,0 +1,52 @@
+package org.fdroid.fdroid.views.swap;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.app.ActionBarActivity;
+import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.data.AppProvider;
+import org.fdroid.fdroid.views.AppListAdapter;
+import org.fdroid.fdroid.views.AvailableAppListAdapter;
+import org.fdroid.fdroid.views.fragments.AppListFragment;
+
+public class SwapAppListActivity extends ActionBarActivity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+
+ if (savedInstanceState == null) {
+ getSupportFragmentManager()
+ .beginTransaction()
+ .add(android.R.id.content, new SwapAppListFragment())
+ .commit();
+ }
+
+ }
+
+ private static class SwapAppListFragment extends AppListFragment {
+
+ @Override
+ protected int getHeaderLayout() {
+ return R.layout.swap_success_header;
+ }
+
+ @Override
+ protected AppListAdapter getAppListAdapter() {
+ return new AvailableAppListAdapter(getActivity(), null);
+ }
+
+ @Override
+ protected String getFromTitle() {
+ return getString(R.string.swap);
+ }
+
+ @Override
+ protected Uri getDataUri() {
+ return AppProvider.getCategoryUri("LocalRepo");
+ }
+
+ }
+
+}
diff --git a/src/org/fdroid/fdroid/views/swap/SwapProcessManager.java b/src/org/fdroid/fdroid/views/swap/SwapProcessManager.java
new file mode 100644
index 000000000..0153fc578
--- /dev/null
+++ b/src/org/fdroid/fdroid/views/swap/SwapProcessManager.java
@@ -0,0 +1,6 @@
+package org.fdroid.fdroid.views.swap;
+
+public interface SwapProcessManager {
+ public void nextStep();
+ public void stopSwapping();
+}
diff --git a/src/org/fdroid/fdroid/views/swap/WifiQrFragment.java b/src/org/fdroid/fdroid/views/swap/WifiQrFragment.java
new file mode 100644
index 000000000..4349382aa
--- /dev/null
+++ b/src/org/fdroid/fdroid/views/swap/WifiQrFragment.java
@@ -0,0 +1,158 @@
+package org.fdroid.fdroid.views.swap;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.LightingColorFilter;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.content.LocalBroadcastManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.google.zxing.integration.android.IntentIntegrator;
+import com.google.zxing.integration.android.IntentResult;
+import org.fdroid.fdroid.FDroid;
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.QrGenAsyncTask;
+import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.data.NewRepoConfig;
+import org.fdroid.fdroid.net.WifiStateChangeService;
+
+import java.util.Locale;
+
+public class WifiQrFragment extends Fragment {
+
+ private static final int CONNECT_TO_SWAP = 1;
+
+ private BroadcastReceiver onWifiChange = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent i) {
+ setUIFromWifi();
+ }
+ };
+
+ private SwapProcessManager swapManager;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.swap_wifi_qr, container, false);
+ ImageView qrImage = (ImageView)view.findViewById(R.id.wifi_qr_code);
+
+ // Replace all blacks with the background blue.
+ qrImage.setColorFilter(new LightingColorFilter(0xffffffff, getResources().getColor(R.color.swap_blue)));
+
+ Button openQr = (Button)view.findViewById(R.id.btn_qr_scanner);
+ openQr.setOnClickListener(new Button.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ IntentIntegrator integrator = new IntentIntegrator(WifiQrFragment.this);
+ integrator.initiateScan();
+ }
+ });
+
+ Button cancel = (Button)view.findViewById(R.id.btn_cancel_swap);
+ cancel.setOnClickListener(new Button.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ swapManager.stopSwapping();
+ }
+ });
+ return view;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ swapManager = (SwapProcessManager)activity;
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
+ if (scanResult != null) {
+ if (scanResult.getContents() != null) {
+ NewRepoConfig repoConfig = new NewRepoConfig(getActivity(), scanResult.getContents());
+ if (repoConfig.isValidRepo()) {
+ startActivityForResult(new Intent(FDroid.ACTION_ADD_REPO, Uri.parse(scanResult.getContents()), getActivity(), ConnectSwapActivity.class), CONNECT_TO_SWAP);
+ } else {
+ Toast.makeText(getActivity(), "The QR code you scanned doesn't look like a swap code.", Toast.LENGTH_SHORT).show();
+ }
+ }
+ } else if (requestCode == CONNECT_TO_SWAP && resultCode == Activity.RESULT_OK) {
+ getActivity().finish();
+ }
+ }
+
+ public void onResume() {
+ super.onResume();
+ setUIFromWifi();
+
+ LocalBroadcastManager.getInstance(getActivity()).registerReceiver(onWifiChange,
+ new IntentFilter(WifiStateChangeService.BROADCAST));
+ }
+
+ @TargetApi(14)
+ private void setUIFromWifi() {
+
+ if (TextUtils.isEmpty(FDroidApp.repo.address))
+ return;
+
+ String scheme = Preferences.get().isLocalRepoHttpsEnabled() ? "https://" : "http://";
+
+ // the fingerprint is not useful on the button label
+ String buttonLabel = scheme + FDroidApp.ipAddressString + ":" + FDroidApp.port;
+ TextView ipAddressView = (TextView) getView().findViewById(R.id.device_ip_address);
+ ipAddressView.setText(buttonLabel);
+
+ /*
+ * Set URL to UPPER for compact QR Code, FDroid will translate it back.
+ * Remove the SSID from the query string since SSIDs are case-sensitive.
+ * Instead the receiver will have to rely on the BSSID to find the right
+ * wifi AP to join. Lots of QR Scanners are buggy and do not respect
+ * custom URI schemes, so we have to use http:// or https:// :-(
+ */
+ Uri sharingUri = Utils.getSharingUri(getActivity(), FDroidApp.repo);
+ String qrUriString = ( scheme + sharingUri.getHost() ).toUpperCase(Locale.ENGLISH);
+ if (sharingUri.getPort() != 80) {
+ qrUriString += ":" + sharingUri.getPort();
+ }
+ qrUriString += sharingUri.getPath().toUpperCase(Locale.ENGLISH);
+ boolean first = true;
+ for (String parameterName : sharingUri.getQueryParameterNames()) {
+ if (!parameterName.equals("ssid")) {
+ if (first) {
+ qrUriString += "?";
+ first = false;
+ } else {
+ qrUriString += "&";
+ }
+ qrUriString += parameterName.toUpperCase(Locale.ENGLISH) + "=" +
+ sharingUri.getQueryParameter(parameterName).toUpperCase(Locale.ENGLISH);
+ }
+ }
+
+ Log.i("QRURI", 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.
+ if (Build.VERSION.SDK_INT >= 8)
+ new QrGenAsyncTask(getActivity(), R.id.wifi_qr_code).execute(qrUriString);
+
+ }
+
+}