Browse Source

remove reading mode, search suggestions, and used by them additional modules

master v0.1.2
R4SAS 5 years ago
parent
commit
9424a26e65
  1. 24
      app/build.gradle
  2. 6
      app/proguard-project.txt
  3. 11
      app/src/main/AndroidManifest.xml
  4. 4342
      app/src/main/java/org/purplei2p/lightning/browser/activity/BrowserActivity.java
  5. 10
      app/src/main/java/org/purplei2p/lightning/browser/fragment/BookmarksFragment.java
  6. 2
      app/src/main/java/org/purplei2p/lightning/constant/Constants.java
  7. 3
      app/src/main/java/org/purplei2p/lightning/di/AppComponent.java
  8. 4
      app/src/main/java/org/purplei2p/lightning/dialog/LightningDialogBuilder.java
  9. 38
      app/src/main/java/org/purplei2p/lightning/preference/PreferenceManager.java
  10. 1215
      app/src/main/java/org/purplei2p/lightning/reading/ArticleTextExtractor.java
  11. 246
      app/src/main/java/org/purplei2p/lightning/reading/Converter.java
  12. 483
      app/src/main/java/org/purplei2p/lightning/reading/HtmlFetcher.java
  13. 31
      app/src/main/java/org/purplei2p/lightning/reading/ImageResult.java
  14. 274
      app/src/main/java/org/purplei2p/lightning/reading/JResult.java
  15. 216
      app/src/main/java/org/purplei2p/lightning/reading/OutputFormatter.java
  16. 29
      app/src/main/java/org/purplei2p/lightning/reading/SCache.java
  17. 451
      app/src/main/java/org/purplei2p/lightning/reading/SHelper.java
  18. 342
      app/src/main/java/org/purplei2p/lightning/reading/activity/ReadingActivity.java
  19. 737
      app/src/main/java/org/purplei2p/lightning/search/SuggestionsAdapter.java
  20. 35
      app/src/main/java/org/purplei2p/lightning/search/SuggestionsManager.kt
  21. 51
      app/src/main/java/org/purplei2p/lightning/search/engine/BaseSearchEngine.java
  22. 15
      app/src/main/java/org/purplei2p/lightning/search/engine/BaseSearchEngine.kt
  23. 16
      app/src/main/java/org/purplei2p/lightning/search/engine/CustomSearch.java
  24. 12
      app/src/main/java/org/purplei2p/lightning/search/engine/CustomSearch.kt
  25. 17
      app/src/main/java/org/purplei2p/lightning/search/engine/DuckLiteSearch.java
  26. 15
      app/src/main/java/org/purplei2p/lightning/search/engine/DuckLiteSearch.kt
  27. 17
      app/src/main/java/org/purplei2p/lightning/search/engine/DuckSearch.java
  28. 15
      app/src/main/java/org/purplei2p/lightning/search/engine/DuckSearch.kt
  29. 17
      app/src/main/java/org/purplei2p/lightning/search/engine/LegworkSearch.java
  30. 15
      app/src/main/java/org/purplei2p/lightning/search/engine/LegworkSearch.kt
  31. 165
      app/src/main/java/org/purplei2p/lightning/search/suggestions/BaseSuggestionsModel.java
  32. 52
      app/src/main/java/org/purplei2p/lightning/search/suggestions/DuckSuggestionsModel.java
  33. 51
      app/src/main/java/org/purplei2p/lightning/search/suggestions/LegworkSuggestionsModel.java
  34. 65
      app/src/main/java/org/purplei2p/lightning/settings/fragment/GeneralSettingsFragment.java
  35. 21
      app/src/main/res/layout/bookmark_drawer.xml
  36. 3
      app/src/main/res/menu-large/incognito.xml
  37. 3
      app/src/main/res/menu-large/main.xml
  38. 3
      app/src/main/res/menu-xlarge/incognito.xml
  39. 3
      app/src/main/res/menu-xlarge/main.xml
  40. 4
      app/src/main/res/menu/incognito.xml
  41. 3
      app/src/main/res/menu/main.xml
  42. 5
      app/src/main/res/values-ru/strings.xml
  43. 6
      app/src/main/res/values/arrays.xml
  44. 5
      app/src/main/res/values/strings.xml
  45. 3
      app/src/main/res/xml/preference_general.xml
  46. 6
      build.gradle

