diff --git a/core/res/values/strings.xml b/core/res/values/strings.xml index a5dc5f8e..a3014d18 100644 --- a/core/res/values/strings.xml +++ b/core/res/values/strings.xml @@ -86,7 +86,7 @@ New label Setting a label is not supported by your client - Torrent added (refreshing) + %1$s added (refreshing) %1$s removed %1$s removed and data deleted %1$s resumed (refreshing) @@ -103,6 +103,7 @@ Torrent search Search for torrents + The Barcode Scanner could not be found. Would you like to install it from the Android Market? Servers Add new server diff --git a/core/src/org/transdroid/core/app/search/BarcodeHelper.java b/core/src/org/transdroid/core/app/search/BarcodeHelper.java new file mode 100644 index 00000000..e504068d --- /dev/null +++ b/core/src/org/transdroid/core/app/search/BarcodeHelper.java @@ -0,0 +1,71 @@ +package org.transdroid.core.app.search; + +import org.transdroid.core.R; +import org.transdroid.core.gui.TorrentsActivity; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.net.Uri; +import android.support.v4.app.DialogFragment; +import android.text.TextUtils; + +import com.actionbarsherlock.app.SherlockFragmentActivity; + +public class BarcodeHelper { + + public static final int ACTIVITY_BARCODE = 0x0000c0de; // A 'random' ID to identify scan intents + public static final Uri SCANNER_MARKET_URI = Uri.parse("market://search?q=pname:com.google.zxing.client.android"); + + /** + * Call this to start a bar code scanner intent. The calling activity will receive an Intent result with ID + * {@link #ACTIVITY_BARCODE}. From there {@link #handleScanResult(int, Intent)} should be called to parse the result + * into a search query. + * @param activity The calling activity, to which the result is returned or a dialog is bound that asks to install + * the bar code scanner + */ + public static void startBarcodeScanner(final SherlockFragmentActivity activity) { + try { + // Start a bar code scanner that can handle the SCAN intent (specifically ZXing) + activity.startActivityForResult(new Intent("com.google.zxing.client.android.SCAN"), ACTIVITY_BARCODE); + } catch (Exception e) { + // Can't start the bar code scanner, for example with a SecurityException or when ZXing is not present + new DialogFragment() { + public android.app.Dialog onCreateDialog(android.os.Bundle savedInstanceState) { + return new AlertDialog.Builder(activity).setIcon(android.R.drawable.ic_dialog_alert) + .setMessage(activity.getString(R.string.search_barcodescannernotfound)) + .setPositiveButton(android.R.string.yes, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + activity.startActivity(new Intent(Intent.ACTION_VIEW, SCANNER_MARKET_URI)); + } + }).setNegativeButton(android.R.string.no, null).create(); + }; + }.show(activity.getSupportFragmentManager(), "installscanner"); + } + } + + /** + * The activity that called {@link #startBarcodeScanner(SherlockFragmentActivity)} should call this after the scan + * result was returned. This will parse the scan data and return a query search query appropriate to the bar code. + * @param resultCode The raw result code as returned by the bar code scanner + * @param data The raw data as returned from the bar code scanner + * @return A String that can be used as new search query, or null if the bar code could not be scanned or no query + * can be constructed for it + */ + public static String handleScanResult(TorrentsActivity activity, int resultCode, Intent data) { + String contents = data.getStringExtra("SCAN_RESULT"); + String formatName = data.getStringExtra("SCAN_RESULT_FORMAT"); + if (formatName != null && formatName.equals("QR_CODE")) { + // Scanned barcode was a QR code: return the contents directly + return contents; + } else { + if (TextUtils.isEmpty(contents)) + return null; + // Get a meaningful search query based on a Google Search product lookup + return GoogleWebSearchBarcodeResolver.resolveBarcode(contents); + } + } + +} diff --git a/core/src/org/transdroid/core/app/search/GoogleWebSearchBarcodeResolver.java b/core/src/org/transdroid/core/app/search/GoogleWebSearchBarcodeResolver.java new file mode 100644 index 00000000..d681deba --- /dev/null +++ b/core/src/org/transdroid/core/app/search/GoogleWebSearchBarcodeResolver.java @@ -0,0 +1,140 @@ +/* + * This file is part of Transdroid + * + * Transdroid is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Transdroid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Transdroid. If not, see . + * + */ +package org.transdroid.core.app.search; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.transdroid.daemon.util.HttpHelper; + +public class GoogleWebSearchBarcodeResolver { + + public static final String apiUrl = "http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=%s"; + + public static String resolveBarcode(String barcode) { + + try { + // We use the Google AJAX Search API to get a JSON-formatted list of web search results + String callUrl = apiUrl.replace("%s", barcode); + DefaultHttpClient httpclient = new DefaultHttpClient(); + HttpGet httpget = new HttpGet(callUrl); + HttpResponse response = httpclient.execute(httpget); + InputStream instream = response.getEntity().getContent(); + String result = HttpHelper.ConvertStreamToString(instream); + JSONArray results = new JSONObject(result).getJSONObject("responseData").getJSONArray("results"); + + // We will combine and filter multiple results, if there are any + if (results.length() < 1) { + return null; + } + return stripGarbage(results, barcode); + } catch (Exception e) { + return null; + } + + } + + private static String stripGarbage(JSONArray results, String barcode) throws JSONException { + + String good = " abcdefghijklmnopqrstuvwxyz"; + final int MAX_TITLE_CONSIDER = 4; + final int MAX_MISSING = 1; + final int MIN_TITLE_CONSIDER = 2; + + // First gather the titles for the first MAX_TITLE_CONSIDER results + List titles = new ArrayList(); + for (int i = 0; i < results.length() && i < MAX_TITLE_CONSIDER; i++) { + + String title = results.getJSONObject(i).getString("titleNoFormatting"); + + // Make string lowercase first + title = title.toLowerCase(Locale.US); + + // Remove the barcode number if it's there + title = title.replace(barcode, ""); + + // Remove unwanted words and HTML special chars + for (String rem : new String[] { "dvd", "blu-ray", "bluray", "&", """, "'", "<", ">" }) { + title = title.replace(rem, ""); + } + + // Remove all non-alphanumeric (and space) characters + String result = ""; + for ( int j = 0; j < title.length(); j++ ) { + if ( good.indexOf(title.charAt(j)) >= 0 ) + result += title.charAt(j); + } + + // Remove double spaces + while (result.contains(" ")) { + result = result.replace(" ", " "); + } + + titles.add(result); + + } + + // Only retain the words that are missing in at most one of the search result titles + List allWords = new ArrayList(); + for (String title : titles) { + for (String word : Arrays.asList(title.split(" "))) { + if (!allWords.contains(word)) { + allWords.add(word); + } + } + } + List remainingWords = new ArrayList(); + int allowMissing = Math.min(MAX_MISSING, Math.max(titles.size() - MIN_TITLE_CONSIDER, 0)); + for (String word : allWords) { + + int missing = 0; + for (String title : titles) { + if (!title.contains(word)) { + // The word is not contained in this result title + missing++; + if (missing > allowMissing) { + // Already misssing more than once, no need to look further + break; + } + } + } + if (missing <= allowMissing) { + // The word was only missing at most once, so we keep it + remainingWords.add(word); + } + } + + // Now the query is the concatenation of the words remaining; with spaces in between + String query = ""; + for (String word : remainingWords) { + query += " " + word; + } + return query.length() > 0? query.substring(1): null; + + } + +} diff --git a/core/src/org/transdroid/core/gui/TorrentsActivity.java b/core/src/org/transdroid/core/gui/TorrentsActivity.java index 48bfdb31..154ffd29 100644 --- a/core/src/org/transdroid/core/gui/TorrentsActivity.java +++ b/core/src/org/transdroid/core/gui/TorrentsActivity.java @@ -9,12 +9,14 @@ import org.androidannotations.annotations.Bean; import org.androidannotations.annotations.EActivity; import org.androidannotations.annotations.FragmentById; import org.androidannotations.annotations.InstanceState; +import org.androidannotations.annotations.OnActivityResult; import org.androidannotations.annotations.OptionsItem; import org.androidannotations.annotations.OptionsMenu; import org.androidannotations.annotations.SystemService; import org.androidannotations.annotations.UiThread; import org.androidannotations.annotations.ViewById; import org.transdroid.core.R; +import org.transdroid.core.app.search.BarcodeHelper; import org.transdroid.core.app.settings.ApplicationSettings; import org.transdroid.core.app.settings.ServerSetting; import org.transdroid.core.gui.lists.LocalTorrent; @@ -33,6 +35,7 @@ import org.transdroid.core.gui.settings.MainSettingsActivity_; import org.transdroid.daemon.Daemon; import org.transdroid.daemon.IDaemonAdapter; import org.transdroid.daemon.Torrent; +import org.transdroid.daemon.task.AddByUrlTask; import org.transdroid.daemon.task.DaemonTaskFailureResult; import org.transdroid.daemon.task.DaemonTaskResult; import org.transdroid.daemon.task.DaemonTaskSuccessResult; @@ -95,11 +98,9 @@ public class TorrentsActivity extends SherlockFragmentActivity implements OnNavi @InstanceState protected boolean turleModeEnabled = false; - // Torrents list components + // Contained torrent and details fragments @FragmentById(resName = "torrent_list") protected TorrentsFragment fragmentTorrents; - - // Details view components @FragmentById(resName = "torrent_details") protected DetailsFragment fragmentDetails; @@ -322,6 +323,30 @@ public class TorrentsActivity extends SherlockFragmentActivity implements OnNavi // TODO: Handle start intent } + @OptionsItem(resName = "action_add_fromurl") + protected void startUrlEntryDialog() { + // TODO: Open URL input dialog + } + + @OptionsItem(resName = "action_add_fromfile") + protected void startFilePicker() { + // TODO: Start file picker + } + + @OptionsItem(resName = "action_add_frombarcode") + protected void startBarcodeScanner() { + BarcodeHelper.startBarcodeScanner(this); + } + + @OnActivityResult(BarcodeHelper.ACTIVITY_BARCODE) + public void onBarcodeScanned(int resultCode, Intent data) { + String query = BarcodeHelper.handleScanResult(this, resultCode, data); + if (query.startsWith("http")) + addTorrentByUrl(query, "QR code result"); // No torrent title known + else + startSearch(query, false, null, false); + } + @OptionsItem(resName = "action_refresh") protected void refreshScreen() { fragmentTorrents.updateIsLoading(true); @@ -340,6 +365,10 @@ public class TorrentsActivity extends SherlockFragmentActivity implements OnNavi updateTurtleMode(false); } + @OptionsItem(resName = "action_filter") + protected void filterList() { + } + @OptionsItem(resName = "action_settings") protected void openSettings() { MainSettingsActivity_.intent(this).start(); @@ -382,6 +411,17 @@ public class TorrentsActivity extends SherlockFragmentActivity implements OnNavi } } + @Background + public void addTorrentByUrl(String url, String title) { + DaemonTaskResult result = AddByUrlTask.create(currentConnection, url, title).execute(); + if (result instanceof DaemonTaskResult) { + onTaskSucceeded((DaemonTaskSuccessResult) result, R.string.result_added, title); + refreshTorrents(); + } else { + onCommunicationError((DaemonTaskFailureResult) result); + } + } + @Background @Override public void resumeTorrent(Torrent torrent) {