24
app/build.gradle

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'com.getkeepsafe.dexcount'
android {
@ -29,6 +28,9 @@ android { @@ -29,6 +28,9 @@ android {
minifyEnabled false
shrinkResources false
proguardFiles 'proguard-project.txt'
applicationVariants.all { variant ->
renameAPK(variant, defaultConfig, 'debug')
}
}
release {
@ -36,6 +38,9 @@ android { @@ -36,6 +38,9 @@ android {
shrinkResources true
signingConfig signingConfigs.r4sas
proguardFiles 'proguard-project.txt'
applicationVariants.all { variant ->
renameAPK(variant, defaultConfig, 'release')
}
}
}
@ -67,9 +72,6 @@ dependencies { @@ -67,9 +72,6 @@ dependencies {
compile "com.android.support:recyclerview-v7:$supportLibVersion"
compile "com.android.support:support-v4:$supportLibVersion"
// html parsing for reading mode
compile 'org.jsoup:jsoup:1.10.2'
// dependency injection
def daggerVersion = '2.11'
compile "com.google.dagger:dagger:$daggerVersion"
@ -84,9 +86,6 @@ dependencies { @@ -84,9 +86,6 @@ dependencies {
// permissions
compile 'com.anthonycr.grant:permissions:1.1.2'
// proxy support
compile 'com.squareup.okhttp3:okhttp:3.8.0'
// tor proxy
def netcipherVersion = '2.0.0-alpha1'
compile "info.guardianproject.netcipher:netcipher:$netcipherVersion"
@ -102,5 +101,14 @@ dependencies { @@ -102,5 +101,14 @@ dependencies {
releaseCompile "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion"
// Kotlin
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
}
def renameAPK(variant, defaultConfig, buildType) {
variant.outputs.each { output ->
def formattedDate = new Date().format('yyMMdd')
def file = output.packageApplication.outputFile
def fileName = defaultConfig.applicationId + "_" + defaultConfig.versionCode + "_" + formattedDate + "_" + buildType + ".apk"
output.packageApplication.outputFile = new File(file.parent, fileName)
}
}

6
app/proguard-project.txt

@ -12,7 +12,6 @@ @@ -12,7 +12,6 @@
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class org.purplei2p.lightning.reading.*
-keepattributes *Annotation*
@ -35,11 +34,6 @@ @@ -35,11 +34,6 @@
@butterknife.* <methods>;
}
# this will fix a force close in ReadingActivity
-keep public class org.jsoup.** {
public *;
}
# Without this rule, openFileChooser does not get called on KitKat
-keep class org.purplei2p.lightning.view.LightningChromeClient {
void openFileChooser(android.webkit.ValueCallback);

11
app/src/main/AndroidManifest.xml

@ -139,17 +139,6 @@ @@ -139,17 +139,6 @@
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name=".reading.activity.ReadingActivity"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboardHidden|keyboard"
android:label="@string/reading_mode"
android:theme="@style/Theme.SettingsTheme">
<intent-filter>
<action android:name="android.intent.action.READING"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<provider
android:name="android.support.v4.content.FileProvider"

4342
app/src/main/java/org/purplei2p/lightning/browser/activity/BrowserActivity.java

File diff suppressed because it is too large Load Diff

10
app/src/main/java/org/purplei2p/lightning/browser/fragment/BookmarksFragment.java

@ -35,7 +35,6 @@ import javax.inject.Inject; @@ -35,7 +35,6 @@ import javax.inject.Inject;
import org.purplei2p.lightning.R;
import org.purplei2p.lightning.browser.bookmark.BookmarkUiModel;
import org.purplei2p.lightning.reading.activity.ReadingActivity;
import org.purplei2p.lightning.browser.TabsManager;
import org.purplei2p.lightning.animation.AnimationUtils;
import org.purplei2p.lightning.BrowserApp;
@ -178,7 +177,6 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener, @@ -178,7 +177,6 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
}
});
setupNavigationButton(view, R.id.action_add_bookmark, R.id.icon_star);
setupNavigationButton(view, R.id.action_reading, R.id.icon_reading);
setupNavigationButton(view, R.id.action_toggle_desktop, R.id.icon_desktop);
mBookmarkAdapter = new BookmarkListAdapter(mFaviconModel, mFolderBitmap, mWebpageBitmap);
@ -341,14 +339,6 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener, @@ -341,14 +339,6 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
case R.id.action_add_bookmark:
mUiController.bookmarkButtonClicked();
break;
case R.id.action_reading:
LightningView currentTab = getTabsManager().getCurrentTab();
if (currentTab != null) {
Intent read = new Intent(getActivity(), ReadingActivity.class);
read.putExtra(Constants.LOAD_READING_URL, currentTab.getUrl());
startActivity(read);
}
break;
case R.id.action_toggle_desktop:
LightningView current = getTabsManager().getCurrentTab();
if (current != null) {

2
app/src/main/java/org/purplei2p/lightning/constant/Constants.java

@ -47,8 +47,6 @@ public final class Constants { @@ -47,8 +47,6 @@ public final class Constants {
" return '';\n" +
"}());";
public static final String LOAD_READING_URL = "ReadingUrl";
// URL Schemes
public static final String HTTP = "http://";
public static final String HTTPS = "https://";

3
app/src/main/java/org/purplei2p/lightning/di/AppComponent.java

@ -3,7 +3,6 @@ package org.purplei2p.lightning.di; @@ -3,7 +3,6 @@ package org.purplei2p.lightning.di;
import javax.inject.Singleton;
import org.purplei2p.lightning.browser.activity.BrowserActivity;
import org.purplei2p.lightning.reading.activity.ReadingActivity;
import org.purplei2p.lightning.browser.TabsManager;
import org.purplei2p.lightning.browser.activity.ThemableBrowserActivity;
import org.purplei2p.lightning.settings.activity.ThemableSettingsActivity;
@ -56,8 +55,6 @@ public interface AppComponent { @@ -56,8 +55,6 @@ public interface AppComponent {
void inject(ProxyUtils proxyUtils);
void inject(ReadingActivity activity);
void inject(LightningWebClient webClient);
void inject(ThemableSettingsActivity activity);

4
app/src/main/java/org/purplei2p/lightning/dialog/LightningDialogBuilder.java

@ -208,10 +208,6 @@ public class LightningDialogBuilder { @@ -208,10 +208,6 @@ public class LightningDialogBuilder {
@Override
public void onItem(@Nullable List<String> folders) {
Preconditions.checkNonNull(folders);
final ArrayAdapter<String> suggestionsAdapter = new ArrayAdapter<>(activity,
android.R.layout.simple_dropdown_item_1line, folders);
getFolder.setThreshold(1);
getFolder.setAdapter(suggestionsAdapter);
editBookmarkDialog.setView(dialogLayout);
editBookmarkDialog.setPositiveButton(activity.getString(R.string.action_ok),
new DialogInterface.OnClickListener() {

38
app/src/main/java/org/purplei2p/lightning/preference/PreferenceManager.java

@ -42,8 +42,6 @@ public class PreferenceManager { @@ -42,8 +42,6 @@ public class PreferenceManager {
static final String BLOCK_THIRD_PARTY = "thirdParty";
static final String ENABLE_COLOR_MODE = "colorMode";
static final String URL_BOX_CONTENTS = "urlContent";
static final String INVERT_COLORS = "invertColors";
static final String READING_TEXT_SIZE = "readingTextSize";
static final String THEME = "Theme";
static final String TEXT_ENCODING = "textEncoding";
static final String CLEAR_WEBSTORAGE_EXIT = "clearWebStorageExit";
@ -51,7 +49,6 @@ public class PreferenceManager { @@ -51,7 +49,6 @@ public class PreferenceManager {
static final String DO_NOT_TRACK = "doNotTrack";
static final String IDENTIFYING_HEADERS = "removeIdentifyingHeaders";
static final String SWAP_BOOKMARKS_AND_TABS = "swapBookmarksAndTabs";
static final String SEARCH_SUGGESTIONS = "searchSuggestions";
static final String BLACK_STATUS_BAR = "blackStatusBar";
static final String USE_PROXY = "useProxy";
@ -62,12 +59,6 @@ public class PreferenceManager { @@ -62,12 +59,6 @@ public class PreferenceManager {
static final String LEAK_CANARY = "leakCanary";
}
public enum Suggestion {
SUGGESTION_LEGWORK,
SUGGESTION_DUCK,
SUGGESTION_NONE
}
@NonNull private final SharedPreferences mPrefs;
private static final String PREFERENCES = "settings";
@ -77,19 +68,6 @@ public class PreferenceManager { @@ -77,19 +68,6 @@ public class PreferenceManager {
mPrefs = context.getSharedPreferences(PREFERENCES, 0);
}
@NonNull
public Suggestion getSearchSuggestionChoice() {
try {
return Suggestion.valueOf(mPrefs.getString(Name.SEARCH_SUGGESTIONS, Suggestion.SUGGESTION_LEGWORK.name()));
} catch (IllegalArgumentException ignored) {
return Suggestion.SUGGESTION_NONE;
}
}
public void setSearchSuggestionChoice(@NonNull Suggestion suggestion) {
putString(Name.SEARCH_SUGGESTIONS, suggestion.name());
}
public boolean getBookmarksAndTabsSwapped() {
return mPrefs.getBoolean(Name.SWAP_BOOKMARKS_AND_TABS, false);
}
@ -152,10 +130,6 @@ public class PreferenceManager { @@ -152,10 +130,6 @@ public class PreferenceManager {
return mPrefs.getBoolean(Name.INCOGNITO_COOKIES, false);
}
public boolean getInvertColors() {
return mPrefs.getBoolean(Name.INVERT_COLORS, false);
}
public boolean getJavaScriptEnabled() {
return mPrefs.getBoolean(Name.JAVASCRIPT, true);
}
@ -177,10 +151,6 @@ public class PreferenceManager { @@ -177,10 +151,6 @@ public class PreferenceManager {
return mPrefs.getInt(Name.USE_PROXY_PORT, 4444);
}
public int getReadingTextSize() {
return mPrefs.getInt(Name.READING_TEXT_SIZE, 2);
}
public int getRenderingMode() {
return mPrefs.getInt(Name.RENDERING_MODE, 0);
}
@ -358,10 +328,6 @@ public class PreferenceManager { @@ -358,10 +328,6 @@ public class PreferenceManager {
putBoolean(Name.INCOGNITO_COOKIES, enable);
}
public void setInvertColors(boolean enable) {
putBoolean(Name.INVERT_COLORS, enable);
}
public void setJavaScriptEnabled(boolean enable) {
putBoolean(Name.JAVASCRIPT, enable);
}
@ -374,10 +340,6 @@ public class PreferenceManager { @@ -374,10 +340,6 @@ public class PreferenceManager {
putBoolean(Name.POPUPS, enable);
}
public void setReadingTextSize(int size) {
putInt(Name.READING_TEXT_SIZE, size);
}
public void setRenderingMode(int mode) {
putInt(Name.RENDERING_MODE, mode);
}

1215
app/src/main/java/org/purplei2p/lightning/reading/ArticleTextExtractor.java

File diff suppressed because it is too large Load Diff

246
app/src/main/java/org/purplei2p/lightning/reading/Converter.java

@ -1,246 +0,0 @@ @@ -1,246 +0,0 @@
/*
* Copyright 2011 Peter Karich
*
* 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 org.purplei2p.lightning.reading;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Locale;
/**
* This class is not thread safe. Use one new instance every time due to
* encoding variable.
*
* @author Peter Karich
*/
public class Converter {
private static final String TAG = "Converter";
private final static String UTF8 = "UTF-8";
private final static String ISO = "ISO-8859-1";
private final static int K2 = 2048;
private int maxBytes = 1000000 / 2;
private String encoding;
private String url;
public Converter(String urlOnlyHint) {
url = urlOnlyHint;
}
public Converter() {
}
public Converter setMaxBytes(int maxBytes) {
this.maxBytes = maxBytes;
return this;
}
public static String extractEncoding(String contentType) {
String[] values;
if (contentType != null)
values = contentType.split(";");
else
values = new String[0];
String charset = "";
for (String value : values) {
value = value.trim().toLowerCase(Locale.getDefault());
if (value.startsWith("charset="))
charset = value.substring("charset=".length());
}
// http1.1 says ISO-8859-1 is the default charset
if (charset.isEmpty())
charset = ISO;
return charset;
}
public String getEncoding() {
if (encoding == null)
return "";
return encoding.toLowerCase(Locale.getDefault());
}
public String streamToString(InputStream is) {
return streamToString(is, maxBytes, encoding);
}
public String streamToString(InputStream is, String enc) {
return streamToString(is, maxBytes, enc);
}
/**
* reads bytes off the string and returns a string
*
* @param is input stream to read
* @param maxBytes
* The max bytes that we want to read from the input stream
* @return String
*/
private String streamToString(InputStream is, int maxBytes, String enc) {
encoding = enc;
// Http 1.1. standard is iso-8859-1 not utf8 :(
// but we force utf-8 as youtube assumes it ;)
if (encoding == null || encoding.isEmpty())
encoding = UTF8;
BufferedInputStream in = null;
try {
in = new BufferedInputStream(is, K2);
ByteArrayOutputStream output = new ByteArrayOutputStream();
// detect encoding with the help of meta tag
try {
in.mark(K2 * 2);
String tmpEnc = detectCharset("charset=", output, in, encoding);
if (tmpEnc != null)
encoding = tmpEnc;
else {
Log.d(TAG, "no charset found in first stage");
// detect with the help of xml beginning ala
// encoding="charset"
tmpEnc = detectCharset("encoding=", output, in, encoding);
if (tmpEnc != null)
encoding = tmpEnc;
else
Log.d(TAG, "no charset found in second stage");
}
if (!Charset.isSupported(encoding))
throw new UnsupportedEncodingException(encoding);
} catch (UnsupportedEncodingException e) {
Log.d(TAG,
"Using default encoding:" + UTF8 + " problem:" + e.getMessage()
+ " encoding:" + encoding + ' ' + url);
encoding = UTF8;
}
// SocketException: Connection reset
// IOException: missing CR => problem on server (probably some xml
// character thing?)
// IOException: Premature EOF => socket unexpectly closed from
// server
int bytesRead = output.size();
byte[] arr = new byte[K2];
while (true) {
if (bytesRead >= maxBytes) {
Log.d(TAG, "Maxbyte of " + maxBytes
+ " exceeded! Maybe html is now broken but try it nevertheless. Url: "
+ url);
break;
}
int n = in.read(arr);
if (n < 0)
break;
bytesRead += n;
output.write(arr, 0, n);
}
return output.toString(encoding);
} catch (IOException e) {
Log.e(TAG, e.toString() + " url:" + url);
} finally {
if (in != null) {
try {
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return "";
}
/**
* This method detects the charset even if the first call only returns some
* bytes. It will read until 4K bytes are reached and then try to determine
* the encoding
*
* @throws IOException
*/
private static String detectCharset(String key, ByteArrayOutputStream bos, BufferedInputStream in,
String enc) throws IOException {
// Grab better encoding from stream
byte[] arr = new byte[K2];
int nSum = 0;
while (nSum < K2) {
int n = in.read(arr);
if (n < 0)
break;
nSum += n;
bos.write(arr, 0, n);
}
String str = bos.toString(enc);
int encIndex = str.indexOf(key);
int clength = key.length();
if (encIndex > 0) {
char startChar = str.charAt(encIndex + clength);
int lastEncIndex;
if (startChar == '\'')
// if we have charset='something'
lastEncIndex = str.indexOf('\'', ++encIndex + clength);
else if (startChar == '\"')
// if we have charset="something"
lastEncIndex = str.indexOf('\"', ++encIndex + clength);
else {
// if we have "text/html; charset=utf-8"
int first = str.indexOf('\"', encIndex + clength);
if (first < 0)
first = Integer.MAX_VALUE;
// or "text/html; charset=utf-8 "
int sec = str.indexOf(' ', encIndex + clength);
if (sec < 0)
sec = Integer.MAX_VALUE;
lastEncIndex = Math.min(first, sec);
// or "text/html; charset=utf-8 '
int third = str.indexOf('\'', encIndex + clength);
if (third > 0)
lastEncIndex = Math.min(lastEncIndex, third);
}
// re-read byte array with different encoding
// assume that the encoding string cannot be greater than 40 chars
if (lastEncIndex > encIndex + clength && lastEncIndex < encIndex + clength + 40) {
String tmpEnc = SHelper.encodingCleanup(str.substring(encIndex + clength,
lastEncIndex));
try {
in.reset();
bos.reset();
return tmpEnc;
} catch (IOException ex) {
Log.e(TAG, "Couldn't reset stream to re-read with new encoding "
+ tmpEnc + ' ' + ex.toString());
}
}
}
return null;
}
}

483
app/src/main/java/org/purplei2p/lightning/reading/HtmlFetcher.java

@ -1,483 +0,0 @@ @@ -1,483 +0,0 @@
/*
* Copyright 2011 Peter Karich
*
* 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 org.purplei2p.lightning.reading;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import org.purplei2p.lightning.utils.Utils;
/**
* Class to fetch articles. This class is thread safe.
*
* @author Peter Karich
*/
public class HtmlFetcher {
private static final Pattern SPACE = Pattern.compile(" ");
static {
SHelper.enableCookieMgmt();
SHelper.enableUserAgentOverwrite();
SHelper.enableAnySSL();
}
public static void main(String[] args) throws Exception {
BufferedReader reader = null;
BufferedWriter writer = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
reader = new BufferedReader(new FileReader("urls.txt"));
String line;
Set<String> existing = new LinkedHashSet<>();
while ((line = reader.readLine()) != null) {
int index1 = line.indexOf('\"');
int index2 = line.indexOf('\"', index1 + 1);
String url = line.substring(index1 + 1, index2);
String domainStr = SHelper.extractDomain(url, true);
String counterStr = "";
// TODO more similarities
if (existing.contains(domainStr))
counterStr = "2";
else
existing.add(domainStr);
String html = new HtmlFetcher().fetchAsString(url, 2000);
String outFile = domainStr + counterStr + ".html";
//noinspection IOResourceOpenedButNotSafelyClosed
writer = new BufferedWriter(new FileWriter(outFile));
writer.write(html);
}
} finally {
Utils.close(reader);
Utils.close(writer);
}
}
private String referrer = "http://jetsli.de/crawler";
private String userAgent = "Mozilla/5.0 (compatible; Jetslide; +" + referrer + ')';
private String cacheControl = "max-age=0";
private String language = "en-us";
private String accept = "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5";
private String charset = "UTF-8";
private SCache cache;
private final AtomicInteger cacheCounter = new AtomicInteger(0);
private int maxTextLength = -1;
private ArticleTextExtractor extractor = new ArticleTextExtractor();
private Set<String> furtherResolveNecessary = new LinkedHashSet<String>() {
{
add("bit.ly");
add("cli.gs");
add("deck.ly");
add("fb.me");
add("feedproxy.google.com");
add("flic.kr");
add("fur.ly");
add("goo.gl");
add("is.gd");
add("ink.co");
add("j.mp");
add("lnkd.in");
add("on.fb.me");
add("ow.ly");
add("plurl.us");
add("sns.mx");
add("snurl.com");
add("su.pr");
add("t.co");
add("tcrn.ch");
add("tl.gd");
add("tiny.cc");
add("tinyurl.com");
add("tmi.me");
add("tr.im");
add("twurl.nl");
}
};
public HtmlFetcher() {
}
public void setExtractor(ArticleTextExtractor extractor) {
this.extractor = extractor;
}
public ArticleTextExtractor getExtractor() {
return extractor;
}
public HtmlFetcher setCache(SCache cache) {
this.cache = cache;
return this;
}
public SCache getCache() {
return cache;
}
public int getCacheCounter() {
return cacheCounter.get();
}
public HtmlFetcher clearCacheCounter() {
cacheCounter.set(0);
return this;
}
public HtmlFetcher setMaxTextLength(int maxTextLength) {
this.maxTextLength = maxTextLength;
return this;
}
public int getMaxTextLength() {
return maxTextLength;
}
public void setAccept(String accept) {
this.accept = accept;
}
public void setCharset(String charset) {
this.charset = charset;
}
public void setCacheControl(String cacheControl) {
this.cacheControl = cacheControl;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public String getReferrer() {
return referrer;
}
public HtmlFetcher setReferrer(String referrer) {
this.referrer = referrer;
return this;
}
public String getUserAgent() {
return userAgent;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
public String getAccept() {
return accept;
}
public String getCacheControl() {
return cacheControl;
}
public String getCharset() {
return charset;
}
public JResult fetchAndExtract(String url, int timeout, boolean resolve) throws Exception {
return fetchAndExtract(url, timeout, resolve, 0, false);
}
// main workhorse to call externally
@SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
private JResult fetchAndExtract(String url, int timeout, boolean resolve,
int maxContentSize, boolean forceReload) throws Exception {
String originalUrl = url;
url = SHelper.removeHashbang(url);
String gUrl = SHelper.getUrlFromUglyGoogleRedirect(url);
if (gUrl != null)
url = gUrl;
else {
gUrl = SHelper.getUrlFromUglyFacebookRedirect(url);
if (gUrl != null)
url = gUrl;
}
if (resolve) {
// check if we can avoid resolving the URL (which hits the website!)
JResult res = getFromCache(url, originalUrl);
if (res != null)
return res;
String resUrl = getResolvedUrl(url, timeout, 0);
if (resUrl.isEmpty()) {
JResult result = new JResult();
if (cache != null)
cache.put(url, result);
return result.setUrl(url);
}
// if resolved url is different then use it!
if (!resUrl.equals(url)) {
// this is necessary e.g. for some homebaken url resolvers which return
// the resolved url relative to url!
url = SHelper.useDomainOfFirstArg4Second(url, resUrl);
}
}
// check if we have the (resolved) URL in cache
JResult res = getFromCache(url, originalUrl);
if (res != null)
return res;
JResult result = new JResult();
// or should we use? <link rel="canonical" href="http://www.N24.de/news/newsitem_6797232.html"/>
result.setUrl(url);
result.setOriginalUrl(originalUrl);
// Immediately put the url into the cache as extracting content takes time.
if (cache != null) {
cache.put(originalUrl, result);
cache.put(url, result);
}
// extract content to the extent appropriate for content type
String lowerUrl = url.toLowerCase();
if (SHelper.isDoc(lowerUrl) || SHelper.isApp(lowerUrl) || SHelper.isPackage(lowerUrl)) {
// skip
} else if (SHelper.isVideo(lowerUrl) || SHelper.isAudio(lowerUrl)) {
result.setVideoUrl(url);
} else if (SHelper.isImage(lowerUrl)) {
result.setImageUrl(url);
} else {
try {
String urlToDownload = url;
if (forceReload) {
urlToDownload = getURLtoBreakCache(url);
}
extractor.extractContent(result, fetchAsString(urlToDownload, timeout), maxContentSize);
} catch (IOException io) {
// do nothing
}
if (result.getFaviconUrl().isEmpty())
result.setFaviconUrl(SHelper.getDefaultFavicon(url));
// some links are relative to root and do not include the domain of the url :(
if (!result.getFaviconUrl().isEmpty())
result.setFaviconUrl(fixUrl(url, result.getFaviconUrl()));
if (!result.getImageUrl().isEmpty())
result.setImageUrl(fixUrl(url, result.getImageUrl()));
if (!result.getVideoUrl().isEmpty())
result.setVideoUrl(fixUrl(url, result.getVideoUrl()));
if (!result.getRssUrl().isEmpty())
result.setRssUrl(fixUrl(url, result.getRssUrl()));
}
result.setText(lessText(result.getText()));
synchronized (result) {
result.notifyAll();
}
return result;
}
// Ugly hack to break free from any cached versions, a few URLs required this.
private static String getURLtoBreakCache(String url) {
try {
URL aURL = new URL(url);
if (aURL.getQuery() != null && aURL.getQuery().isEmpty()) {
return url + "?1";
} else {
return url + "&1";
}
} catch (MalformedURLException e) {
return url;
}
}
private String lessText(String text) {
if (text == null)
return "";
if (maxTextLength >= 0 && text.length() > maxTextLength)
return text.substring(0, maxTextLength);
return text;
}
private static String fixUrl(String url, String urlOrPath) {
return SHelper.useDomainOfFirstArg4Second(url, urlOrPath);
}
private String fetchAsString(String urlAsString, int timeout)
throws IOException {
return fetchAsString(urlAsString, timeout, true);
}
// main routine to get raw webpage content
private String fetchAsString(String urlAsString, int timeout, boolean includeSomeGooseOptions)
throws IOException {
HttpURLConnection hConn = createUrlConnection(urlAsString, timeout, includeSomeGooseOptions);
hConn.setInstanceFollowRedirects(true);
String encoding = hConn.getContentEncoding();
InputStream is;
if ("gzip".equalsIgnoreCase(encoding)) {
is = new GZIPInputStream(hConn.getInputStream());
} else if ("deflate".equalsIgnoreCase(encoding)) {
is = new InflaterInputStream(hConn.getInputStream(), new Inflater(true));
} else {
is = hConn.getInputStream();
}
String enc = Converter.extractEncoding(hConn.getContentType());
return createConverter(urlAsString).streamToString(is, enc);
}
private static Converter createConverter(String url) {
return new Converter(url);
}
/**
* On some devices we have to hack:
* http://developers.sun.com/mobility/reference/techart/design_guidelines/http_redirection.html
*
* @param timeout Sets a specified timeout value, in milliseconds
* @return the resolved url if any. Or null if it couldn't resolve the url
* (within the specified time) or the same url if response code is OK
*/
private String getResolvedUrl(String urlAsString, int timeout,
int num_redirects) {
String newUrl;
int responseCode;
try {
HttpURLConnection hConn = createUrlConnection(urlAsString, timeout, true);
// force no follow
hConn.setInstanceFollowRedirects(false);
// the program doesn't care what the content actually is !!
// http://java.sun.com/developer/JDCTechTips/2003/tt0422.html
hConn.setRequestMethod("HEAD");
hConn.connect();
responseCode = hConn.getResponseCode();
hConn.getInputStream().close();
if (responseCode == HttpURLConnection.HTTP_OK)
return urlAsString;
newUrl = hConn.getHeaderField("Location");
// Note that the max recursion level is 5.
if (responseCode / 100 == 3 && newUrl != null && num_redirects < 5) {
newUrl = SPACE.matcher(newUrl).replaceAll("+");
// some services use (none-standard) utf8 in their location header
if (urlAsString.contains("://bit.ly")
|| urlAsString.contains("://is.gd"))
newUrl = encodeUriFromHeader(newUrl);
// AP: This code is not longer need, instead we always follow
// multiple redirects.
//
// fix problems if shortened twice. as it is often the case after twitters' t.co bullshit
//if (furtherResolveNecessary.contains(SHelper.extractDomain(newUrl, true)))
// newUrl = getResolvedUrl(newUrl, timeout);
// Add support for URLs with multiple levels of redirection,
// call getResolvedUrl until there is no more redirects or a
// max number of redirects is reached.
newUrl = SHelper.useDomainOfFirstArg4Second(urlAsString, newUrl);
newUrl = getResolvedUrl(newUrl, timeout, num_redirects + 1);
return newUrl;
} else
return urlAsString;
} catch (Exception ex) {
return "";
}
}
/**
* Takes a URI that was decoded as ISO-8859-1 and applies percent-encoding
* to non-ASCII characters. Workaround for broken origin servers that send
* UTF-8 in the Location: header.
*/
private static String encodeUriFromHeader(String badLocation) {
StringBuilder sb = new StringBuilder(badLocation.length());
for (char ch : badLocation.toCharArray()) {
if (ch < (char) 128) {
sb.append(ch);
} else {
// this is ONLY valid if the uri was decoded using ISO-8859-1
sb.append(String.format("%%%02X", (int) ch));
}
}
return sb.toString();
}
private HttpURLConnection createUrlConnection(String urlAsStr, int timeout,
boolean includeSomeGooseOptions) throws IOException {
URL url = new URL(urlAsStr);
//using proxy may increase latency
HttpURLConnection hConn = (HttpURLConnection) url.openConnection(Proxy.NO_PROXY);
hConn.setRequestProperty("User-Agent", userAgent);
hConn.setRequestProperty("Accept", accept);
if (includeSomeGooseOptions) {
hConn.setRequestProperty("Accept-Language", language);
hConn.setRequestProperty("content-charset", charset);
hConn.addRequestProperty("Referer", referrer);
// avoid the cache for testing purposes only?
hConn.setRequestProperty("Cache-Control", cacheControl);
}
// suggest respond to be gzipped or deflated (which is just another compression)
// http://stackoverflow.com/q/3932117
hConn.setRequestProperty("Accept-Encoding", "gzip, deflate");
hConn.setConnectTimeout(timeout);
hConn.setReadTimeout(timeout);
return hConn;
}
private JResult getFromCache(String url, String originalUrl) {
if (cache != null) {
JResult res = cache.get(url);
if (res != null) {
// e.g. the cache returned a shortened url as original url now we want to store the
// current original url! Also it can be that the cache response to url but the JResult
// does not contain it so overwrite it:
res.setUrl(url);
res.setOriginalUrl(originalUrl);
cacheCounter.addAndGet(1);
return res;
}
}
return null;
}
}

31
app/src/main/java/org/purplei2p/lightning/reading/ImageResult.java

@ -1,31 +0,0 @@ @@ -1,31 +0,0 @@
package org.purplei2p.lightning.reading;
import org.jsoup.nodes.Element;
/**
* Class which encapsulates the data from an image found under an element
*
* @author Chris Alexander, chris@chris-alexander.co.uk
*/
class ImageResult {
private final String src;
public final Integer weight;
private final String title;
private final int height;
private final int width;
private final String alt;
private final boolean noFollow;
public Element element;
public ImageResult(String src, Integer weight, String title, int height, int width, String alt,
boolean noFollow) {
this.src = src;
this.weight = weight;
this.title = title;
this.height = height;
this.width = width;
this.alt = alt;
this.noFollow = noFollow;
}
}

274
app/src/main/java/org/purplei2p/lightning/reading/JResult.java

@ -1,274 +0,0 @@ @@ -1,274 +0,0 @@
/*
* Copyright 2011 Peter Karich
*
* 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 org.purplei2p.lightning.reading;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* Parsed result from web page containing important title, text and image.
*
* @author Peter Karich
*/
public class JResult implements Serializable {
private String title;
private String url;
private String originalUrl;
private String canonicalUrl;
private String imageUrl;
private String videoUrl;
private String rssUrl;
private String text;
private String faviconUrl;
private String description;
private String authorName;
private String authorDescription;
private Date date;
private Collection<String> keywords;
private List<ImageResult> images = null;
private final List<Map<String, String>> links = new ArrayList<>();
private String type;
private String sitename;
private String language;
public JResult() {
}
public String getUrl() {
if (url == null)
return "";
return url;
}
public JResult setUrl(String url) {
this.url = url;
return this;
}
public JResult setOriginalUrl(String originalUrl) {
this.originalUrl = originalUrl;
return this;
}
public String getOriginalUrl() {
return originalUrl;
}
public JResult setCanonicalUrl(String canonicalUrl) {
this.canonicalUrl = canonicalUrl;
return this;
}
public String getCanonicalUrl() {
return canonicalUrl;
}
public String getFaviconUrl() {
if (faviconUrl == null)
return "";
return faviconUrl;
}
public JResult setFaviconUrl(String faviconUrl) {
this.faviconUrl = faviconUrl;
return this;
}
public JResult setRssUrl(String rssUrl) {
this.rssUrl = rssUrl;
return this;
}
public String getRssUrl() {
if (rssUrl == null)
return "";
return rssUrl;
}
public String getDescription() {
if (description == null)
return "";
return description;
}
public JResult setDescription(String description) {
this.description = description;
return this;
}
public String getAuthorName() {
if (authorName == null)
return "";
return authorName;
}
public JResult setAuthorName(String authorName) {
this.authorName = authorName;
return this;
}
public String getAuthorDescription() {
if (authorDescription == null)
return "";
return authorDescription;
}
public JResult setAuthorDescription(String authorDescription) {
this.authorDescription = authorDescription;
return this;
}
public String getImageUrl() {
if (imageUrl == null)
return "";
return imageUrl;
}
public JResult setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
return this;
}
public String getText() {
if (text == null)
return "";
return text;
}
public JResult setText(String text) {
this.text = text;
return this;
}
public String getTitle() {
if (title == null)
return "";
return title;
}
public JResult setTitle(String title) {
this.title = title;
return this;
}
public String getVideoUrl() {
if (videoUrl == null)
return "";
return videoUrl;
}
public JResult setVideoUrl(String videoUrl) {
this.videoUrl = videoUrl;
return this;
}
public JResult setDate(Date date) {
this.date = date;
return this;
}
public Collection<String> getKeywords() {
return keywords;
}
public void setKeywords(Collection<String> keywords) {
this.keywords = keywords;
}
/**
* @return get date from url or guessed from text
*/
public Date getDate() {
return date;
}
/**
* @return images list
*/
public List<ImageResult> getImages() {
if (images == null)
return Collections.emptyList();
return images;
}
/**
* @return images count
*/
public int getImagesCount() {
if (images == null)
return 0;
return images.size();
}
/**
* set images list
*/
public void setImages(List<ImageResult> images) {
this.images = images;
}
public void addLink(String url, String text, Integer pos) {
Map<String, String> link = new HashMap<>();
link.put("url", url);
link.put("text", text);
link.put("offset", String.valueOf(pos));
links.add(link);
}
public List<Map<String, String>> getLinks() {
if (links == null)
return Collections.emptyList();
return links;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getSitename() {
return sitename;
}
public void setSitename(String sitename) {
this.sitename = sitename;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
@Override
public String toString() {
return "title:" + getTitle() + " imageUrl:" + getImageUrl() + " text:" + text;
}
}

216
app/src/main/java/org/purplei2p/lightning/reading/OutputFormatter.java

@ -1,216 +0,0 @@ @@ -1,216 +0,0 @@
package org.purplei2p.lightning.reading;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
/**
* @author goose | jim
* @author karussell
* <p/>
* this class will be responsible for taking our top node and stripping out junk
* we don't want and getting it ready for how we want it presented to the user
*/
public class OutputFormatter {
private static final int MIN_FIRST_PARAGRAPH_TEXT = 50; // Min size of first paragraph
private static final int MIN_PARAGRAPH_TEXT = 30; // Min size of any other paragraphs
private static final List<String> NODES_TO_REPLACE = Arrays.asList("strong", "b", "i");
private Pattern unlikelyPattern = Pattern.compile("display:none|visibility:hidden");
private final int minFirstParagraphText;
private final int minParagraphText;
private final List<String> nodesToReplace;
private String nodesToKeepCssSelector = "p, ol";
public OutputFormatter() {
this(MIN_FIRST_PARAGRAPH_TEXT, MIN_PARAGRAPH_TEXT, NODES_TO_REPLACE);
}
public OutputFormatter(int minParagraphText) {
this(minParagraphText, minParagraphText, NODES_TO_REPLACE);
}
public OutputFormatter(int minFirstParagraphText, int minParagraphText) {
this(minFirstParagraphText, minParagraphText, NODES_TO_REPLACE);
}
private OutputFormatter(int minFirstParagraphText, int minParagraphText,
List<String> nodesToReplace) {
this.minFirstParagraphText = minFirstParagraphText;
this.minParagraphText = minParagraphText;
this.nodesToReplace = nodesToReplace;
}
/**
* set elements to keep in output text
*/
public void setNodesToKeepCssSelector(String nodesToKeepCssSelector) {
this.nodesToKeepCssSelector = nodesToKeepCssSelector;
}
/**
* takes an element and turns the P tags into \n\n
*/
public String getFormattedText(Element topNode) {
setParagraphIndex(topNode, nodesToKeepCssSelector);
removeNodesWithNegativeScores(topNode);
StringBuilder sb = new StringBuilder();
int countOfP = append(topNode, sb, nodesToKeepCssSelector);
String str = SHelper.innerTrim(sb.toString());
int topNodeLength = topNode.text().length();
if (topNodeLength == 0) {
topNodeLength = 1;
}
boolean lowTextRatio = ((str.length() / (topNodeLength * 1.0)) < 0.25);
if (str.length() > 100 && countOfP > 0 && !lowTextRatio)
return str;
// no subelements
if (str.isEmpty() || (!topNode.text().isEmpty()
&& str.length() <= topNode.ownText().length())
|| countOfP == 0 || lowTextRatio) {
str = topNode.text();
}
// if jsoup failed to parse the whole html now parse this smaller
// snippet again to avoid html tags disturbing our text:
return Jsoup.parse(str).text();
}
/**
* If there are elements inside our top node that have a negative gravity
* score remove them
*/
private void removeNodesWithNegativeScores(Element topNode) {
Elements gravityItems = topNode.select("*[gravityScore]");
for (Element item : gravityItems) {
int score = getScore(item);
int paragraphIndex = getParagraphIndex(item);
if (score < 0 || item.text().length() < getMinParagraph(paragraphIndex)) {
item.remove();
}
}
}
private int append(Element node, StringBuilder sb, String tagName) {
int countOfP = 0; // Number of P elements in the article
int paragraphWithTextIndex = 0;
// is select more costly then getElementsByTag?
MAIN:
for (Element e : node.select(tagName)) {
Element tmpEl = e;
// check all elements until 'node'
while (tmpEl != null && !tmpEl.equals(node)) {
if (unlikely(tmpEl))
continue MAIN;
tmpEl = tmpEl.parent();
}
String text = node2Text(e);
if (text.isEmpty() || text.length() < getMinParagraph(paragraphWithTextIndex)
|| text.length() > SHelper.countLetters(text) * 2) {
continue;
}
if (e.tagName().equals("p")) {
countOfP++;
}
sb.append(text);
sb.append("\n\n");
paragraphWithTextIndex += 1;
}
return countOfP;
}
private static void setParagraphIndex(Element node, String tagName) {
int paragraphIndex = 0;
for (Element e : node.select(tagName)) {
e.attr("paragraphIndex", Integer.toString(paragraphIndex++));
}
}
private int getMinParagraph(int paragraphIndex) {
if (paragraphIndex < 1) {
return minFirstParagraphText;
} else {
return minParagraphText;
}
}
private static int getParagraphIndex(Element el) {
try {
return Integer.parseInt(el.attr("paragraphIndex"));
} catch (NumberFormatException ex) {
return -1;
}
}
private static int getScore(Element el) {
try {
return Integer.parseInt(el.attr("gravityScore"));
} catch (Exception ex) {
return 0;
}
}
private boolean unlikely(Node e) {
if (e.attr("class") != null && e.attr("class").toLowerCase().contains("caption"))
return true;
String style = e.attr("style");
String clazz = e.attr("class");
return unlikelyPattern.matcher(style).find() || unlikelyPattern.matcher(clazz).find();
}
private void appendTextSkipHidden(Element e, StringBuilder accum, int indent) {
for (Node child : e.childNodes()) {
if (unlikely(child)) {
continue;
}
if (child instanceof TextNode) {
TextNode textNode = (TextNode) child;
String txt = textNode.text();
accum.append(txt);
} else if (child instanceof Element) {
Element element = (Element) child;
if (accum.length() > 0 && element.isBlock()
&& !lastCharIsWhitespace(accum))
accum.append(' ');
else if (element.tagName().equals("br"))
accum.append(' ');
appendTextSkipHidden(element, accum, indent + 1);
}
}
}
private static boolean lastCharIsWhitespace(StringBuilder accum) {
return accum.length() != 0 && Character.isWhitespace(accum.charAt(accum.length() - 1));
}
private String node2Text(Element el) {
StringBuilder sb = new StringBuilder(200);
appendTextSkipHidden(el, sb, 0);
return sb.toString();
}
private OutputFormatter setUnlikelyPattern(String unlikelyPattern) {
this.unlikelyPattern = Pattern.compile(unlikelyPattern);
return this;
}
public OutputFormatter appendUnlikelyPattern(String str) {
return setUnlikelyPattern(unlikelyPattern.toString() + '|' + str);
}
}

29
app/src/main/java/org/purplei2p/lightning/reading/SCache.java

@ -1,29 +0,0 @@ @@ -1,29 +0,0 @@
/*
* Copyright 2011 Peter Karich
*
* 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 org.purplei2p.lightning.reading;
/**
*
* @author Peter Karich
*/
public interface SCache {
JResult get(String url);
void put(String url, JResult res);
int getSize();
}

451
app/src/main/java/org/purplei2p/lightning/reading/SHelper.java

@ -1,451 +0,0 @@ @@ -1,451 +0,0 @@
/*
* Copyright 2011 Peter Karich
*
* 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 org.purplei2p.lightning.reading;
import org.jsoup.nodes.Element;
import java.io.UnsupportedEncodingException;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
/**
* @author Peter Karich
*/
class SHelper {
private static final String UTF8 = "UTF-8";
private static final Pattern SPACE = Pattern.compile(" ");
public static String replaceSpaces(String url) {
if (!url.isEmpty()) {
url = url.trim();
if (url.contains(" ")) {
Matcher spaces = SPACE.matcher(url);
url = spaces.replaceAll("%20");
}
}
return url;
}
public static int count(String str, String substring) {
int c = 0;
int index1 = str.indexOf(substring);
if (index1 >= 0) {
c++;
c += count(str.substring(index1 + substring.length()), substring);
}
return c;
}
/**
* remove more than two spaces or newlines
*/
public static String innerTrim(String str) {
if (str.isEmpty())
return "";
StringBuilder sb = new StringBuilder(str.length());
boolean previousSpace = false;
for (int i = 0, length = str.length(); i < length; i++) {
char c = str.charAt(i);
if (c == ' ' || (int) c == 9 || c == '\n') {
previousSpace = true;
continue;
}
if (previousSpace)
sb.append(' ');
previousSpace = false;
sb.append(c);
}
return sb.toString().trim();
}
/**
* Starts reading the encoding from the first valid character until an
* invalid encoding character occurs.
*/
public static String encodingCleanup(String str) {
StringBuilder sb = new StringBuilder(str.length());
boolean startedWithCorrectString = false;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (Character.isDigit(c) || Character.isLetter(c) || c == '-' || c == '_') {
startedWithCorrectString = true;
sb.append(c);
continue;
}
if (startedWithCorrectString)
break;
}
return sb.toString().trim();
}
/**
* @return the longest substring as str1.substring(result[0], result[1]);
*/
public static String getLongestSubstring(String str1, String str2) {
int res[] = longestSubstring(str1, str2);
if (res == null || res[0] >= res[1])
return "";
return str1.substring(res[0], res[1]);
}
private static int[] longestSubstring(String str1, String str2) {
if (str1 == null || str1.isEmpty() || str2 == null || str2.isEmpty())
return null;
// dynamic programming => save already identical length into array
// to understand this algo simply print identical length in every entry of the array
// i+1, j+1 then reuses information from i,j
// java initializes them already with 0
int[][] num = new int[str1.length()][str2.length()];
int maxlen = 0;
int lastSubstrBegin = 0;
int endIndex = 0;
for (int i = 0; i < str1.length(); i++) {
for (int j = 0; j < str2.length(); j++) {
if (str1.charAt(i) == str2.charAt(j)) {
if ((i == 0) || (j == 0))
num[i][j] = 1;
else
num[i][j] = 1 + num[i - 1][j - 1];
if (num[i][j] > maxlen) {
maxlen = num[i][j];
// generate substring from str1 => i
lastSubstrBegin = i - num[i][j] + 1;
endIndex = i + 1;
}
}
}
}
return new int[]{lastSubstrBegin, endIndex};
}
public static String getDefaultFavicon(String url) {
return useDomainOfFirstArg4Second(url, "/favicon.ico");
}
/**
* @param urlForDomain extract the domain from this url
* @param path this url does not have a domain
* @return
*/
public static String useDomainOfFirstArg4Second(String urlForDomain, String path) {
try {
// See: http://stackoverflow.com/questions/1389184/building-an-absolute-url-from-a-relative-url-in-java
URL baseUrl = new URL(urlForDomain);
URL relativeurl = new URL(baseUrl, path);
return relativeurl.toString();
} catch (MalformedURLException ex) {
return path;
}
}
public static String extractHost(String url) {
return extractDomain(url, false);
}
public static String extractDomain(String url, boolean aggressive) {
if (url.startsWith("http://"))
url = url.substring("http://".length());
else if (url.startsWith("https://"))
url = url.substring("https://".length());
if (aggressive) {
if (url.startsWith("www."))
url = url.substring("www.".length());
// strip mobile from start
if (url.startsWith("m."))
url = url.substring("m.".length());
}
int slashIndex = url.indexOf('/');
if (slashIndex > 0)
url = url.substring(0, slashIndex);
return url;
}
public static boolean isVideoLink(String url) {
url = extractDomain(url, true);
return url.startsWith("youtube.com") || url.startsWith("video.yahoo.com")
|| url.startsWith("vimeo.com") || url.startsWith("blip.tv");
}
public static boolean isVideo(String url) {
return url.endsWith(".mpeg") || url.endsWith(".mpg") || url.endsWith(".avi") || url.endsWith(".mov")
|| url.endsWith(".mpg4") || url.endsWith(".mp4") || url.endsWith(".flv") || url.endsWith(".wmv");
}
public static boolean isAudio(String url) {
return url.endsWith(".mp3") || url.endsWith(".ogg") || url.endsWith(".m3u") || url.endsWith(".wav");
}
public static boolean isDoc(String url) {
return url.endsWith(".pdf") || url.endsWith(".ppt") || url.endsWith(".doc")
|| url.endsWith(".swf") || url.endsWith(".rtf") || url.endsWith(".xls");
}
public static boolean isPackage(String url) {
return url.endsWith(".gz") || url.endsWith(".tgz") || url.endsWith(".zip")
|| url.endsWith(".rar") || url.endsWith(".deb") || url.endsWith(".rpm") || url.endsWith(".7z");
}
public static boolean isApp(String url) {
return url.endsWith(".exe") || url.endsWith(".bin") || url.endsWith(".bat") || url.endsWith(".dmg");
}
public static boolean isImage(String url) {
return url.endsWith(".png") || url.endsWith(".jpeg") || url.endsWith(".gif")
|| url.endsWith(".jpg") || url.endsWith(".bmp") || url.endsWith(".ico") || url.endsWith(".eps");
}
/**
* @see "http://blogs.sun.com/CoreJavaTechTips/entry/cookie_handling_in_java_se"
*/
public static void enableCookieMgmt() {
CookieManager manager = new CookieManager();
manager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
CookieHandler.setDefault(manager);
}
/**
* @see "http://stackoverflow.com/questions/2529682/setting-user-agent-of-a-java-urlconnection"
*/
public static void enableUserAgentOverwrite() {
System.setProperty("http.agent", "");
}
public static String getUrlFromUglyGoogleRedirect(String url) {
if (url.startsWith("https://www.google.com/url?")) {
url = url.substring("https://www.google.com/url?".length());
String arr[] = urlDecode(url).split("&");
for (String str : arr) {
if (str.startsWith("q="))
return str.substring("q=".length());
}
}
return null;
}
public static String getUrlFromUglyFacebookRedirect(String url) {
if (url.startsWith("https://www.facebook.com/l.php?u=")) {
url = url.substring("https://www.facebook.com/l.php?u=".length());
return urlDecode(url);
}
return null;
}
public static String urlEncode(String str) {
try {
return URLEncoder.encode(str, UTF8);
} catch (UnsupportedEncodingException ex) {
return str;
}
}
private static String urlDecode(String str) {
try {
return URLDecoder.decode(str, UTF8);
} catch (UnsupportedEncodingException ex) {
return str;
}
}
/**
* Popular sites uses the #! to indicate the importance of the following
* chars. Ugly but true. Such as: facebook, twitter, gizmodo, ...
*/
public static String removeHashbang(String url) {
return url.replaceFirst("#!", "");
}
public static String printNode(Element root) {
return printNode(root, 0);
}
private static String printNode(Element root, int indentation) {
StringBuilder sb = new StringBuilder(indentation);
for (int i = 0; i < indentation; i++) {
sb.append(' ');
}
sb.append(root.tagName());
sb.append(':');
sb.append(root.ownText());
sb.append('\n');
for (Element el : root.children()) {
sb.append(printNode(el, indentation + 1));
sb.append('\n');
}
return sb.toString();
}
public static String estimateDate(String url) {
int index = url.indexOf("://");
if (index > 0)
url = url.substring(index + 3);
int year = -1;
int yearCounter = -1;
int month = -1;
int monthCounter = -1;
int day = -1;
String strs[] = url.split("/");
for (int counter = 0; counter < strs.length; counter++) {
String str = strs[counter];
if (str.length() == 4) {
try {
year = Integer.parseInt(str);
} catch (Exception ex) {
continue;
}
if (year < 1970 || year > 3000) {
year = -1;
continue;
}
yearCounter = counter;
} else if (str.length() == 2) {
if (monthCounter < 0 && counter == yearCounter + 1) {
try {
month = Integer.parseInt(str);
} catch (Exception ex) {
continue;
}
if (month < 1 || month > 12) {
month = -1;
continue;
}
monthCounter = counter;
} else if (counter == monthCounter + 1) {
try {
day = Integer.parseInt(str);
} catch (Exception ignored) {
// ignored
}
if (day < 1 || day > 31) {
day = -1;
continue;
}
break;
}
}
}
if (year < 0)
return null;
StringBuilder str = new StringBuilder(year);
if (month < 1)
return str.toString();
str.append('/');
if (month < 10)
str.append('0');
str.append(month);
if (day < 1)
return str.toString();
str.append('/');
if (day < 10)
str.append('0');
str.append(day);
return str.toString();
}
public static String completeDate(String dateStr) {
if (dateStr == null)
return null;
int index = dateStr.indexOf('/');
if (index > 0) {
index = dateStr.indexOf('/', index + 1);
if (index > 0)
return dateStr;
else
return dateStr + "/01";
}
return dateStr + "/01/01";
}
// with the help of http://stackoverflow.com/questions/1828775/httpclient-and-ssl
public static void enableAnySSL() {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(new KeyManager[0], new TrustManager[]{new DefaultTrustManager()}, new SecureRandom());
SSLContext.setDefault(ctx);
} catch (Exception ex) {
ex.printStackTrace();
}
}
private static class DefaultTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] certs, String arg1) throws CertificateException {
Date today = new Date();
for (X509Certificate certificate : certs) {
certificate.checkValidity(today);
}
}
@Override
public void checkServerTrusted(X509Certificate[] certs, String arg1) throws CertificateException {
Date today = new Date();
for (X509Certificate certificate : certs) {
certificate.checkValidity(today);
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
public static int countLetters(String str) {
int len = str.length();
int chars = 0;
for (int i = 0; i < len; i++) {
if (Character.isLetter(str.charAt(i)))
chars++;
}
return chars;
}
}

342
app/src/main/java/org/purplei2p/lightning/reading/activity/ReadingActivity.java

@ -1,342 +0,0 @@ @@ -1,342 +0,0 @@
package org.purplei2p.lightning.reading.activity;
import android.animation.ObjectAnimator;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import javax.inject.Inject;
import org.purplei2p.lightning.R;
import org.purplei2p.lightning.BrowserApp;
import org.purplei2p.lightning.constant.Constants;
import org.purplei2p.lightning.dialog.BrowserDialog;
import org.purplei2p.lightning.preference.PreferenceManager;
import com.anthonycr.bonsai.Schedulers;
import com.anthonycr.bonsai.Single;
import com.anthonycr.bonsai.SingleAction;
import com.anthonycr.bonsai.SingleOnSubscribe;
import com.anthonycr.bonsai.SingleSubscriber;
import com.anthonycr.bonsai.Subscription;
import org.purplei2p.lightning.reading.HtmlFetcher;
import org.purplei2p.lightning.reading.JResult;
import org.purplei2p.lightning.utils.ThemeUtils;
import org.purplei2p.lightning.utils.Utils;
import butterknife.BindView;
import butterknife.ButterKnife;
public class ReadingActivity extends AppCompatActivity {
private static final String TAG = "ReadingActivity";
@BindView(R.id.textViewTitle) TextView mTitle;
@BindView(R.id.textViewBody) TextView mBody;
@Inject PreferenceManager mPreferences;
private boolean mInvert;
private String mUrl = null;
private int mTextSize;
private ProgressDialog mProgressDialog;
private Subscription mPageLoaderSubscription;
private static final float XXLARGE = 30.0f;
private static final float XLARGE = 26.0f;
private static final float LARGE = 22.0f;
private static final float MEDIUM = 18.0f;
private static final float SMALL = 14.0f;
private static final float XSMALL = 10.0f;
@Override
protected void onCreate(Bundle savedInstanceState) {
BrowserApp.getAppComponent().inject(this);
overridePendingTransition(R.anim.slide_in_from_right, R.anim.fade_out_scale);
mInvert = mPreferences.getInvertColors();
final int color;
if (mInvert) {
setTheme(R.style.Theme_SettingsTheme_Dark);
color = ThemeUtils.getPrimaryColorDark(this);
getWindow().setBackgroundDrawable(new ColorDrawable(color));
} else {
setTheme(R.style.Theme_SettingsTheme);
color = ThemeUtils.getPrimaryColor(this);
getWindow().setBackgroundDrawable(new ColorDrawable(color));
}
super.onCreate(savedInstanceState);
setContentView(R.layout.reading_view);
ButterKnife.bind(this);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mTextSize = mPreferences.getReadingTextSize();
mBody.setTextSize(getTextSize(mTextSize));
mTitle.setText(getString(R.string.untitled));
mBody.setText(getString(R.string.loading));
mTitle.setVisibility(View.INVISIBLE);
mBody.setVisibility(View.INVISIBLE);
Intent intent = getIntent();
if (!loadPage(intent)) {
setText(getString(R.string.untitled), getString(R.string.loading_failed));
}
}
private static float getTextSize(int size) {
switch (size) {
case 0:
return XSMALL;
case 1:
return SMALL;
case 2:
return MEDIUM;
case 3:
return LARGE;
case 4:
return XLARGE;
case 5:
return XXLARGE;
default:
return MEDIUM;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.reading, menu);
MenuItem invert = menu.findItem(R.id.invert_item);
MenuItem textSize = menu.findItem(R.id.text_size_item);
int iconColor = ThemeUtils.getIconThemeColor(this, mInvert);
if (invert != null && invert.getIcon() != null) {
invert.getIcon().mutate().setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
}
if (textSize != null && textSize.getIcon() != null) {
textSize.getIcon().mutate().setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
}
return super.onCreateOptionsMenu(menu);
}
private boolean loadPage(Intent intent) {
if (intent == null) {
return false;
}
mUrl = intent.getStringExtra(Constants.LOAD_READING_URL);
if (mUrl == null) {
return false;
}
if (getSupportActionBar() != null)
getSupportActionBar().setTitle(Utils.getDomainName(mUrl));
mPageLoaderSubscription = loadPage(mUrl).subscribeOn(Schedulers.worker())
.observeOn(Schedulers.main())
.subscribe(new SingleOnSubscribe<ReaderInfo>() {
@Override
public void onStart() {
mProgressDialog = new ProgressDialog(ReadingActivity.this);
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
mProgressDialog.setCancelable(false);
mProgressDialog.setIndeterminate(true);
mProgressDialog.setMessage(getString(R.string.loading));
mProgressDialog.show();
BrowserDialog.setDialogSize(ReadingActivity.this, mProgressDialog);
}
@Override
public void onItem(@Nullable ReaderInfo item) {
if (item == null || item.getTitle().isEmpty() || item.getBody().isEmpty()) {
setText(getString(R.string.untitled), getString(R.string.loading_failed));
} else {
setText(item.getTitle(), item.getBody());
}
}
@Override
public void onError(@NonNull Throwable throwable) {
setText(getString(R.string.untitled), getString(R.string.loading_failed));
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
mProgressDialog = null;
}
}
@Override
public void onComplete() {
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
mProgressDialog = null;
}
}
});
return true;
}
private static Single<ReaderInfo> loadPage(@NonNull final String url) {
return Single.create(new SingleAction<ReaderInfo>() {
@Override
public void onSubscribe(@NonNull SingleSubscriber<ReaderInfo> subscriber) {
HtmlFetcher fetcher = new HtmlFetcher();
try {
JResult result = fetcher.fetchAndExtract(url, 2500, true);
subscriber.onItem(new ReaderInfo(result.getTitle(), result.getText()));
} catch (Exception e) {
subscriber.onError(new Throwable("Encountered exception"));
Log.e(TAG, "Error parsing page", e);
} catch (OutOfMemoryError e) {
System.gc();
subscriber.onError(new Throwable("Out of memory"));
Log.e(TAG, "Out of memory", e);
}
subscriber.onComplete();
}
});
}
private static class ReaderInfo {
@NonNull private final String mTitleText;
@NonNull private final String mBodyText;
public ReaderInfo(@NonNull String title, @NonNull String body) {
mTitleText = title;
mBodyText = body;
}
@NonNull
public String getTitle() {
return mTitleText;
}
@NonNull
public String getBody() {
return mBodyText;
}
}
private void setText(String title, String body) {
if (mTitle == null || mBody == null)
return;
if (mTitle.getVisibility() == View.INVISIBLE) {
mTitle.setAlpha(0.0f);
mTitle.setVisibility(View.VISIBLE);
mTitle.setText(title);
ObjectAnimator animator = ObjectAnimator.ofFloat(mTitle, "alpha", 1.0f);
animator.setDuration(300);
animator.start();
} else {
mTitle.setText(title);
}
if (mBody.getVisibility() == View.INVISIBLE) {
mBody.setAlpha(0.0f);
mBody.setVisibility(View.VISIBLE);
mBody.setText(body);
ObjectAnimator animator = ObjectAnimator.ofFloat(mBody, "alpha", 1.0f);
animator.setDuration(300);
animator.start();
} else {
mBody.setText(body);
}
}
@Override
protected void onDestroy() {
mPageLoaderSubscription.unsubscribe();
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
mProgressDialog = null;
}
super.onDestroy();
}
@Override
protected void onPause() {
super.onPause();
if (isFinishing()) {
overridePendingTransition(R.anim.fade_in_scale, R.anim.slide_out_to_right);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.invert_item:
mPreferences.setInvertColors(!mInvert);
Intent read = new Intent(this, ReadingActivity.class);
read.putExtra(Constants.LOAD_READING_URL, mUrl);
startActivity(read);
finish();
break;
case R.id.text_size_item:
View view = LayoutInflater.from(this).inflate(R.layout.dialog_seek_bar, null);
final SeekBar bar = view.findViewById(R.id.text_size_seekbar);
bar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar view, int size, boolean user) {
mBody.setTextSize(getTextSize(size));
}
@Override
public void onStartTrackingTouch(SeekBar arg0) {
}
@Override
public void onStopTrackingTouch(SeekBar arg0) {
}
});
bar.setMax(5);
bar.setProgress(mTextSize);
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setView(view)
.setTitle(R.string.size)
.setPositiveButton(android.R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int arg1) {
mTextSize = bar.getProgress();
mBody.setTextSize(getTextSize(mTextSize));
mPreferences.setReadingTextSize(bar.getProgress());
}
});
Dialog dialog = builder.show();
BrowserDialog.setDialogSize(this, dialog);
break;
default:
finish();
break;
}
return super.onOptionsItemSelected(item);
}
}

737
app/src/main/java/org/purplei2p/lightning/search/SuggestionsAdapter.java

@ -47,378 +47,365 @@ import org.purplei2p.lightning.utils.ThemeUtils; @@ -47,378 +47,365 @@ import org.purplei2p.lightning.utils.ThemeUtils;
public class SuggestionsAdapter extends BaseAdapter implements Filterable {
private static final Scheduler FILTER_SCHEDULER = Schedulers.newSingleThreadedScheduler();
private static final String CACHE_FILE_TYPE = ".sgg";
private final List<HistoryItem> mFilteredList = new ArrayList<>(5);
private final List<HistoryItem> mHistory = new ArrayList<>(5);
private final List<HistoryItem> mBookmarks = new ArrayList<>(5);
private final List<HistoryItem> mSuggestions = new ArrayList<>(5);
private static final int MAX_SUGGESTIONS = 5;
@NonNull private final Drawable mSearchDrawable;
@NonNull private final Drawable mHistoryDrawable;
@NonNull private final Drawable mBookmarkDrawable;
private final Comparator<HistoryItem> mFilterComparator = new SuggestionsComparator();
@Inject BookmarkModel mBookmarkManager;
@Inject PreferenceManager mPreferenceManager;
@Inject HistoryModel mHistoryModel;
@Inject Application mApplication;
private final List<HistoryItem> mAllBookmarks = new ArrayList<>(5);
private final boolean mDarkTheme;
private boolean mIsIncognito = true;
@NonNull private final Context mContext;
private PreferenceManager.Suggestion mSuggestionChoice;
public SuggestionsAdapter(@NonNull Context context, boolean dark, boolean incognito) {
super();
BrowserApp.getAppComponent().inject(this);
mContext = context;
mDarkTheme = dark || incognito;
mIsIncognito = incognito;
refreshPreferences();
refreshBookmarks();
mSearchDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_search, mDarkTheme);
mBookmarkDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_bookmark, mDarkTheme);
mHistoryDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_history, mDarkTheme);
}
public void refreshPreferences() {
mSuggestionChoice = mPreferenceManager.getSearchSuggestionChoice();
}
public void clearCache() {
// We don't need these cache files anymore
Schedulers.io().execute(new ClearCacheRunnable(mApplication));
}
public void refreshBookmarks() {
mBookmarkManager.getAllBookmarks()
.subscribeOn(Schedulers.io())
.subscribe(new SingleOnSubscribe<List<HistoryItem>>() {
@Override
public void onItem(@Nullable List<HistoryItem> item) {
Preconditions.checkNonNull(item);
mAllBookmarks.clear();
mAllBookmarks.addAll(item);
}
});
}
@Override
public int getCount() {
return mFilteredList.size();
}
@Nullable
@Override
public Object getItem(int position) {
if (position > mFilteredList.size() || position < 0) {
return null;
}
return mFilteredList.get(position);
}
@Override
public long getItemId(int position) {
return 0;
}
private static class SuggestionHolder {
SuggestionHolder(@NonNull View view) {
mTitle = view.findViewById(R.id.title);
mUrl = view.findViewById(R.id.url);
mImage = view.findViewById(R.id.suggestionIcon);
}
@NonNull final ImageView mImage;
@NonNull final TextView mTitle;
@NonNull final TextView mUrl;
}
@Nullable
@Override
public View getView(int position, @Nullable View convertView, ViewGroup parent) {
SuggestionHolder holder;
if (convertView == null) {
LayoutInflater inflater = LayoutInflater.from(mContext);
convertView = inflater.inflate(R.layout.two_line_autocomplete, parent, false);
holder = new SuggestionHolder(convertView);
convertView.setTag(holder);
} else {
holder = (SuggestionHolder) convertView.getTag();
}
HistoryItem web;
web = mFilteredList.get(position);
holder.mTitle.setText(web.getTitle());
holder.mUrl.setText(web.getUrl());
if (mDarkTheme) {
holder.mTitle.setTextColor(Color.WHITE);
}
Drawable image;
switch (web.getImageId()) {
case R.drawable.ic_bookmark: {
image = mBookmarkDrawable;
break;
}
case R.drawable.ic_search: {
image = mSearchDrawable;
break;
}
case R.drawable.ic_history: {
image = mHistoryDrawable;
break;
}
default:
image = mSearchDrawable;
break;
}
holder.mImage.setImageDrawable(image);
return convertView;
}
@NonNull
@Override
public Filter getFilter() {
return new SearchFilter(this, mHistoryModel);
}
private synchronized void publishResults(@NonNull List<HistoryItem> list) {
mFilteredList.clear();
mFilteredList.addAll(list);
notifyDataSetChanged();
}
private void clearSuggestions() {
Completable.create(new CompletableAction() {
@Override
public void onSubscribe(@NonNull CompletableSubscriber subscriber) {
mBookmarks.clear();
mHistory.clear();
mSuggestions.clear();
subscriber.onComplete();
}
}).subscribeOn(FILTER_SCHEDULER)
.observeOn(Schedulers.main())
.subscribe();
}
private void combineResults(final @Nullable List<HistoryItem> bookmarkList,
final @Nullable List<HistoryItem> historyList,
final @Nullable List<HistoryItem> suggestionList) {
Single.create(new SingleAction<List<HistoryItem>>() {
@Override
public void onSubscribe(@NonNull SingleSubscriber<List<HistoryItem>> subscriber) {
List<HistoryItem> list = new ArrayList<>(5);
if (bookmarkList != null) {
mBookmarks.clear();
mBookmarks.addAll(bookmarkList);
}
if (historyList != null) {
mHistory.clear();
mHistory.addAll(historyList);
}
if (suggestionList != null) {
mSuggestions.clear();
mSuggestions.addAll(suggestionList);
}
Iterator<HistoryItem> bookmark = mBookmarks.iterator();
Iterator<HistoryItem> history = mHistory.iterator();
Iterator<HistoryItem> suggestion = mSuggestions.listIterator();
while (list.size() < MAX_SUGGESTIONS) {
if (!bookmark.hasNext() && !suggestion.hasNext() && !history.hasNext()) {
break;
}
if (bookmark.hasNext()) {
list.add(bookmark.next());
}
if (suggestion.hasNext() && list.size() < MAX_SUGGESTIONS) {
list.add(suggestion.next());
}
if (history.hasNext() && list.size() < MAX_SUGGESTIONS) {
list.add(history.next());
}
}
Collections.sort(list, mFilterComparator);
subscriber.onItem(list);
subscriber.onComplete();
}
}).subscribeOn(FILTER_SCHEDULER)
.observeOn(Schedulers.main())
.subscribe(new SingleOnSubscribe<List<HistoryItem>>() {
@Override
public void onItem(@Nullable List<HistoryItem> item) {
Preconditions.checkNonNull(item);
publishResults(item);
}
});
}
@NonNull
private Single<List<HistoryItem>> getBookmarksForQuery(@NonNull final String query) {
return Single.create(new SingleAction<List<HistoryItem>>() {
@Override
public void onSubscribe(@NonNull SingleSubscriber<List<HistoryItem>> subscriber) {
List<HistoryItem> bookmarks = new ArrayList<>(5);
int counter = 0;
for (int n = 0; n < mAllBookmarks.size(); n++) {
if (counter >= 5) {
break;
}
if (mAllBookmarks.get(n).getTitle().toLowerCase(Locale.getDefault())
.startsWith(query)) {
bookmarks.add(mAllBookmarks.get(n));
counter++;
} else if (mAllBookmarks.get(n).getUrl().contains(query)) {
bookmarks.add(mAllBookmarks.get(n));
counter++;
}
}
subscriber.onItem(bookmarks);
subscriber.onComplete();
}
});
}
@NonNull
private Single<List<HistoryItem>> getSuggestionsForQuery(@NonNull final String query) {
if (mSuggestionChoice == PreferenceManager.Suggestion.SUGGESTION_LEGWORK) {
return SuggestionsManager.createLegworkQueryObservable(query, mApplication);
} else if (mSuggestionChoice == PreferenceManager.Suggestion.SUGGESTION_DUCK) {
return SuggestionsManager.createDuckQueryObservable(query, mApplication);
} else {
return Single.empty();
}
}
private boolean shouldRequestNetwork() {
return !mIsIncognito && mSuggestionChoice != PreferenceManager.Suggestion.SUGGESTION_NONE;
}
private static class SearchFilter extends Filter {
@NonNull private final SuggestionsAdapter mSuggestionsAdapter;
@NonNull private final HistoryModel mHistoryModel;
SearchFilter(@NonNull SuggestionsAdapter suggestionsAdapter,
@NonNull HistoryModel historyModel) {
mSuggestionsAdapter = suggestionsAdapter;
mHistoryModel = historyModel;
}
@NonNull
@Override
protected FilterResults performFiltering(@Nullable CharSequence constraint) {
FilterResults results = new FilterResults();
if (constraint == null || constraint.length() == 0) {
mSuggestionsAdapter.clearSuggestions();
return results;
}
String query = constraint.toString().toLowerCase(Locale.getDefault()).trim();
if (mSuggestionsAdapter.shouldRequestNetwork() && !SuggestionsManager.isRequestInProgress()) {
mSuggestionsAdapter.getSuggestionsForQuery(query)
.subscribeOn(Schedulers.worker())
.observeOn(Schedulers.main())
.subscribe(new SingleOnSubscribe<List<HistoryItem>>() {
@Override
public void onItem(@Nullable List<HistoryItem> item) {
mSuggestionsAdapter.combineResults(null, null, item);
}
});
}
mSuggestionsAdapter.getBookmarksForQuery(query)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.main())
.subscribe(new SingleOnSubscribe<List<HistoryItem>>() {
@Override
public void onItem(@Nullable List<HistoryItem> item) {
mSuggestionsAdapter.combineResults(item, null, null);
}
});
mHistoryModel.findHistoryItemsContaining(query)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.main())
.subscribe(new SingleOnSubscribe<List<HistoryItem>>() {
@Override
public void onItem(@Nullable List<HistoryItem> item) {
mSuggestionsAdapter.combineResults(null, item, null);
}
});
results.count = 1;
return results;
}
@NonNull
@Override
public CharSequence convertResultToString(@NonNull Object resultValue) {
return ((HistoryItem) resultValue).getUrl();
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
mSuggestionsAdapter.combineResults(null, null, null);
}
}
private static class ClearCacheRunnable implements Runnable {
@NonNull
private final Application app;
ClearCacheRunnable(@NonNull Application app) {
this.app = app;
}
@Override
public void run() {
File dir = new File(app.getCacheDir().toString());
String[] fileList = dir.list(new NameFilter());
for (String fileName : fileList) {
File file = new File(dir.getPath() + fileName);
file.delete();
}
}
private static class NameFilter implements FilenameFilter {
@Override
public boolean accept(File dir, @NonNull String filename) {
return filename.endsWith(CACHE_FILE_TYPE);
}
}
}
private static class SuggestionsComparator implements Comparator<HistoryItem> {
@Override
public int compare(@NonNull HistoryItem lhs, @NonNull HistoryItem rhs) {
if (lhs.getImageId() == rhs.getImageId()) return 0;
if (lhs.getImageId() == R.drawable.ic_bookmark) return -1;
if (rhs.getImageId() == R.drawable.ic_bookmark) return 1;
if (lhs.getImageId() == R.drawable.ic_history) return -1;
return 1;
}
}
}
private static final Scheduler FILTER_SCHEDULER = Schedulers.newSingleThreadedScheduler();
private static final String CACHE_FILE_TYPE = ".sgg";
private final List<HistoryItem> mFilteredList = new ArrayList<>(5);
private final List<HistoryItem> mHistory = new ArrayList<>(5);
private final List<HistoryItem> mBookmarks = new ArrayList<>(5);
private final List<HistoryItem> mSuggestions = new ArrayList<>(5);
private static final int MAX_SUGGESTIONS = 5;
@NonNull private final Drawable mSearchDrawable;
@NonNull private final Drawable mHistoryDrawable;
@NonNull private final Drawable mBookmarkDrawable;
private final Comparator<HistoryItem> mFilterComparator = new SuggestionsComparator();
@Inject BookmarkModel mBookmarkManager;
@Inject PreferenceManager mPreferenceManager;
@Inject HistoryModel mHistoryModel;
@Inject Application mApplication;
private final List<HistoryItem> mAllBookmarks = new ArrayList<>(5);
private final boolean mDarkTheme;
private boolean mIsIncognito = true;
@NonNull private final Context mContext;
public SuggestionsAdapter(@NonNull Context context, boolean dark, boolean incognito) {
super();
BrowserApp.getAppComponent().inject(this);
mContext = context;
mDarkTheme = dark || incognito;
mIsIncognito = incognito;
refreshBookmarks();
mSearchDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_search, mDarkTheme);
mBookmarkDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_bookmark, mDarkTheme);
mHistoryDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_history, mDarkTheme);
}
public void clearCache() {
// We don't need these cache files anymore
Schedulers.io().execute(new ClearCacheRunnable(mApplication));
}
public void refreshBookmarks() {
mBookmarkManager.getAllBookmarks()
.subscribeOn(Schedulers.io())
.subscribe(new SingleOnSubscribe<List<HistoryItem>>() {
@Override
public void onItem(@Nullable List<HistoryItem> item) {
Preconditions.checkNonNull(item);
mAllBookmarks.clear();
mAllBookmarks.addAll(item);
}
});
}
@Override
public int getCount() {
return mFilteredList.size();
}
@Nullable
@Override
public Object getItem(int position) {
if (position > mFilteredList.size() || position < 0) {
return null;
}
return mFilteredList.get(position);
}
@Override
public long getItemId(int position) {
return 0;
}
private static class SuggestionHolder {
SuggestionHolder(@NonNull View view) {
mTitle = view.findViewById(R.id.title);
mUrl = view.findViewById(R.id.url);
mImage = view.findViewById(R.id.suggestionIcon);
}
@NonNull final ImageView mImage;
@NonNull final TextView mTitle;
@NonNull final TextView mUrl;
}
@Nullable
@Override
public View getView(int position, @Nullable View convertView, ViewGroup parent) {
SuggestionHolder holder;
if (convertView == null) {
LayoutInflater inflater = LayoutInflater.from(mContext);
convertView = inflater.inflate(R.layout.two_line_autocomplete, parent, false);
holder = new SuggestionHolder(convertView);
convertView.setTag(holder);
} else {
holder = (SuggestionHolder) convertView.getTag();
}
HistoryItem web;
web = mFilteredList.get(position);
holder.mTitle.setText(web.getTitle());
holder.mUrl.setText(web.getUrl());
if (mDarkTheme) {
holder.mTitle.setTextColor(Color.WHITE);
}
Drawable image;
switch (web.getImageId()) {
case R.drawable.ic_bookmark: {
image = mBookmarkDrawable;
break;
}
case R.drawable.ic_search: {
image = mSearchDrawable;
break;
}
case R.drawable.ic_history: {
image = mHistoryDrawable;
break;
}
default:
image = mSearchDrawable;
break;
}
holder.mImage.setImageDrawable(image);
return convertView;
}
@NonNull
@Override
public Filter getFilter() {
return new SearchFilter(this, mHistoryModel);
}
private synchronized void publishResults(@NonNull List<HistoryItem> list) {
mFilteredList.clear();
mFilteredList.addAll(list);
notifyDataSetChanged();
}
private void clearSuggestions() {
Completable.create(new CompletableAction() {
@Override
public void onSubscribe(@NonNull CompletableSubscriber subscriber) {
mBookmarks.clear();
mHistory.clear();
mSuggestions.clear();
subscriber.onComplete();
}
}).subscribeOn(FILTER_SCHEDULER)
.observeOn(Schedulers.main())
.subscribe();
}
private void combineResults(final @Nullable List<HistoryItem> bookmarkList,
final @Nullable List<HistoryItem> historyList,
final @Nullable List<HistoryItem> suggestionList) {
Single.create(new SingleAction<List<HistoryItem>>() {
@Override
public void onSubscribe(@NonNull SingleSubscriber<List<HistoryItem>> subscriber) {
List<HistoryItem> list = new ArrayList<>(5);
if (bookmarkList != null) {
mBookmarks.clear();
mBookmarks.addAll(bookmarkList);
}
if (historyList != null) {
mHistory.clear();
mHistory.addAll(historyList);
}
if (suggestionList != null) {
mSuggestions.clear();
mSuggestions.addAll(suggestionList);
}
Iterator<HistoryItem> bookmark = mBookmarks.iterator();
Iterator<HistoryItem> history = mHistory.iterator();
Iterator<HistoryItem> suggestion = mSuggestions.listIterator();
while (list.size() < MAX_SUGGESTIONS) {
if (!bookmark.hasNext() && !suggestion.hasNext() && !history.hasNext()) {
break;
}
if (bookmark.hasNext()) {
list.add(bookmark.next());
}
if (suggestion.hasNext() && list.size() < MAX_SUGGESTIONS) {
list.add(suggestion.next());
}
if (history.hasNext() && list.size() < MAX_SUGGESTIONS) {
list.add(history.next());
}
}
Collections.sort(list, mFilterComparator);
subscriber.onItem(list);
subscriber.onComplete();
}
}).subscribeOn(FILTER_SCHEDULER)
.observeOn(Schedulers.main())
.subscribe(new SingleOnSubscribe<List<HistoryItem>>() {
@Override
public void onItem(@Nullable List<HistoryItem> item) {
Preconditions.checkNonNull(item);
publishResults(item);
}
});
}
@NonNull
private Single<List<HistoryItem>> getBookmarksForQuery(@NonNull final String query) {
return Single.create(new SingleAction<List<HistoryItem>>() {
@Override
public void onSubscribe(@NonNull SingleSubscriber<List<HistoryItem>> subscriber) {
List<HistoryItem> bookmarks = new ArrayList<>(5);
int counter = 0;
for (int n = 0; n < mAllBookmarks.size(); n++) {
if (counter >= 5) {
break;
}
if (mAllBookmarks.get(n).getTitle().toLowerCase(Locale.getDefault())
.startsWith(query)) {
bookmarks.add(mAllBookmarks.get(n));
counter++;
} else if (mAllBookmarks.get(n).getUrl().contains(query)) {
bookmarks.add(mAllBookmarks.get(n));
counter++;
}
}
subscriber.onItem(bookmarks);
subscriber.onComplete();
}
});
}
@NonNull
private Single<List<HistoryItem>> getSuggestionsForQuery(@NonNull final String query) {
return Single.empty();
}
private boolean shouldRequestNetwork() {
return !mIsIncognito;
}
private static class SearchFilter extends Filter {
@NonNull private final SuggestionsAdapter mSuggestionsAdapter;
@NonNull private final HistoryModel mHistoryModel;
SearchFilter(@NonNull SuggestionsAdapter suggestionsAdapter,
@NonNull HistoryModel historyModel) {
mSuggestionsAdapter = suggestionsAdapter;
mHistoryModel = historyModel;
}
@NonNull
@Override
protected FilterResults performFiltering(@Nullable CharSequence constraint) {
FilterResults results = new FilterResults();
if (constraint == null || constraint.length() == 0) {
mSuggestionsAdapter.clearSuggestions();
return results;
}
String query = constraint.toString().toLowerCase(Locale.getDefault()).trim();
if (mSuggestionsAdapter.shouldRequestNetwork()) {
mSuggestionsAdapter.getSuggestionsForQuery(query)
.subscribeOn(Schedulers.worker())
.observeOn(Schedulers.main())
.subscribe(new SingleOnSubscribe<List<HistoryItem>>() {
@Override
public void onItem(@Nullable List<HistoryItem> item) {
mSuggestionsAdapter.combineResults(null, null, item);
}
});
}
mSuggestionsAdapter.getBookmarksForQuery(query)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.main())
.subscribe(new SingleOnSubscribe<List<HistoryItem>>() {
@Override
public void onItem(@Nullable List<HistoryItem> item) {
mSuggestionsAdapter.combineResults(item, null, null);
}
});
mHistoryModel.findHistoryItemsContaining(query)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.main())
.subscribe(new SingleOnSubscribe<List<HistoryItem>>() {
@Override
public void onItem(@Nullable List<HistoryItem> item) {
mSuggestionsAdapter.combineResults(null, item, null);
}
});
results.count = 1;
return results;
}
@NonNull
@Override
public CharSequence convertResultToString(@NonNull Object resultValue) {
return ((HistoryItem) resultValue).getUrl();
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
mSuggestionsAdapter.combineResults(null, null, null);
}
}
private static class ClearCacheRunnable implements Runnable {
@NonNull
private final Application app;
ClearCacheRunnable(@NonNull Application app) {
this.app = app;
}
@Override
public void run() {
File dir = new File(app.getCacheDir().toString());
String[] fileList = dir.list(new NameFilter());
for (String fileName : fileList) {
File file = new File(dir.getPath() + fileName);
file.delete();
}
}
private static class NameFilter implements FilenameFilter {
@Override
public boolean accept(File dir, @NonNull String filename) {
return filename.endsWith(CACHE_FILE_TYPE);
}
}
}
private static class SuggestionsComparator implements Comparator<HistoryItem> {
@Override
public int compare(@NonNull HistoryItem lhs, @NonNull HistoryItem rhs) {
if (lhs.getImageId() == rhs.getImageId()) return 0;
if (lhs.getImageId() == R.drawable.ic_bookmark) return -1;
if (rhs.getImageId() == R.drawable.ic_bookmark) return 1;
if (lhs.getImageId() == R.drawable.ic_history) return -1;
return 1;
}
}
}

35
app/src/main/java/org/purplei2p/lightning/search/SuggestionsManager.kt

@ -1,35 +0,0 @@ @@ -1,35 +0,0 @@
package org.purplei2p.lightning.search
import org.purplei2p.lightning.database.HistoryItem
import org.purplei2p.lightning.search.suggestions.DuckSuggestionsModel
import org.purplei2p.lightning.search.suggestions.LegworkSuggestionsModel
import android.app.Application
import com.anthonycr.bonsai.Single
import com.anthonycr.bonsai.SingleAction
internal object SuggestionsManager {
@JvmStatic
@Volatile var isRequestInProgress: Boolean = false
@JvmStatic
fun createLegworkQueryObservable(query: String, application: Application) =
Single.create(SingleAction<List<HistoryItem>> { subscriber ->
isRequestInProgress = true
val results = LegworkSuggestionsModel(application).fetchResults(query)
subscriber.onItem(results)
subscriber.onComplete()
isRequestInProgress = false
})
@JvmStatic
fun createDuckQueryObservable(query: String, application: Application) =
Single.create(SingleAction<List<HistoryItem>> { subscriber ->
isRequestInProgress = true
val results = DuckSuggestionsModel(application).fetchResults(query)
subscriber.onItem(results)
subscriber.onComplete()
isRequestInProgress = false
})
}

51
app/src/main/java/org/purplei2p/lightning/search/engine/BaseSearchEngine.java

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
package org.purplei2p.lightning.search.engine;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import org.purplei2p.lightning.utils.Preconditions;
/**
* A class representative of a search engine.
* <p>
* Contains three key pieces of information:
* <ul>
* <li>The icon shown for the search engine, should point to a local assets URL.</li>
* <li>The query URL for the search engine, the query will be appended to the end.</li>
* <li>The title string resource for the search engine.</li>
* </ul>
*/
public class BaseSearchEngine {
@NonNull private final String mIconUrl;
@NonNull private final String mQueryUrl;
@StringRes private final int mTitleRes;
BaseSearchEngine(@NonNull String iconUrl,
@NonNull String queryUrl,
@StringRes int titleRes) {
Preconditions.checkNonNull(iconUrl);
Preconditions.checkNonNull(queryUrl);
mIconUrl = iconUrl;
mQueryUrl = queryUrl;
mTitleRes = titleRes;
}
@NonNull
public final String getIconUrl() {
return mIconUrl;
}
@NonNull
public final String getQueryUrl() {
return mQueryUrl;
}
@StringRes
public final int getTitleRes() {
return mTitleRes;
}
}

15
app/src/main/java/org/purplei2p/lightning/search/engine/BaseSearchEngine.kt

@ -1,15 +0,0 @@ @@ -1,15 +0,0 @@
package org.purplei2p.lightning.search.engine
import android.support.annotation.StringRes
/**
* A class representative of a search engine.
*
* Contains three key pieces of information:
* * The icon shown for the search engine, should point to a local assets URL.
* * The query URL for the search engine, the query will be appended to the end.
* * The title string resource for the search engine.
*/
open class BaseSearchEngine internal constructor(val iconUrl: String,
val queryUrl: String,
@StringRes val titleRes: Int)

16
app/src/main/java/org/purplei2p/lightning/search/engine/CustomSearch.java

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
package org.purplei2p.lightning.search.engine;
import android.support.annotation.NonNull;
import org.purplei2p.lightning.R;
/**
* A custom search engine.
*/
public class CustomSearch extends BaseSearchEngine {
public CustomSearch(@NonNull String queryUrl) {
super("file:///android_asset/lightning.png", queryUrl, R.string.search_engine_custom);
}
}

12
app/src/main/java/org/purplei2p/lightning/search/engine/CustomSearch.kt

@ -1,12 +0,0 @@ @@ -1,12 +0,0 @@
package org.purplei2p.lightning.search.engine
import org.purplei2p.lightning.R
/**
* A custom search engine.
*/
class CustomSearch(queryUrl: String) : BaseSearchEngine(
"file:///android_asset/lightning.png",
queryUrl,
R.string.search_engine_custom
)

17
app/src/main/java/org/purplei2p/lightning/search/engine/DuckLiteSearch.java

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
package org.purplei2p.lightning.search.engine;
import org.purplei2p.lightning.R;
import org.purplei2p.lightning.constant.Constants;
/**
* The DuckDuckGo Lite search engine.
* <p>
* See https://duckduckgo.com/assets/logo_homepage.normal.v101.png for the icon.
*/
public class DuckLiteSearch extends BaseSearchEngine {
public DuckLiteSearch() {
super("file:///android_asset/duckduckgo.png", Constants.DUCK_LITE_SEARCH, R.string.search_engine_duckduckgo_lite);
}
}

15
app/src/main/java/org/purplei2p/lightning/search/engine/DuckLiteSearch.kt

@ -1,15 +0,0 @@ @@ -1,15 +0,0 @@
package org.purplei2p.lightning.search.engine
import org.purplei2p.lightning.R
import org.purplei2p.lightning.constant.Constants
/**
* The DuckDuckGo Lite search engine.
*
* See https://duckduckgo.com/assets/logo_homepage.normal.v101.png for the icon.
*/
class DuckLiteSearch : BaseSearchEngine(
"file:///android_asset/duckduckgo.png",
Constants.DUCK_LITE_SEARCH,
R.string.search_engine_duckduckgo_lite
)

17
app/src/main/java/org/purplei2p/lightning/search/engine/DuckSearch.java

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
package org.purplei2p.lightning.search.engine;
import org.purplei2p.lightning.R;
import org.purplei2p.lightning.constant.Constants;
/**
* The DuckDuckGo search engine.
* <p>
* See https://duckduckgo.com/assets/logo_homepage.normal.v101.png for the icon.
*/
public class DuckSearch extends BaseSearchEngine {
public DuckSearch() {
super("file:///android_asset/duckduckgo.png", Constants.DUCK_SEARCH, R.string.search_engine_duckduckgo);
}
}

15
app/src/main/java/org/purplei2p/lightning/search/engine/DuckSearch.kt

@ -1,15 +0,0 @@ @@ -1,15 +0,0 @@
package org.purplei2p.lightning.search.engine
import org.purplei2p.lightning.R
import org.purplei2p.lightning.constant.Constants
/**
* The DuckDuckGo search engine.
*
* See https://duckduckgo.com/assets/logo_homepage.normal.v101.png for the icon.
*/
class DuckSearch : BaseSearchEngine(
"file:///android_asset/duckduckgo.png",
Constants.DUCK_SEARCH,
R.string.search_engine_duckduckgo
)

17
app/src/main/java/org/purplei2p/lightning/search/engine/LegworkSearch.java

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
package org.purplei2p.lightning.search.engine;
import org.purplei2p.lightning.R;
import org.purplei2p.lightning.constant.Constants;
/**
* The Legwork.I2P search engine.
* <p>
* See http://legwork.i2p/env/grafics/LegworkLogo_200.png for the icon.
*/
public class LegworkSearch extends BaseSearchEngine {
public LegworkSearch() {
super("file:///android_asset/legwork.png", Constants.LEGWORK_SEARCH, R.string.search_engine_legwork);
}
}

15
app/src/main/java/org/purplei2p/lightning/search/engine/LegworkSearch.kt

@ -1,15 +0,0 @@ @@ -1,15 +0,0 @@
package org.purplei2p.lightning.search.engine
import org.purplei2p.lightning.R
import org.purplei2p.lightning.constant.Constants
/**
* The Legwork.I2P search engine.
*
* See http://legwork.i2p/env/grafics/LegworkLogo_200.png for the icon.
*/
class LegworkSearch : BaseSearchEngine(
"file:///android_asset/legwork.png",
Constants.LEGWORK_SEARCH,
R.string.search_engine_legwork
)

165
app/src/main/java/org/purplei2p/lightning/search/suggestions/BaseSuggestionsModel.java

@ -1,165 +0,0 @@ @@ -1,165 +0,0 @@
package org.purplei2p.lightning.search.suggestions;
import android.app.Application;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import org.purplei2p.lightning.database.HistoryItem;
import org.purplei2p.lightning.utils.FileUtils;
import org.purplei2p.lightning.utils.Utils;
import okhttp3.Cache;
import okhttp3.CacheControl;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* The base search suggestions API. Provides common
* fetching and caching functionality for each potential
* suggestions provider.
*/
public abstract class BaseSuggestionsModel {
private static final String TAG = "BaseSuggestionsModel";
static final int MAX_RESULTS = 5;
private static final long INTERVAL_DAY = TimeUnit.DAYS.toSeconds(1);
@NonNull private static final String DEFAULT_LANGUAGE = "en";
@NonNull private final OkHttpClient mHttpClient;
@NonNull private final CacheControl mCacheControl;
@NonNull private final String mEncoding;
@NonNull private final String mLanguage;
/**
* Create a URL for the given query in the given language.
*
* @param query the query that was made.
* @param language the locale of the user.
* @return should return a URL that can be fetched using a GET.
*/
@NonNull
protected abstract String createQueryUrl(@NonNull String query, @NonNull String language);
/**
* Parse the results of an input stream into a list of {@link HistoryItem}.
*
* @param inputStream the raw input to parse.
* @param results the list to populate.
* @throws Exception throw an exception if anything goes wrong.
*/
protected abstract void parseResults(@NonNull InputStream inputStream, @NonNull List<HistoryItem> results) throws Exception;
BaseSuggestionsModel(@NonNull Application application, @NonNull String encoding) {
mEncoding = encoding;
mLanguage = getLanguage();
File suggestionsCache = new File(application.getCacheDir(), "suggestion_responses");
mHttpClient = new OkHttpClient.Builder()
.cache(new Cache(suggestionsCache, FileUtils.megabytesToBytes(1)))
.addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)
.build();
mCacheControl = new CacheControl.Builder().maxStale(1, TimeUnit.DAYS).build();
}
/**
* Retrieves the results for a query.
*
* @param rawQuery the raw query to retrieve the results for.
* @return a list of history items for the query.
*/
@NonNull
public final List<HistoryItem> fetchResults(@NonNull final String rawQuery) {
List<HistoryItem> filter = new ArrayList<>(5);
String query;
try {
query = URLEncoder.encode(rawQuery, mEncoding);
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Unable to encode the URL", e);
return filter;
}
InputStream inputStream = downloadSuggestionsForQuery(query, mLanguage);
if (inputStream == null) {
// There are no suggestions for this query, return an empty list.
return filter;
}
try {
parseResults(inputStream, filter);
} catch (Exception e) {
Log.e(TAG, "Unable to parse results", e);
} finally {
Utils.close(inputStream);
}
return filter;
}
/**
* This method downloads the search suggestions for the specific query.
* NOTE: This is a blocking operation, do not fetchResults on the UI thread.
*
* @param query the query to get suggestions for
* @return the cache file containing the suggestions
*/
@Nullable
private InputStream downloadSuggestionsForQuery(@NonNull String query, @NonNull String language) {
String queryUrl = createQueryUrl(query, language);
try {
URL url = new URL(queryUrl);
// OkHttp automatically gzips requests
Request suggestionsRequest = new Request.Builder().url(url)
.addHeader("Accept-Charset", mEncoding)
.cacheControl(mCacheControl)
.build();
Response suggestionsResponse = mHttpClient.newCall(suggestionsRequest).execute();
ResponseBody responseBody = suggestionsResponse.body();
return responseBody != null ? responseBody.byteStream() : null;
} catch (IOException exception) {
Log.e(TAG, "Problem getting search suggestions", exception);
}
return null;
}
@NonNull
private static String getLanguage() {
String language = Locale.getDefault().getLanguage();
if (TextUtils.isEmpty(language)) {
language = DEFAULT_LANGUAGE;
}
return language;
}
@NonNull
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.header("cache-control", "max-age=" + INTERVAL_DAY + ", max-stale=" + INTERVAL_DAY)
.build();
}
};
}

52
app/src/main/java/org/purplei2p/lightning/search/suggestions/DuckSuggestionsModel.java

@ -1,52 +0,0 @@ @@ -1,52 +0,0 @@
package org.purplei2p.lightning.search.suggestions;
import android.app.Application;
import android.support.annotation.NonNull;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.InputStream;
import java.util.List;
import org.purplei2p.lightning.R;
import org.purplei2p.lightning.database.HistoryItem;
import org.purplei2p.lightning.utils.FileUtils;
/**
* The search suggestions provider for the DuckDuckGo search engine.
*/
public final class DuckSuggestionsModel extends BaseSuggestionsModel {
@NonNull private static final String ENCODING = "UTF-8";
@NonNull private final String mSearchSubtitle;
public DuckSuggestionsModel(@NonNull Application application) {
super(application, ENCODING);
mSearchSubtitle = application.getString(R.string.suggestion);
}
@NonNull
@Override
protected String createQueryUrl(@NonNull String query, @NonNull String language) {
return "https://duckduckgo.com/ac/?q=" + query;
}
@Override
protected void parseResults(@NonNull InputStream inputStream, @NonNull List<HistoryItem> results) throws Exception {
String content = FileUtils.readStringFromStream(inputStream, ENCODING);
JSONArray jsonArray = new JSONArray(content);
int counter = 0;
for (int n = 0, size = jsonArray.length(); n < size; n++) {
JSONObject object = jsonArray.getJSONObject(n);
String suggestion = object.getString("phrase");
results.add(new HistoryItem(mSearchSubtitle + " \"" + suggestion + '"',
suggestion, R.drawable.ic_search));
counter++;
if (counter >= MAX_RESULTS) {
break;
}
}
}
}

51
app/src/main/java/org/purplei2p/lightning/search/suggestions/LegworkSuggestionsModel.java

@ -1,51 +0,0 @@ @@ -1,51 +0,0 @@
package org.purplei2p.lightning.search.suggestions;
import android.app.Application;
import android.support.annotation.NonNull;
import org.json.JSONArray;
import java.io.InputStream;
import java.util.List;
import org.purplei2p.lightning.R;
import org.purplei2p.lightning.database.HistoryItem;
import org.purplei2p.lightning.utils.FileUtils;
/**
* The search suggestions provider for the DuckDuckGo search engine.
*/
public final class LegworkSuggestionsModel extends BaseSuggestionsModel {
@NonNull private static final String ENCODING = "UTF-8";
@NonNull private final String mSearchSubtitle;
public LegworkSuggestionsModel(@NonNull Application application) {
super(application, ENCODING);
mSearchSubtitle = application.getString(R.string.suggestion);
}
@NonNull
@Override
protected String createQueryUrl(@NonNull String query, @NonNull String language) {
return "http://legwork.i2p/suggest.json?query=" + query;
}
@Override
protected void parseResults(@NonNull InputStream inputStream, @NonNull List<HistoryItem> results) throws Exception {
String content = FileUtils.readStringFromStream(inputStream, ENCODING);
JSONArray respArray = new JSONArray(content);
JSONArray jsonArray = respArray.getJSONArray(1);
int counter = 0;
for (int n = 0, size = jsonArray.length(); n < size; n++) {
String suggestion = jsonArray.getString(n);
results.add(new HistoryItem(mSearchSubtitle + " \"" + suggestion + '"',
suggestion, R.drawable.ic_search));
counter++;
if (counter >= MAX_RESULTS) {
break;
}
}
}
}

65
app/src/main/java/org/purplei2p/lightning/settings/fragment/GeneralSettingsFragment.java

@ -38,8 +38,6 @@ import org.purplei2p.lightning.utils.ProxyUtils; @@ -38,8 +38,6 @@ import org.purplei2p.lightning.utils.ProxyUtils;
import org.purplei2p.lightning.utils.ThemeUtils;
import org.purplei2p.lightning.utils.Utils;
import static org.purplei2p.lightning.preference.PreferenceManager.Suggestion;
public class GeneralSettingsFragment extends LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
private static final String SETTINGS_PROXY = "proxy";
@ -50,11 +48,10 @@ public class GeneralSettingsFragment extends LightningPreferenceFragment impleme @@ -50,11 +48,10 @@ public class GeneralSettingsFragment extends LightningPreferenceFragment impleme
private static final String SETTINGS_DOWNLOAD = "download";
private static final String SETTINGS_HOME = "home";
private static final String SETTINGS_SEARCHENGINE = "search";
private static final String SETTINGS_SUGGESTIONS = "suggestions_choice";
private Activity mActivity;
private static final int API = android.os.Build.VERSION.SDK_INT;
private CharSequence[] mProxyChoices;
private Preference proxy, useragent, downloadloc, home, searchengine, searchsSuggestions;
private Preference proxy, useragent, downloadloc, home, searchengine;
private String mDownloadLocation;
private int mAgentChoice;
private String mHomepage;
@ -80,7 +77,6 @@ public class GeneralSettingsFragment extends LightningPreferenceFragment impleme @@ -80,7 +77,6 @@ public class GeneralSettingsFragment extends LightningPreferenceFragment impleme
downloadloc = findPreference(SETTINGS_DOWNLOAD);
home = findPreference(SETTINGS_HOME);
searchengine = findPreference(SETTINGS_SEARCHENGINE);
searchsSuggestions = findPreference(SETTINGS_SUGGESTIONS);
CheckBoxPreference cbImages = (CheckBoxPreference) findPreference(SETTINGS_IMAGES);
CheckBoxPreference cbJsScript = (CheckBoxPreference) findPreference(SETTINGS_JAVASCRIPT);
@ -90,7 +86,6 @@ public class GeneralSettingsFragment extends LightningPreferenceFragment impleme @@ -90,7 +86,6 @@ public class GeneralSettingsFragment extends LightningPreferenceFragment impleme
useragent.setOnPreferenceClickListener(this);
downloadloc.setOnPreferenceClickListener(this);
home.setOnPreferenceClickListener(this);
searchsSuggestions.setOnPreferenceClickListener(this);
searchengine.setOnPreferenceClickListener(this);
cbImages.setOnPreferenceChangeListener(this);
cbJsScript.setOnPreferenceChangeListener(this);
@ -114,18 +109,6 @@ public class GeneralSettingsFragment extends LightningPreferenceFragment impleme @@ -114,18 +109,6 @@ public class GeneralSettingsFragment extends LightningPreferenceFragment impleme
downloadloc.setSummary(mDownloadLocation);
switch (mPreferenceManager.getSearchSuggestionChoice()) {
case SUGGESTION_LEGWORK:
searchsSuggestions.setSummary(R.string.powered_by_legwork);
break;
case SUGGESTION_DUCK:
searchsSuggestions.setSummary(R.string.powered_by_duck);
break;
case SUGGESTION_NONE:
searchsSuggestions.setSummary(R.string.search_suggestions_off);
break;
}
if (mHomepage.contains(Constants.SCHEME_HOMEPAGE)) {
home.setSummary(getResources().getString(R.string.action_homepage));
@ -341,49 +324,6 @@ public class GeneralSettingsFragment extends LightningPreferenceFragment impleme @@ -341,49 +324,6 @@ public class GeneralSettingsFragment extends LightningPreferenceFragment impleme
BrowserDialog.setDialogSize(mActivity, dialog);
}
private void suggestionsDialog() {
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.search_suggestions));
int currentChoice = 2;
switch (mPreferenceManager.getSearchSuggestionChoice()) {
case SUGGESTION_LEGWORK:
currentChoice = 0;
break;
case SUGGESTION_DUCK:
currentChoice = 1;
break;
case SUGGESTION_NONE:
currentChoice = 2;
break;
}
picker.setSingleChoiceItems(R.array.suggestions, currentChoice,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case 0:
mPreferenceManager.setSearchSuggestionChoice(Suggestion.SUGGESTION_LEGWORK);
searchsSuggestions.setSummary(R.string.powered_by_legwork);
break;
case 1:
mPreferenceManager.setSearchSuggestionChoice(Suggestion.SUGGESTION_DUCK);
searchsSuggestions.setSummary(R.string.powered_by_duck);
break;
case 2:
mPreferenceManager.setSearchSuggestionChoice(Suggestion.SUGGESTION_NONE);
searchsSuggestions.setSummary(R.string.search_suggestions_off);
break;
}
}
});
picker.setPositiveButton(getResources().getString(R.string.action_ok), null);
Dialog dialog = picker.show();
BrowserDialog.setDialogSize(mActivity, dialog);
}
private void homePicker() {
String currentHomepage;
mHomepage = mPreferenceManager.getHomepage();
@ -539,9 +479,6 @@ public class GeneralSettingsFragment extends LightningPreferenceFragment impleme @@ -539,9 +479,6 @@ public class GeneralSettingsFragment extends LightningPreferenceFragment impleme
case SETTINGS_SEARCHENGINE:
searchDialog();
return true;
case SETTINGS_SUGGESTIONS:
suggestionsDialog();
return true;
default:
return false;
}

21
app/src/main/res/layout/bookmark_drawer.xml

@ -100,26 +100,5 @@ @@ -100,26 +100,5 @@
android:paddingTop="4dp"
app:srcCompat="@drawable/ic_action_star"/>
</FrameLayout>
<FrameLayout
android:id="@+id/action_reading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?attr/actionBarItemBackground"
android:clickable="true">
<ImageView
android:id="@+id/icon_reading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="@string/action_forward"
android:paddingBottom="4dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingTop="4dp"
app:srcCompat="@drawable/ic_action_reading"/>
</FrameLayout>
</LinearLayout>
</LinearLayout>

3
app/src/main/res/menu-large/incognito.xml

@ -33,8 +33,5 @@ @@ -33,8 +33,5 @@
<item
android:id="@+id/action_bookmarks"
android:title="@string/action_bookmarks"/>
<item
android:id="@+id/action_reading_mode"
android:title="@string/reading_mode"/>
</menu>

3
app/src/main/res/menu-large/main.xml

@ -65,9 +65,6 @@ @@ -65,9 +65,6 @@
<item
android:id="@+id/action_add_bookmark"
android:title="@string/action_add_bookmark"/>
<item
android:id="@+id/action_reading_mode"
android:title="@string/reading_mode"/>
<item
android:id="@+id/action_settings"
android:title="@string/settings"/>

3
app/src/main/res/menu-xlarge/incognito.xml

@ -33,8 +33,5 @@ @@ -33,8 +33,5 @@
<item
android:id="@+id/action_bookmarks"
android:title="@string/action_bookmarks"/>
<item
android:id="@+id/action_reading_mode"
android:title="@string/reading_mode"/>
</menu>

3
app/src/main/res/menu-xlarge/main.xml

@ -65,9 +65,6 @@ @@ -65,9 +65,6 @@
<item
android:id="@+id/action_add_bookmark"
android:title="@string/action_add_bookmark"/>
<item
android:id="@+id/action_reading_mode"
android:title="@string/reading_mode"/>
<item
android:id="@+id/action_settings"
android:title="@string/settings"/>

4
app/src/main/res/menu/incognito.xml

@ -16,9 +16,5 @@ @@ -16,9 +16,5 @@
<item
android:id="@+id/action_bookmarks"
android:title="@string/action_bookmarks"/>
<item
android:id="@+id/action_reading_mode"
android:title="@string/reading_mode"/>
</menu>

3
app/src/main/res/menu/main.xml

@ -47,9 +47,6 @@ @@ -47,9 +47,6 @@
<item
android:id="@+id/action_add_bookmark"
android:title="@string/action_add_bookmark"/>
<item
android:id="@+id/action_reading_mode"
android:title="@string/reading_mode"/>
<item
android:id="@+id/action_settings"
android:title="@string/settings"/>

5
app/src/main/res/values-ru/strings.xml

@ -119,10 +119,6 @@ @@ -119,10 +119,6 @@
<string name="label_realm">Сообщение сервера: %s</string>
<string name="hint_username">Имя пользователя</string>
<string name="hint_password">Пароль</string>
<string name="search_suggestions">Подсказки поиска</string>
<string name="powered_by_legwork">Используя Legwork</string>
<string name="powered_by_duck">Используя DuckDuckGo</string>
<string name="search_suggestions_off">Без поисковых подсказок</string>
<string name="http_proxy">HTTP прокси</string>
<string-array name="proxy_choices_array">
<item>Нет</item>
@ -170,7 +166,6 @@ @@ -170,7 +166,6 @@
<string name="close_other_tabs">Закрыть остальные вкладки</string>
<string name="third_party">Блокировать куки со сторонних сайтов</string>
<string name="color_mode">Включить цветной режим</string>
<string name="reading_mode">Режим чтения</string>
<string name="loading">Загрузка&#8230;</string>
<string name="loading_failed">Не удалось загрузить страницу.</string>
<string name="snacktory">Snacktory</string>

6
app/src/main/res/values/arrays.xml

@ -19,12 +19,6 @@ @@ -19,12 +19,6 @@
<item>@string/action_webpage</item>
</string-array>
<string-array name="suggestions">
<item>@string/powered_by_legwork</item>
<item>@string/powered_by_duck</item>
<item>@string/search_suggestions_off</item>
</string-array>
<string-array name="themes">
<item>@string/light_theme</item>
<item>@string/dark_theme</item>

5
app/src/main/res/values/strings.xml

@ -119,10 +119,6 @@ @@ -119,10 +119,6 @@
<string name="label_realm">Server message: %s</string>
<string name="hint_username">Username</string>
<string name="hint_password">Password</string>
<string name="search_suggestions">Search suggestions</string>
<string name="powered_by_legwork">Powered by Legwork</string>
<string name="powered_by_duck">Powered by DuckDuckGo</string>
<string name="search_suggestions_off">No search suggestions</string>
<string name="http_proxy">HTTP proxy</string>
<string-array name="proxy_choices_array">
<item>None</item>
@ -170,7 +166,6 @@ @@ -170,7 +166,6 @@
<string name="close_other_tabs">Close other tabs</string>
<string name="third_party">Block 3rd party cookies</string>
<string name="color_mode">Enable color mode</string>
<string name="reading_mode">Reader mode</string>
<string name="loading">Loading&#8230;</string>
<string name="loading_failed">Couldn\'t load anything from the page.</string>
<string name="snacktory">Snacktory</string>

3
app/src/main/res/xml/preference_general.xml

@ -29,8 +29,5 @@ @@ -29,8 +29,5 @@
<Preference
android:key="search"
android:title="@string/search" />
<Preference
android:key="suggestions_choice"
android:title="@string/search_suggestions" />
</PreferenceCategory>
</PreferenceScreen>

6
build.gradle

@ -1,12 +1,10 @@ @@ -1,12 +1,10 @@
buildscript {
ext.kotlin_version = '1.1.3'
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.6.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@ -23,6 +21,6 @@ ext { @@ -23,6 +21,6 @@ ext {
targetSdkVersion = 26
buildToolsVersion = '26.0.3'
versionName = '4.5.1'
versionCode = 96
versionName = '0.1.2'
versionCode = 3
}

Loading…
Cancel
Save