diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 51ddc7258..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,26 +0,0 @@ -感谢您提交 Issue,请在提交前仔细阅读以下信息。 - -Issue 的内容仅可为 -* Bug 反馈 -* 功能申请 - -若您希望得到使用指导(譬如如何删除快速搜索),请至 [G+ 社区](https://plus.google.com/u/1/communities/103823982034655188459) 发贴。若有其他问题,可在应用内找到开发者的邮箱地址,使用电子邮件与开发者交流。 - -若内容为 Bug 反馈,请先查看 [release 页面](https://github.com/seven332/EhViewer/releases),以帮助确认该 bug 是否存在于最新版中;同时应详细描述 bug 内容与重现方法,必要时可附加截图,但请确定截图符合风序良俗。 - -功能申请包括添加新功能与改进现有功能。 - -一个 Issue 只包含一个 Bug 反馈或者功能申请。若有多个需求,请提交多个 Issue。 - -尽量不提交重复的 Issue,可先进行搜索。对于重复的 Issue,开发者可能会直接关闭并添加 Duplicate 标签。 - - -Thank you for taking the time to submit an issue. Before you proceed, please consider the following. - -An issue can only be about: -* Bug Report -* Feature Request - -If you need application instructions, please post your question in [G+ community](https://plus.google.com/u/1/communities/103823982034655188459). If you have other questions, please get the developer's email address in the application, and send him/her an email. - -One issue one bug report or feature request. diff --git a/.github/ISSUE_TEMPLATE/-------feature-request.md b/.github/ISSUE_TEMPLATE/-------feature-request.md new file mode 100644 index 000000000..1c51bcc30 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/-------feature-request.md @@ -0,0 +1,20 @@ +--- +name: 功能申请 / Feature request +about: 有个想法 / Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**这个功能是用来解决问题的吗? / Is your feature request related to a problem? Please describe.** +描述这个问题。 +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**描述你想要的解决方法 / Describe the solution you'd like** +描述应通过何种行为来解决这个问题。 +A clear and concise description of what you want to happen. + +**备注 / Additional context** +还有别的想说的吗? +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/bug------bug-report.md b/.github/ISSUE_TEMPLATE/bug------bug-report.md new file mode 100644 index 000000000..698923bad --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug------bug-report.md @@ -0,0 +1,41 @@ +--- +name: Bug 反馈 / Bug report +about: 出问题了 / Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**简略描述 / Describe the bug** +大概说下是什么问题。 +A clear and concise description of what the bug is. + +**如何重现 / To Reproduce** +重现的步骤: +1. 打开什么什么 +2. 点击什么什么 +3. 出现什么什么问题 +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**预期行为 / Expected behavior** +应该是什么样子的? +A clear and concise description of what you expected to happen. + +**截图 / Screenshots** +能截图的话就发截图,请注意风序良俗。 +If applicable, add screenshots to help explain your problem. + +**设备型号与 Android 版本 / Device model and Android version** + - 设备型号:【比如 Pixel 6 XXXL】 + - Android 版本:【比如 Android 12.1】 + - Device model: [e.g. Pixel 6 XXXL] + - Android version: [e.g. Android 12.1] + +**备注 / Additional context** +还有什么想说的? +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/sad-panda----------slow-download-speed.md b/.github/ISSUE_TEMPLATE/sad-panda----------slow-download-speed.md new file mode 100644 index 000000000..c8475236c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/sad-panda----------slow-download-speed.md @@ -0,0 +1,11 @@ +--- +name: Sad Panda / 下载太慢 / Slow download speed +about: 所有其他问题 / All other questions +title: '' +labels: '' +assignees: '' + +--- + +加入我们的 Telegram 群组,[https://t.me/ehviewer](https://t.me/ehviewer)。温柔[大小]姐姐手把手帮你解决问题。 +Join our Telegram group, [https://t.me/ehviewer](https://t.me/ehviewer). Administrators will help you solve the problem. diff --git a/.gitignore b/.gitignore index 28bbe8f98..cee31a082 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ *.iml .idea /captures +release +google-services.json diff --git a/README.md b/README.md index f43bc2e41..d7ebcda19 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +# DEPRECATED + +[![Telegram](https://img.shields.io/badge/chat-Telegram-blue.svg)](https://t.me/ehviewer) + # EhViewer ![Icon](art/launcher_icon-web.png) @@ -31,6 +35,13 @@ Linux The apk is in app\build\outputs\apk +# Download + +[下载](https://github.com/seven332/EhViewer/releases) + +[Download](https://github.com/seven332/EhViewer/releases) + + # Thanks 本项目受到了诸多开源项目的帮助 @@ -55,7 +66,7 @@ Here is the libraries # License - Copyright (C) 2014-2016 Hippo Seven + Copyright (C) 2014-2019 Hippo Seven Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/build.gradle b/app/build.gradle index bcc651249..81fdefad4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,31 +1,48 @@ -import com.android.build.gradle.internal.tasks.DefaultAndroidTask - apply plugin: 'com.android.application' -apply plugin: 'android-chinese-string' + +if (file('google-services.json').exists()) { + apply plugin: 'com.google.gms.google-services' + apply plugin: 'io.fabric' +} android { - compileSdkVersion 25 - buildToolsVersion "25.0.3" + compileSdkVersion 28 defaultConfig { applicationId "com.hippo.ehviewer" minSdkVersion 14 - targetSdkVersion 25 - versionCode 77 - versionName "1.0.24" + targetSdkVersion 28 + versionCode 104 + versionName "1.7.3" vectorDrawables.useSupportLibrary = true resConfigs "zh", "zh-rCN", "zh-rHK", "zh-rTW", - "es" + "es", "ja", "ko", "fr", "de", "th" + testOptions.unitTests.includeAndroidResources = true + ndk { + abiFilters "armeabi-v7a", "x86" + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } lintOptions { + disable 'MissingTranslation' abortOnError true + checkReleaseBuilds true } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + buildConfigField 'String', 'FILE_PROVIDER_AUTHORITY', '"com.hippo.ehviewer.fileprovider"' + } + debug { + applicationIdSuffix ".debug" + buildConfigField 'String', 'FILE_PROVIDER_AUTHORITY', '"com.hippo.ehviewer.debug.fileprovider"' } } @@ -42,7 +59,7 @@ task copyNotice(type: Copy) { finalizedBy ":daogenerator:executeDaoGenerator" } -tasks.withType(DefaultAndroidTask) { +tasks.withType(JavaCompile) { task -> task.dependsOn copyNotice } @@ -51,35 +68,52 @@ clean { } dependencies { - compile fileTree(include: ['*.jar'], dir: 'libs') - compile "com.android.support:appcompat-v7:$supportLibrary" - compile "com.android.support:design:$supportLibrary" - compile 'com.github.amlcurran.showcaseview:library:5.4.3' - compile 'com.github.seven332:android-recaptcha:2bbb3459a8' - compile 'com.github.seven332:animator:0.1.0' - compile 'com.github.seven332:beerbelly:0.1.4' - compile 'com.github.seven332:conaco:0.1.5-eh' - compile 'com.github.seven332:drawerlayout:0.2.1' - compile 'com.github.seven332:easyrecyclerview:0.1.1' - compile 'com.github.seven332:glgallery:0.1.2' - compile 'com.github.seven332:glview:0.1.0' - compile 'com.github.seven332:glview-image:0.1.0' - compile 'com.github.seven332:hotspot:0.1.0' - compile 'com.github.seven332:image:0.1.12' - compile 'com.github.seven332:okhttp:3.5.0' - compile 'com.github.seven332:refreshlayout:0.1.0' - compile 'com.github.seven332:ripple:0.1.2' - compile 'com.github.seven332:streampipe:0.1.0' - compile 'com.github.seven332:tuxiang:0.1.2' - compile 'com.github.seven332:unifile:0.2.0' - compile 'com.github.seven332:yorozuya:0.1.2' - compile 'com.github.seven332:yorozuya-thread:0.1.1' - compile 'com.github.seven332:yorozuya-collect:0.1.4' - compile 'com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.9.2' - compile 'org.ccil.cowan.tagsoup:tagsoup:1.2.1' - compile 'org.greenrobot:greendao:2.2.0' - compile 'org.jsoup:jsoup:1.9.2' - testCompile 'junit:junit:4.12' - testCompile 'org.robolectric:robolectric:3.4' - testCompile 'org.jooq:joor:0.9.6' + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'com.google.android.material:material:1.0.0' + implementation 'com.google.firebase:firebase-core:16.0.9' + implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' + implementation 'com.getkeepsafe.relinker:relinker:1.3.1' + implementation 'com.github.amlcurran.showcaseview:library:5.4.3' + implementation 'com.github.seven332.a7zip:extract-lite:d4ddec5793' + implementation 'com.github.seven332:android-recaptcha:2bbb3459a8' + implementation 'com.github.seven332:android-resource:0.1.0' + implementation 'com.github.seven332:animator:0.1.0' + implementation 'com.github.seven332:beerbelly:0.1.4' + implementation 'com.github.seven332:conaco:0.1.5-eh' + implementation 'com.github.seven332:drawerlayout:ea2bb388f0' + implementation 'com.github.seven332:easyrecyclerview:0.1.1' + implementation 'com.github.seven332:glgallery:0.1.2' + implementation 'com.github.seven332:glview:0.1.0' + implementation 'com.github.seven332:glview-image:0.1.0' + implementation 'com.github.seven332:hotspot:0.1.0' + implementation 'com.github.seven332:image:0.1.12' + implementation 'com.github.seven332:refreshlayout:0.1.0' + implementation 'com.github.seven332:ripple:0.1.2' + implementation 'com.github.seven332:streampipe:0.1.0' + implementation 'com.github.seven332:tuxiang:0.1.6' + implementation 'com.github.seven332:unifile:9ec57bcd8f' + implementation 'com.github.seven332:yorozuya:0.1.2' + implementation 'com.github.seven332:yorozuya-thread:0.1.1' + implementation 'com.github.seven332:yorozuya-collect:0.1.4' + implementation 'com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:1.0.0' + implementation 'com.squareup.okhttp3:okhttp:3.12.3' + implementation 'org.ccil.cowan.tagsoup:tagsoup:1.2.1' + implementation 'org.greenrobot:greendao:2.2.1' + implementation 'org.jsoup:jsoup:1.12.1' + testImplementation 'junit:junit:4.12' + testImplementation 'org.robolectric:robolectric:4.2.1' + testImplementation 'org.jooq:joor:0.9.6' +} + +configurations.all { + resolutionStrategy { + force 'com.github.seven332:glgallery:25893283ca' + force 'com.github.seven332:glview:ba6aee61d7' + force 'com.github.seven332:glview-image:68d94b0fc2' + force 'com.github.seven332:image:09b43c0c68' + + exclude group: 'com.github.seven332', module: 'okhttp' + } } diff --git a/app/libs/libGoogleAnalyticsServices.jar b/app/libs/libGoogleAnalyticsServices.jar deleted file mode 100644 index 082a9180e..000000000 Binary files a/app/libs/libGoogleAnalyticsServices.jar and /dev/null differ diff --git a/app/src/debug/res/values-es/strings.xml b/app/src/debug/res/values-es/strings.xml new file mode 100755 index 000000000..82d363589 --- /dev/null +++ b/app/src/debug/res/values-es/strings.xml @@ -0,0 +1,22 @@ + + + + + + EhViewer Debug + + diff --git a/app/src/debug/res/values-ja/strings.xml b/app/src/debug/res/values-ja/strings.xml new file mode 100755 index 000000000..82d363589 --- /dev/null +++ b/app/src/debug/res/values-ja/strings.xml @@ -0,0 +1,22 @@ + + + + + + EhViewer Debug + + diff --git a/app/src/debug/res/values-ko/strings.xml b/app/src/debug/res/values-ko/strings.xml new file mode 100755 index 000000000..82d363589 --- /dev/null +++ b/app/src/debug/res/values-ko/strings.xml @@ -0,0 +1,22 @@ + + + + + + EhViewer Debug + + diff --git a/app/src/debug/res/values-zh-rCN/strings.xml b/app/src/debug/res/values-zh-rCN/strings.xml new file mode 100755 index 000000000..82d363589 --- /dev/null +++ b/app/src/debug/res/values-zh-rCN/strings.xml @@ -0,0 +1,22 @@ + + + + + + EhViewer Debug + + diff --git a/app/src/debug/res/values-zh-rHK/strings.xml b/app/src/debug/res/values-zh-rHK/strings.xml new file mode 100755 index 000000000..82d363589 --- /dev/null +++ b/app/src/debug/res/values-zh-rHK/strings.xml @@ -0,0 +1,22 @@ + + + + + + EhViewer Debug + + diff --git a/app/src/debug/res/values-zh-rTW/strings.xml b/app/src/debug/res/values-zh-rTW/strings.xml new file mode 100755 index 000000000..82d363589 --- /dev/null +++ b/app/src/debug/res/values-zh-rTW/strings.xml @@ -0,0 +1,22 @@ + + + + + + EhViewer Debug + + diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml new file mode 100755 index 000000000..82d363589 --- /dev/null +++ b/app/src/debug/res/values/strings.xml @@ -0,0 +1,22 @@ + + + + + + EhViewer Debug + + diff --git a/app/src/debug/res/xml-v25/shortcuts.xml b/app/src/debug/res/xml-v25/shortcuts.xml new file mode 100644 index 000000000..b84b778dd --- /dev/null +++ b/app/src/debug/res/xml-v25/shortcuts.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 27eb8daf7..e81110a99 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,7 +22,9 @@ - + + + @@ -34,7 +36,12 @@ android:theme="@style/AppTheme" android:allowBackup="true" android:fullBackupContent="@xml/backup_scheme" - android:largeHeap="true"> + android:largeHeap="true" + android:networkSecurityConfig="@xml/network_security_config"> + + + + + + @@ -82,17 +92,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -100,50 +134,76 @@ + android:theme="@style/AppTheme.Settings"/> + + + + + + + + + diff --git a/app/src/main/java/com/hippo/app/AppCompatPreferenceActivity.java b/app/src/main/java/com/hippo/app/AppCompatPreferenceActivity.java index 809b9b4c0..b3ba6de2b 100644 --- a/app/src/main/java/com/hippo/app/AppCompatPreferenceActivity.java +++ b/app/src/main/java/com/hippo/app/AppCompatPreferenceActivity.java @@ -16,39 +16,70 @@ package com.hippo.app; +import android.annotation.SuppressLint; import android.content.Intent; import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; import android.os.Bundle; import android.preference.PreferenceActivity; -import android.support.annotation.CallSuper; -import android.support.annotation.LayoutRes; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.ActivityCompat; -import android.support.v4.app.NavUtils; -import android.support.v4.app.TaskStackBuilder; -import android.support.v7.app.ActionBar; -import android.support.v7.app.ActionBarDrawerToggle; -import android.support.v7.app.AppCompatCallback; -import android.support.v7.app.AppCompatDelegate; -import android.support.v7.view.ActionMode; -import android.support.v7.widget.Toolbar; +import android.util.DisplayMetrics; +import android.view.KeyEvent; +import android.view.Menu; import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; +import android.view.Window; +import androidx.annotation.CallSuper; +import androidx.annotation.IdRes; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AppCompatCallback; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.view.ActionMode; +import androidx.appcompat.widget.Toolbar; +import androidx.appcompat.widget.VectorEnabledTintResources; +import androidx.core.app.ActivityCompat; +import androidx.core.app.NavUtils; +import androidx.core.app.TaskStackBuilder; public abstract class AppCompatPreferenceActivity extends PreferenceActivity implements AppCompatCallback, - TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider { + TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider { private AppCompatDelegate mDelegate; + private int mThemeId = 0; + private Resources mResources; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { - getDelegate().installViewFactory(); - getDelegate().onCreate(savedInstanceState); + final AppCompatDelegate delegate = getDelegate(); + delegate.installViewFactory(); + delegate.onCreate(savedInstanceState); + if (delegate.applyDayNight() && mThemeId != 0) { + // If DayNight has been applied, we need to re-apply the theme for + // the changes to take effect. On API 23+, we should bypass + // setTheme(), which will no-op if the theme ID is identical to the + // current theme ID. + if (Build.VERSION.SDK_INT >= 23) { + onApplyThemeResource(getTheme(), mThemeId, false); + } else { + setTheme(mThemeId); + } + } super.onCreate(savedInstanceState); } + @Override + public void setTheme(@StyleRes final int resid) { + super.setTheme(resid); + // Keep hold of the theme id so that we can re-set it later if needed + mThemeId = resid; + } + @Override protected void onPostCreate(@Nullable Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); @@ -68,26 +99,25 @@ public ActionBar getSupportActionBar() { } /** - * Set a {@link android.widget.Toolbar Toolbar} to act as the {@link android.support.v7.app.ActionBar} for this - * Activity window. + * Set a {@link android.widget.Toolbar Toolbar} to act as the + * {@link androidx.appcompat.app.ActionBar} for this Activity window. * *

When set to a non-null value the {@link #getActionBar()} method will return - * an {@link android.support.v7.app.ActionBar} object that can be used to control the given toolbar as if it were - * a traditional window decor action bar. The toolbar's menu will be populated with the - * Activity's options menu and the navigation button will be wired through the standard - * {@link android.R.id#home home} menu select action.

+ * an {@link androidx.appcompat.app.ActionBar} object that can be used to control the given + * toolbar as if it were a traditional window decor action bar. The toolbar's menu will be + * populated with the Activity's options menu and the navigation button will be wired through + * the standard {@link android.R.id#home home} menu select action.

* *

In order to use a Toolbar within the Activity's window content the application * must not request the window feature * {@link android.view.Window#FEATURE_ACTION_BAR FEATURE_SUPPORT_ACTION_BAR}.

* - * @param toolbar Toolbar to set as the Activity's action bar + * @param toolbar Toolbar to set as the Activity's action bar, or {@code null} to clear it */ public void setSupportActionBar(@Nullable Toolbar toolbar) { getDelegate().setSupportActionBar(toolbar); } - @NonNull @Override public MenuInflater getMenuInflater() { return getDelegate().getMenuInflater(); @@ -117,6 +147,24 @@ public void addContentView(View view, ViewGroup.LayoutParams params) { public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); getDelegate().onConfigurationChanged(newConfig); + if (mResources != null) { + // The real (and thus managed) resources object was already updated + // by ResourcesManager, so pull the current metrics from there. + final DisplayMetrics newMetrics = super.getResources().getDisplayMetrics(); + mResources.updateConfiguration(newConfig, newMetrics); + } + } + + @Override + protected void onPostResume() { + super.onPostResume(); + getDelegate().onPostResume(); + } + + @Override + protected void onStart() { + super.onStart(); + getDelegate().onStart(); } @Override @@ -125,10 +173,10 @@ protected void onStop() { getDelegate().onStop(); } + @SuppressWarnings("TypeParameterUnusedInFormals") @Override - protected void onPostResume() { - super.onPostResume(); - getDelegate().onPostResume(); + public T findViewById(@IdRes int id) { + return getDelegate().findViewById(id); } @Override @@ -139,7 +187,7 @@ public final boolean onMenuItemSelected(int featureId, android.view.MenuItem ite final ActionBar ab = getSupportActionBar(); if (item.getItemId() == android.R.id.home && ab != null && - (ab.getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) != 0) { + (ab.getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) != 0) { return onSupportNavigateUp(); } return false; @@ -165,7 +213,7 @@ protected void onTitleChanged(CharSequence title, int color) { *

* * @param featureId The desired feature as defined in - * {@link android.view.Window} or {@link android.support.v4.view.WindowCompat}. + * {@link android.view.Window} or {@link androidx.core.view.WindowCompat}. * @return Returns true if the requested feature is supported and now enabled. * * @see android.app.Activity#requestWindowFeature @@ -192,7 +240,7 @@ public void invalidateOptionsMenu() { */ @Override @CallSuper - public void onSupportActionModeStarted(ActionMode mode) { + public void onSupportActionModeStarted(@NonNull ActionMode mode) { } /** @@ -203,7 +251,7 @@ public void onSupportActionModeStarted(ActionMode mode) { */ @Override @CallSuper - public void onSupportActionModeFinished(ActionMode mode) { + public void onSupportActionModeFinished(@NonNull ActionMode mode) { } /** @@ -217,14 +265,49 @@ public void onSupportActionModeFinished(ActionMode mode) { */ @Nullable @Override - public ActionMode onWindowStartingSupportActionMode(ActionMode.Callback callback) { + public ActionMode onWindowStartingSupportActionMode(@NonNull ActionMode.Callback callback) { return null; } - public ActionMode startSupportActionMode(ActionMode.Callback callback) { + /** + * Start an action mode. + * + * @param callback Callback that will manage lifecycle events for this context mode + * @return The ContextMode that was started, or null if it was canceled + */ + @Nullable + public ActionMode startSupportActionMode(@NonNull ActionMode.Callback callback) { return getDelegate().startSupportActionMode(callback); } + /** + * @deprecated Progress bars are no longer provided in AppCompat. + */ + @Deprecated + public void setSupportProgressBarVisibility(boolean visible) { + } + + /** + * @deprecated Progress bars are no longer provided in AppCompat. + */ + @Deprecated + public void setSupportProgressBarIndeterminateVisibility(boolean visible) { + } + + /** + * @deprecated Progress bars are no longer provided in AppCompat. + */ + @Deprecated + public void setSupportProgressBarIndeterminate(boolean indeterminate) { + } + + /** + * @deprecated Progress bars are no longer provided in AppCompat. + */ + @Deprecated + public void setSupportProgress(int progress) { + } + /** * Support version of {@link #onCreateNavigateUpTaskStack(android.app.TaskStackBuilder)}. * This method will be called on all platform versions. @@ -233,7 +316,7 @@ public ActionMode startSupportActionMode(ActionMode.Callback callback) { * a different task. * *

The default implementation of this method adds the parent chain of this activity - * as specified in the manifest to the supplied {@link android.support.v4.app.TaskStackBuilder}. Applications + * as specified in the manifest to the supplied {@link androidx.core.app.TaskStackBuilder}. Applications * may choose to override this method to construct the desired task stack in a different * way.

* @@ -243,12 +326,12 @@ public ActionMode startSupportActionMode(ActionMode.Callback callback) { * *

Applications that wish to supply extra Intent parameters to the parent stack defined * by the manifest should override - * {@link #onPrepareSupportNavigateUpTaskStack(android.support.v4.app.TaskStackBuilder)}.

+ * {@link #onPrepareSupportNavigateUpTaskStack(androidx.core.app.TaskStackBuilder)}.

* * @param builder An empty TaskStackBuilder - the application should add intents representing * the desired task stack */ - public void onCreateSupportNavigateUpTaskStack(TaskStackBuilder builder) { + public void onCreateSupportNavigateUpTaskStack(@NonNull TaskStackBuilder builder) { builder.addParentStack(this); } @@ -259,15 +342,15 @@ public void onCreateSupportNavigateUpTaskStack(TaskStackBuilder builder) { * Prepare the synthetic task stack that will be generated during Up navigation * from a different task. * - *

This method receives the {@link android.support.v4.app.TaskStackBuilder} with the constructed series of - * Intents as generated by {@link #onCreateSupportNavigateUpTaskStack(android.support.v4.app.TaskStackBuilder)}. + *

This method receives the {@link androidx.core.app.TaskStackBuilder} with the constructed series of + * Intents as generated by {@link #onCreateSupportNavigateUpTaskStack(androidx.core.app.TaskStackBuilder)}. * If any extra data should be added to these intents before launching the new task, * the application should override this method and add that data here.

* * @param builder A TaskStackBuilder that has been populated with Intents by * onCreateNavigateUpTaskStack. */ - public void onPrepareSupportNavigateUpTaskStack(TaskStackBuilder builder) { + public void onPrepareSupportNavigateUpTaskStack(@NonNull TaskStackBuilder builder) { } /** @@ -278,7 +361,7 @@ public void onPrepareSupportNavigateUpTaskStack(TaskStackBuilder builder) { * default Up navigation will be handled automatically. See * {@link #getSupportParentActivityIntent()} for how to specify the parent. If any activity * along the parent chain requires extra Intent arguments, the Activity subclass - * should override the method {@link #onPrepareSupportNavigateUpTaskStack(android.support.v4.app.TaskStackBuilder)} + * should override the method {@link #onPrepareSupportNavigateUpTaskStack(androidx.core.app.TaskStackBuilder)} * to supply those arguments.

* *

See Tasks and @@ -286,7 +369,7 @@ public void onPrepareSupportNavigateUpTaskStack(TaskStackBuilder builder) { * Navigation from the design guide * for more information about navigating within your app.

* - *

See the {@link android.support.v4.app.TaskStackBuilder} class and the Activity methods + *

See the {@link androidx.core.app.TaskStackBuilder} class and the Activity methods * {@link #getSupportParentActivityIntent()}, {@link #supportShouldUpRecreateTask(android.content.Intent)}, and * {@link #supportNavigateUpTo(android.content.Intent)} for help implementing custom Up navigation.

* @@ -322,15 +405,15 @@ public boolean onSupportNavigateUp() { /** * Obtain an {@link android.content.Intent} that will launch an explicit target activity - * specified by sourceActivity's {@link android.support.v4.app.NavUtils#PARENT_ACTIVITY} <meta-data> + * specified by sourceActivity's {@link androidx.core.app.NavUtils#PARENT_ACTIVITY} <meta-data> * element in the application's manifest. If the device is running * Jellybean or newer, the android:parentActivityName attribute will be preferred * if it is present. * * @return a new Intent targeting the defined parent activity of sourceActivity */ - @Override @Nullable + @Override public Intent getSupportParentActivityIntent() { return NavUtils.getParentActivityIntent(this); } @@ -342,13 +425,13 @@ public Intent getSupportParentActivityIntent() { *

If this method returns false the app can trivially call * {@link #supportNavigateUpTo(android.content.Intent)} using the same parameters to correctly perform * up navigation. If this method returns false, the app should synthesize a new task stack - * by using {@link android.support.v4.app.TaskStackBuilder} or another similar mechanism to perform up navigation.

+ * by using {@link androidx.core.app.TaskStackBuilder} or another similar mechanism to perform up navigation.

* * @param targetIntent An intent representing the target destination for up navigation * @return true if navigating up should recreate a new task stack, false if the same task * should be used for the destination */ - public boolean supportShouldUpRecreateTask(Intent targetIntent) { + public boolean supportShouldUpRecreateTask(@NonNull Intent targetIntent) { return NavUtils.shouldUpRecreateTask(this, targetIntent); } @@ -364,7 +447,7 @@ public boolean supportShouldUpRecreateTask(Intent targetIntent) { * * @param upIntent An intent representing the target destination for up navigation */ - public void supportNavigateUpTo(Intent upIntent) { + public void supportNavigateUpTo(@NonNull Intent upIntent) { NavUtils.navigateUpTo(this, upIntent); } @@ -374,13 +457,114 @@ public ActionBarDrawerToggle.Delegate getDrawerToggleDelegate() { return getDelegate().getDrawerToggleDelegate(); } + /** + * {@inheritDoc} + * + *

Please note: AppCompat uses its own feature id for the action bar: + * {@link AppCompatDelegate#FEATURE_SUPPORT_ACTION_BAR FEATURE_SUPPORT_ACTION_BAR}.

+ */ + @Override + public boolean onMenuOpened(int featureId, Menu menu) { + return super.onMenuOpened(featureId, menu); + } + + /** + * {@inheritDoc} + * + *

Please note: AppCompat uses its own feature id for the action bar: + * {@link AppCompatDelegate#FEATURE_SUPPORT_ACTION_BAR FEATURE_SUPPORT_ACTION_BAR}.

+ */ + @Override + public void onPanelClosed(int featureId, Menu menu) { + super.onPanelClosed(featureId, menu); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + getDelegate().onSaveInstanceState(outState); + } + /** * @return The {@link AppCompatDelegate} being used by this Activity. */ + @NonNull public AppCompatDelegate getDelegate() { if (mDelegate == null) { mDelegate = AppCompatDelegate.create(this, this); } return mDelegate; } + + @SuppressLint("RestrictedApi") + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let support action bars open menus in response to the menu key prioritized over + // the window handling it + final int keyCode = event.getKeyCode(); + final ActionBar actionBar = getSupportActionBar(); + if (keyCode == KeyEvent.KEYCODE_MENU + && actionBar != null && actionBar.onMenuKeyEvent(event)) { + return true; + } + return super.dispatchKeyEvent(event); + } + + @SuppressLint("RestrictedApi") + @Override + public Resources getResources() { + if (mResources == null && VectorEnabledTintResources.shouldBeUsed()) { + mResources = new VectorEnabledTintResources(this, super.getResources()); + } + return mResources == null ? super.getResources() : mResources; + } + + /** + * KeyEvents with non-default modifiers are not dispatched to menu's performShortcut in API 25 + * or lower. Here, we check if the keypress corresponds to a menuitem's shortcut combination + * and perform the corresponding action. + */ + private boolean performMenuItemShortcut(int keycode, KeyEvent event) { + if (!(Build.VERSION.SDK_INT >= 26) && !event.isCtrlPressed() + && !KeyEvent.metaStateHasNoModifiers(event.getMetaState()) + && event.getRepeatCount() == 0 + && !KeyEvent.isModifierKey(event.getKeyCode())) { + final Window currentWindow = getWindow(); + if (currentWindow != null && currentWindow.getDecorView() != null) { + final View decorView = currentWindow.getDecorView(); + if (decorView.dispatchKeyShortcutEvent(event)) { + return true; + } + } + } + return false; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (performMenuItemShortcut(keyCode, event)) { + return true; + } + return super.onKeyDown(keyCode, event); + } + + @SuppressLint("RestrictedApi") + @Override + public void openOptionsMenu() { + ActionBar actionBar = getSupportActionBar(); + if (getWindow().hasFeature(Window.FEATURE_OPTIONS_PANEL) + && (actionBar == null || !actionBar.openOptionsMenu())) { + super.openOptionsMenu(); + } + } + + @SuppressLint("RestrictedApi") + @Override + public void closeOptionsMenu() { + ActionBar actionBar = getSupportActionBar(); + if (getWindow().hasFeature(Window.FEATURE_OPTIONS_PANEL) + && (actionBar == null || !actionBar.closeOptionsMenu())) { + super.closeOptionsMenu(); + } + } } diff --git a/app/src/main/java/com/hippo/app/CheckBoxDialogBuilder.java b/app/src/main/java/com/hippo/app/CheckBoxDialogBuilder.java index 34af8a97b..ca9a4c9d4 100644 --- a/app/src/main/java/com/hippo/app/CheckBoxDialogBuilder.java +++ b/app/src/main/java/com/hippo/app/CheckBoxDialogBuilder.java @@ -18,12 +18,11 @@ import android.annotation.SuppressLint; import android.content.Context; -import android.support.v7.app.AlertDialog; import android.view.LayoutInflater; import android.view.View; import android.widget.CheckBox; import android.widget.TextView; - +import androidx.appcompat.app.AlertDialog; import com.hippo.ehviewer.R; public class CheckBoxDialogBuilder extends AlertDialog.Builder { diff --git a/app/src/main/java/com/hippo/app/EditTextDialogBuilder.java b/app/src/main/java/com/hippo/app/EditTextDialogBuilder.java index 750bc39bf..4c89ce9d3 100644 --- a/app/src/main/java/com/hippo/app/EditTextDialogBuilder.java +++ b/app/src/main/java/com/hippo/app/EditTextDialogBuilder.java @@ -19,15 +19,14 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.DialogInterface; -import android.support.design.widget.TextInputLayout; -import android.support.v7.app.AlertDialog; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; - +import androidx.appcompat.app.AlertDialog; +import com.google.android.material.textfield.TextInputLayout; import com.hippo.ehviewer.R; public class EditTextDialogBuilder extends AlertDialog.Builder implements EditText.OnEditorActionListener { diff --git a/app/src/main/java/com/hippo/app/ListCheckBoxDialogBuilder.java b/app/src/main/java/com/hippo/app/ListCheckBoxDialogBuilder.java index ebdb09763..a84822787 100644 --- a/app/src/main/java/com/hippo/app/ListCheckBoxDialogBuilder.java +++ b/app/src/main/java/com/hippo/app/ListCheckBoxDialogBuilder.java @@ -18,14 +18,13 @@ import android.annotation.SuppressLint; import android.content.Context; -import android.support.v7.app.AlertDialog; import android.view.LayoutInflater; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.CheckBox; import android.widget.ListView; - +import androidx.appcompat.app.AlertDialog; import com.hippo.ehviewer.R; import com.hippo.yorozuya.ViewUtils; diff --git a/app/src/main/java/com/hippo/app/PrettyPreferenceActivity.java b/app/src/main/java/com/hippo/app/PrettyPreferenceActivity.java new file mode 100644 index 000000000..2ddb26b2c --- /dev/null +++ b/app/src/main/java/com/hippo/app/PrettyPreferenceActivity.java @@ -0,0 +1,113 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.app; + +import android.content.Context; +import android.preference.PreferenceActivity; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.TextView; +import androidx.annotation.NonNull; +import com.hippo.ehviewer.R; +import java.util.ArrayList; +import java.util.List; + +public abstract class PrettyPreferenceActivity extends AppCompatPreferenceActivity { + + @Override + public void setListAdapter(ListAdapter adapter) { + if (adapter == null) { + super.setListAdapter(null); + return; + } + + int count = adapter.getCount(); + List headers = new ArrayList<>(count); + for (int i = 0; i < count; ++i) { + headers.add((PreferenceActivity.Header) adapter.getItem(i)); + } + + super.setListAdapter(new HeaderAdapter(this, headers, R.layout.item_preference_header, true)); + } + + private static class HeaderAdapter extends ArrayAdapter { + private static class HeaderViewHolder { + ImageView icon; + TextView title; + TextView summary; + } + + private LayoutInflater mInflater; + private int mLayoutResId; + private boolean mRemoveIconIfEmpty; + + private HeaderAdapter(Context context, List objects, int layoutResId, + boolean removeIconBehavior) { + super(context, 0, objects); + mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mLayoutResId = layoutResId; + mRemoveIconIfEmpty = removeIconBehavior; + } + + @NonNull + @Override + public View getView(int position, View convertView, @NonNull ViewGroup parent) { + HeaderViewHolder holder; + View view; + + if (convertView == null) { + view = mInflater.inflate(mLayoutResId, parent, false); + holder = new HeaderViewHolder(); + holder.icon = view.findViewById(android.R.id.icon); + holder.title = view.findViewById(android.R.id.title); + holder.summary = view.findViewById(android.R.id.summary); + view.setTag(holder); + } else { + view = convertView; + holder = (HeaderViewHolder) view.getTag(); + } + + // All view fields must be updated every time, because the view may be recycled + PreferenceActivity.Header header = getItem(position); + if (mRemoveIconIfEmpty) { + if (header.iconRes == 0) { + holder.icon.setVisibility(View.GONE); + } else { + holder.icon.setVisibility(View.VISIBLE); + holder.icon.setImageResource(header.iconRes); + } + } else { + holder.icon.setImageResource(header.iconRes); + } + holder.title.setText(header.getTitle(getContext().getResources())); + CharSequence summary = header.getSummary(getContext().getResources()); + if (!TextUtils.isEmpty(summary)) { + holder.summary.setVisibility(View.VISIBLE); + holder.summary.setText(summary); + } else { + holder.summary.setVisibility(View.GONE); + } + + return view; + } + } +} diff --git a/app/src/main/java/com/hippo/app/ProgressDialog.java b/app/src/main/java/com/hippo/app/ProgressDialog.java index 93f093f6b..096dc048c 100644 --- a/app/src/main/java/com/hippo/app/ProgressDialog.java +++ b/app/src/main/java/com/hippo/app/ProgressDialog.java @@ -21,7 +21,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Message; -import android.support.v7.app.AlertDialog; import android.text.Spannable; import android.text.SpannableString; import android.text.style.StyleSpan; @@ -29,9 +28,8 @@ import android.view.View; import android.widget.ProgressBar; import android.widget.TextView; - +import androidx.appcompat.app.AlertDialog; import com.hippo.ehviewer.R; - import java.text.NumberFormat; /** diff --git a/app/src/main/java/com/hippo/content/ContextLocalWrapper.java b/app/src/main/java/com/hippo/content/ContextLocalWrapper.java new file mode 100644 index 000000000..87e2d5087 --- /dev/null +++ b/app/src/main/java/com/hippo/content/ContextLocalWrapper.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.content; + +/* + * Created by Hippo on 2018/3/27. + */ + +import android.content.Context; +import android.content.ContextWrapper; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; +import android.os.LocaleList; +import java.util.Locale; + +public class ContextLocalWrapper extends ContextWrapper { + + public ContextLocalWrapper(Context base) { + super(base); + } + + public static ContextLocalWrapper wrap(Context context, Locale newLocale) { + Resources res = context.getResources(); + Configuration configuration = res.getConfiguration(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + configuration.setLocale(newLocale); + + LocaleList localeList = new LocaleList(newLocale); + LocaleList.setDefault(localeList); + configuration.setLocales(localeList); + + context = context.createConfigurationContext(configuration); + + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + configuration.setLocale(newLocale); + context = context.createConfigurationContext(configuration); + + } else { + configuration.locale = newLocale; + res.updateConfiguration(configuration, res.getDisplayMetrics()); + } + + return new ContextLocalWrapper(context); + } +} diff --git a/app/src/main/java/com/hippo/content/FileProvider.java b/app/src/main/java/com/hippo/content/FileProvider.java index 3f233b34e..718b3920f 100644 --- a/app/src/main/java/com/hippo/content/FileProvider.java +++ b/app/src/main/java/com/hippo/content/FileProvider.java @@ -16,6 +16,9 @@ package com.hippo.content; +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.START_TAG; + import android.content.ClipData; import android.content.ContentProvider; import android.content.ContentValues; @@ -31,20 +34,15 @@ import android.os.Environment; import android.os.ParcelFileDescriptor; import android.provider.MediaStore; -import android.support.v4.content.ContextCompat; import android.text.TextUtils; import android.webkit.MimeTypeMap; - -import org.xmlpull.v1.XmlPullParserException; - +import androidx.core.content.ContextCompat; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.HashMap; import java.util.Map; - -import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; -import static org.xmlpull.v1.XmlPullParser.START_TAG; +import org.xmlpull.v1.XmlPullParserException; /** * FileProvider is a special subclass of {@link ContentProvider} that facilitates secure sharing @@ -331,7 +329,7 @@ public class FileProvider extends ContentProvider { }; private static final String - META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS"; + META_DATA_FILE_PROVIDER_PATHS = "com.hippo.content.FILE_PROVIDER_PATHS"; private static final String TAG_ROOT_PATH = "root-path"; private static final String TAG_FILES_PATH = "files-path"; diff --git a/app/src/main/java/com/hippo/content/RecordingApplication.java b/app/src/main/java/com/hippo/content/RecordingApplication.java new file mode 100644 index 000000000..76ff3a4ad --- /dev/null +++ b/app/src/main/java/com/hippo/content/RecordingApplication.java @@ -0,0 +1,90 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.content; + +/* + * Created by Hippo on 2018/3/27. + */ + +import android.app.Activity; +import android.os.Bundle; +import com.hippo.scene.SceneApplication; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +public abstract class RecordingApplication extends SceneApplication { + + private List> list = new LinkedList<>(); + + @Override + public void onCreate() { + super.onCreate(); + + registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + list.add(new java.lang.ref.WeakReference<>(activity)); + } + + @Override + public void onActivityDestroyed(Activity activity) { + Iterator> iterator = list.iterator(); + while (iterator.hasNext()) { + WeakReference reference = iterator.next(); + Activity a = reference.get(); + // Remove current activity and null + if (a == null || a == activity) { + iterator.remove(); + } + } + } + + @Override + public void onActivityStarted(Activity activity) {} + + @Override + public void onActivityResumed(Activity activity) {} + + @Override + public void onActivityPaused(Activity activity) {} + + @Override + public void onActivityStopped(Activity activity) {} + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} + }); + } + + public void recreate() { + // Copy list + ArrayList listCopy = new ArrayList<>(list.size()); + for (WeakReference reference: list) { + Activity activity = reference.get(); + if (activity == null) continue; + listCopy.add(activity); + } + + // Finish all activities + for (int i = listCopy.size() - 1; i >= 0; --i) { + listCopy.get(i).recreate(); + } + } +} diff --git a/app/src/main/java/com/hippo/drawable/AddDeleteDrawable.java b/app/src/main/java/com/hippo/drawable/AddDeleteDrawable.java index 0700fa23f..80c6daeae 100644 --- a/app/src/main/java/com/hippo/drawable/AddDeleteDrawable.java +++ b/app/src/main/java/com/hippo/drawable/AddDeleteDrawable.java @@ -46,13 +46,13 @@ public class AddDeleteDrawable extends Drawable { /** * @param context used to get the configuration for the drawable from */ - public AddDeleteDrawable(Context context) { + public AddDeleteDrawable(Context context, int color) { Resources resources = context.getResources(); mSize = resources.getDimensionPixelSize(R.dimen.add_size); float barThickness = Math.round(resources.getDimension(R.dimen.add_thickness)); - mPaint.setColor(resources.getColor(R.color.primary_drawable_light)); + mPaint.setColor(color); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeJoin(Paint.Join.MITER); mPaint.setStrokeCap(Paint.Cap.BUTT); diff --git a/app/src/main/java/com/hippo/drawable/BitmapPool.java b/app/src/main/java/com/hippo/drawable/BitmapPool.java index e0352d43b..6f6868ca9 100644 --- a/app/src/main/java/com/hippo/drawable/BitmapPool.java +++ b/app/src/main/java/com/hippo/drawable/BitmapPool.java @@ -17,9 +17,8 @@ package com.hippo.drawable; import android.graphics.Bitmap; -import android.support.annotation.Nullable; import android.util.Log; - +import androidx.annotation.Nullable; import java.lang.ref.WeakReference; import java.util.Iterator; import java.util.LinkedHashSet; diff --git a/app/src/main/java/com/hippo/drawable/DrawableWrapper.java b/app/src/main/java/com/hippo/drawable/DrawableWrapper.java new file mode 100644 index 000000000..7e6f1d86d --- /dev/null +++ b/app/src/main/java/com/hippo/drawable/DrawableWrapper.java @@ -0,0 +1,180 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.drawable; + +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.Drawable; +import androidx.annotation.NonNull; +import androidx.core.graphics.drawable.DrawableCompat; + +public class DrawableWrapper extends Drawable implements Drawable.Callback { + private Drawable mDrawable; + + public DrawableWrapper(Drawable drawable) { + this.setWrappedDrawable(drawable); + } + + public void draw(@NonNull Canvas canvas) { + this.mDrawable.draw(canvas); + } + + protected void onBoundsChange(Rect bounds) { + this.mDrawable.setBounds(bounds); + } + + public void setChangingConfigurations(int configs) { + this.mDrawable.setChangingConfigurations(configs); + } + + public int getChangingConfigurations() { + return this.mDrawable.getChangingConfigurations(); + } + + public void setDither(boolean dither) { + this.mDrawable.setDither(dither); + } + + public void setFilterBitmap(boolean filter) { + this.mDrawable.setFilterBitmap(filter); + } + + public void setAlpha(int alpha) { + this.mDrawable.setAlpha(alpha); + } + + public void setColorFilter(ColorFilter cf) { + this.mDrawable.setColorFilter(cf); + } + + public boolean isStateful() { + return this.mDrawable.isStateful(); + } + + public boolean setState(@NonNull int[] stateSet) { + return this.mDrawable.setState(stateSet); + } + + @NonNull + public int[] getState() { + return this.mDrawable.getState(); + } + + public void jumpToCurrentState() { + this.mDrawable.jumpToCurrentState(); + } + + @NonNull + public Drawable getCurrent() { + return this.mDrawable.getCurrent(); + } + + public boolean setVisible(boolean visible, boolean restart) { + return super.setVisible(visible, restart) || this.mDrawable.setVisible(visible, restart); + } + + public int getOpacity() { + return this.mDrawable.getOpacity(); + } + + public Region getTransparentRegion() { + return this.mDrawable.getTransparentRegion(); + } + + public int getIntrinsicWidth() { + return this.mDrawable.getIntrinsicWidth(); + } + + public int getIntrinsicHeight() { + return this.mDrawable.getIntrinsicHeight(); + } + + public int getMinimumWidth() { + return this.mDrawable.getMinimumWidth(); + } + + public int getMinimumHeight() { + return this.mDrawable.getMinimumHeight(); + } + + public boolean getPadding(@NonNull Rect padding) { + return this.mDrawable.getPadding(padding); + } + + public void invalidateDrawable(@NonNull Drawable who) { + this.invalidateSelf(); + } + + public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { + this.scheduleSelf(what, when); + } + + public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { + this.unscheduleSelf(what); + } + + protected boolean onLevelChange(int level) { + return this.mDrawable.setLevel(level); + } + + public void setAutoMirrored(boolean mirrored) { + DrawableCompat.setAutoMirrored(this.mDrawable, mirrored); + } + + public boolean isAutoMirrored() { + return DrawableCompat.isAutoMirrored(this.mDrawable); + } + + public void setTint(int tint) { + DrawableCompat.setTint(this.mDrawable, tint); + } + + public void setTintList(ColorStateList tint) { + DrawableCompat.setTintList(this.mDrawable, tint); + } + + public void setTintMode(@NonNull PorterDuff.Mode tintMode) { + DrawableCompat.setTintMode(this.mDrawable, tintMode); + } + + public void setHotspot(float x, float y) { + DrawableCompat.setHotspot(this.mDrawable, x, y); + } + + public void setHotspotBounds(int left, int top, int right, int bottom) { + DrawableCompat.setHotspotBounds(this.mDrawable, left, top, right, bottom); + } + + public Drawable getWrappedDrawable() { + return this.mDrawable; + } + + public void setWrappedDrawable(Drawable drawable) { + if (this.mDrawable != null) { + this.mDrawable.setCallback(null); + } + + this.mDrawable = drawable; + if (drawable != null) { + drawable.setCallback(this); + } + } +} diff --git a/app/src/main/java/com/hippo/drawable/DrawerArrowDrawable.java b/app/src/main/java/com/hippo/drawable/DrawerArrowDrawable.java index e7b429bb9..402475718 100644 --- a/app/src/main/java/com/hippo/drawable/DrawerArrowDrawable.java +++ b/app/src/main/java/com/hippo/drawable/DrawerArrowDrawable.java @@ -27,7 +27,7 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; - +import androidx.annotation.ColorInt; import com.hippo.ehviewer.R; import com.hippo.yorozuya.MathUtils; @@ -66,11 +66,11 @@ public class DrawerArrowDrawable extends Drawable { /** * @param context used to get the configuration for the drawable from */ - public DrawerArrowDrawable(Context context) { + public DrawerArrowDrawable(Context context, int color) { Resources resources = context.getResources(); mPaint.setAntiAlias(true); - mPaint.setColor(resources.getColor(R.color.primary_drawable_light)); + mPaint.setColor(color); mSize = resources.getDimensionPixelSize(R.dimen.dad_drawable_size); // round this because having this floating may cause bad measurements mBarSize = Math.round(resources.getDimension(R.dimen.dad_bar_size)); @@ -144,6 +144,11 @@ public void draw(Canvas canvas) { canvas.restore(); } + public void setColor(@ColorInt int color) { + mPaint.setColor(color); + invalidateSelf(); + } + @Override public void setAlpha(int i) { mPaint.setAlpha(i); diff --git a/app/src/main/java/com/hippo/drawable/PreciselyClipDrawable.java b/app/src/main/java/com/hippo/drawable/PreciselyClipDrawable.java index 043b69f7c..1a1261e07 100644 --- a/app/src/main/java/com/hippo/drawable/PreciselyClipDrawable.java +++ b/app/src/main/java/com/hippo/drawable/PreciselyClipDrawable.java @@ -20,7 +20,6 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; -import android.support.v7.graphics.drawable.DrawableWrapper; import com.hippo.yorozuya.MathUtils; diff --git a/app/src/main/java/com/hippo/drawable/TextDrawable.java b/app/src/main/java/com/hippo/drawable/TextDrawable.java deleted file mode 100644 index 4aefc9f12..000000000 --- a/app/src/main/java/com/hippo/drawable/TextDrawable.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2017 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.drawable; - -/* - * Created by Hippo on 2017/9/6. - */ - -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.ColorFilter; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.support.annotation.IntRange; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; - -public class TextDrawable extends Drawable { - - private static final float STANDARD_TEXT_SIZE = 1000.0f; - private static final Paint STANDARD_PAINT = new Paint(); - - static { - STANDARD_PAINT.setTextSize(STANDARD_TEXT_SIZE); - } - - private String text; - private float contentPercent; - - private Paint backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - private Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - - private int textColor = Color.BLACK; - private int backgroundColor = Color.BLACK; - - private float x = 0.0f; - private float y = 0.0f; - private boolean textSizeDirty = false; - private Rect textBounds = new Rect(); - - public TextDrawable(String text, float contentPercent) { - this.text = text; - this.contentPercent = contentPercent; - STANDARD_PAINT.getTextBounds(text, 0, text.length(), textBounds); - } - - public int getTextColor() { - return textColor; - } - - public void setTextColor(int textColor) { - if (this.textColor != textColor) { - this.textColor = textColor; - invalidateSelf(); - } - } - - public int getBackgroundColor() { - return backgroundColor; - } - - public void setBackgroundColor(int backgroundColor) { - if (this.backgroundColor != backgroundColor) { - this.backgroundColor = backgroundColor; - invalidateSelf(); - } - } - - @Override - protected void onBoundsChange(Rect bounds) { - super.onBoundsChange(bounds); - textSizeDirty = true; - } - - private void updateTextSizeIfDirty(Rect bounds) { - if (!textSizeDirty) { - return; - } - textSizeDirty = false; - - int contentWidth = (int) (bounds.width() * contentPercent); - int contentHeight = (int) (bounds.height() * contentPercent); - float widthRatio = (float) contentWidth / (float) textBounds.width(); - float heightRatio = (float) contentHeight / (float) textBounds.height(); - float ratio = Math.min(widthRatio, heightRatio); - float textSize = STANDARD_TEXT_SIZE * ratio; - textPaint.setTextSize(textSize); - x = (bounds.width() - textBounds.width() * ratio) / 2 - textBounds.left * ratio; - y = (bounds.height() - textBounds.height() * ratio) / 2 - textBounds.top * ratio; - } - - @Override - public void draw(@NonNull Canvas canvas) { - Rect bounds = getBounds(); - if (!bounds.isEmpty()) { - // Draw background - backgroundPaint.setColor(backgroundColor); - canvas.drawRect(bounds, backgroundPaint); - - if (!TextUtils.isEmpty(text)) { - // Draw text - updateTextSizeIfDirty(bounds); - textPaint.setColor(textColor); - canvas.drawText(text, x, y, textPaint); - } - } - } - - @Override - public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {} - - @Override - public void setColorFilter(@Nullable ColorFilter colorFilter) { - backgroundPaint.setColorFilter(colorFilter); - textPaint.setColorFilter(colorFilter); - invalidateSelf(); - } - - @Override - public int getOpacity() { - return PixelFormat.TRANSLUCENT; - } -} diff --git a/app/src/main/java/com/hippo/drawable/UnikeryDrawable.java b/app/src/main/java/com/hippo/drawable/UnikeryDrawable.java index d576be2bc..1353e6e41 100644 --- a/app/src/main/java/com/hippo/drawable/UnikeryDrawable.java +++ b/app/src/main/java/com/hippo/drawable/UnikeryDrawable.java @@ -17,9 +17,8 @@ package com.hippo.drawable; import android.graphics.drawable.Drawable; -import android.support.annotation.NonNull; import android.util.Log; - +import androidx.annotation.NonNull; import com.hippo.conaco.Conaco; import com.hippo.conaco.ConacoTask; import com.hippo.conaco.Unikery; diff --git a/app/src/main/java/com/hippo/ehviewer/Analytics.java b/app/src/main/java/com/hippo/ehviewer/Analytics.java index 561eeb012..00c023d02 100644 --- a/app/src/main/java/com/hippo/ehviewer/Analytics.java +++ b/app/src/main/java/com/hippo/ehviewer/Analytics.java @@ -17,15 +17,47 @@ package com.hippo.ehviewer; import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import com.google.firebase.analytics.FirebaseAnalytics; +import com.hippo.scene.SceneFragment; +import java.util.Locale; -import com.google.analytics.tracking.android.EasyTracker; +public final class Analytics { + private static final String DEVICE_LANGUAGE = "device_language"; -public final class Analytics { + private static FirebaseAnalytics analytics; private Analytics() {} public static void start(Context context) { - EasyTracker.getInstance(context).set("&uid", Settings.getUserID()); + analytics = FirebaseAnalytics.getInstance(context); + analytics.setUserId(Settings.getUserID()); + + Locale locale = Locale.getDefault(); + String language = locale.getLanguage(); + if (TextUtils.isEmpty(language)) { + language = "none"; + } + String country = locale.getCountry(); + if (!TextUtils.isEmpty(country)) { + language = language + "-" + country; + } + language = language.toLowerCase(); + analytics.setUserProperty(DEVICE_LANGUAGE, language); + } + + public static boolean isEnabled() { + return analytics != null && Settings.getEnableAnalytics(); + } + + public static void onSceneView(SceneFragment scene) { + if (isEnabled()) { + Bundle bundle = new Bundle(); + bundle.putString("scene_simple_class", scene.getClass().getSimpleName()); + bundle.putString("scene_class", scene.getClass().getName()); + analytics.logEvent("scene_view", bundle); + } } } diff --git a/app/src/main/java/com/hippo/ehviewer/AppConfig.java b/app/src/main/java/com/hippo/ehviewer/AppConfig.java index cb4df0d15..03a9f30fe 100644 --- a/app/src/main/java/com/hippo/ehviewer/AppConfig.java +++ b/app/src/main/java/com/hippo/ehviewer/AppConfig.java @@ -18,13 +18,11 @@ import android.content.Context; import android.os.Environment; -import android.support.annotation.Nullable; - +import androidx.annotation.Nullable; import com.hippo.ehviewer.client.exception.ParseException; import com.hippo.util.ReadableTime; import com.hippo.yorozuya.FileUtils; import com.hippo.yorozuya.IOUtils; - import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -37,10 +35,10 @@ public class AppConfig { private static final String DOWNLOAD = "download"; private static final String TEMP = "temp"; private static final String IMAGE = "image"; - private static final String CRASH = "crash"; private static final String PARSE_ERROR = "parse_error"; private static final String LOGCAT = "logcat"; private static final String DATA = "data"; + private static final String CRASH = "crash"; private static Context sContext; @@ -95,11 +93,6 @@ public static File getExternalImageDir() { return getDirInExternalAppDir(IMAGE); } - @Nullable - public static File getExternalCrashDir() { - return getDirInExternalAppDir(CRASH); - } - @Nullable public static File getExternalParseErrorDir() { return getDirInExternalAppDir(PARSE_ERROR); @@ -115,6 +108,11 @@ public static File getExternalDataDir() { return getDirInExternalAppDir(DATA); } + @Nullable + public static File getExternalCrashDir() { + return getDirInExternalAppDir(CRASH); + } + @Nullable public static File createExternalTempFile() { return FileUtils.createTempFile(getExternalTempDir(), null); @@ -162,4 +160,19 @@ public static void saveParseErrorBody(ParseException e) { IOUtils.closeQuietly(os); } } + + @Nullable + public static File getFilesDir(String name) { + File dir = sContext.getFilesDir(); + if (dir == null) { + return null; + } + + dir = new File(dir, name); + if (dir.isDirectory() || dir.mkdirs()) { + return dir; + } else { + return null; + } + } } diff --git a/app/src/main/java/com/hippo/ehviewer/Crash.java b/app/src/main/java/com/hippo/ehviewer/Crash.java index 13b08135c..ea486294a 100644 --- a/app/src/main/java/com/hippo/ehviewer/Crash.java +++ b/app/src/main/java/com/hippo/ehviewer/Crash.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 Hippo Seven + * Copyright 2019 Hippo Seven * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,184 +22,135 @@ import android.content.pm.PackageManager; import android.os.Build; import android.os.Debug; -import android.support.annotation.NonNull; - +import androidx.annotation.NonNull; import com.hippo.scene.StageActivity; import com.hippo.util.PackageUtils; import com.hippo.util.ReadableTime; import com.hippo.yorozuya.FileUtils; import com.hippo.yorozuya.IOUtils; import com.hippo.yorozuya.OSUtils; - import java.io.File; -import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; -import java.io.InputStream; import java.io.PrintWriter; public final class Crash { - private Crash() {} - - @NonNull - private static String avoidNull(String str) { - return null != str ? str : "null"; - } - - @SuppressWarnings("deprecation") - private static void collectInfo(Context context, FileWriter fw) throws IOException { - try { - PackageManager pm = context.getPackageManager(); - PackageInfo pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES); - if (pi != null) { - String versionName = pi.versionName == null ? "null" : pi.versionName; - String versionCode = String.valueOf(pi.versionCode); - fw.write("======== PackageInfo ========\r\n"); - fw.write("PackageName=");fw.write(pi.packageName);fw.write("\r\n"); - fw.write("VersionName=");fw.write(versionName);fw.write("\r\n"); - fw.write("VersionCode=");fw.write(versionCode);fw.write("\r\n"); - String signature = PackageUtils.getSignature(context, pi.packageName); - fw.write("Signature=");fw.write(null != signature ? signature : "null");fw.write("\r\n"); - fw.write("\r\n"); - } - } catch (PackageManager.NameNotFoundException e) { - fw.write("======== PackageInfo ========\r\n"); - fw.write("Can't get package information\r\n"); - fw.write("\r\n"); - } - - // Runtime - String topActivityClazzName = "null"; - String topSceneClazzName = "null"; - try { - Activity topActivity = ((EhApplication) context.getApplicationContext()).getTopActivity(); - if (null != topActivity) { - topActivityClazzName = topActivity.getClass().getName(); - if (topActivity instanceof StageActivity) { - Class clazz = ((StageActivity) topActivity).getTopSceneClass(); - if (clazz != null) { - topSceneClazzName = clazz.getName(); - } - } - } - } catch (Throwable e) { - // Ignore - } - fw.write("======== Runtime ========\r\n"); - fw.write("TopActivity=");fw.write(avoidNull(topActivityClazzName));fw.write("\r\n"); - fw.write("TopScene=");fw.write(avoidNull(topSceneClazzName));fw.write("\r\n"); - fw.write("\r\n"); - - // Device info - fw.write("======== DeviceInfo ========\r\n"); - fw.write("BOARD=");fw.write(Build.BOARD);fw.write("\r\n"); - fw.write("BOOTLOADER=");fw.write(Build.BOOTLOADER);fw.write("\r\n"); - fw.write("CPU_ABI=");fw.write(Build.CPU_ABI);fw.write("\r\n"); - fw.write("CPU_ABI2=");fw.write(Build.CPU_ABI2);fw.write("\r\n"); - fw.write("DEVICE=");fw.write(Build.DEVICE);fw.write("\r\n"); - fw.write("DISPLAY=");fw.write(Build.DISPLAY);fw.write("\r\n"); - fw.write("FINGERPRINT=");fw.write(Build.FINGERPRINT);fw.write("\r\n"); - fw.write("HARDWARE=");fw.write(Build.HARDWARE);fw.write("\r\n"); - fw.write("HOST=");fw.write(Build.HOST);fw.write("\r\n"); - fw.write("ID=");fw.write(Build.ID);fw.write("\r\n"); - fw.write("MANUFACTURER=");fw.write(Build.MANUFACTURER);fw.write("\r\n"); - fw.write("MODEL=");fw.write(Build.MODEL);fw.write("\r\n"); - fw.write("PRODUCT=");fw.write(Build.PRODUCT);fw.write("\r\n"); - fw.write("RADIO=");fw.write(Build.getRadioVersion());fw.write("\r\n"); - fw.write("SERIAL=");fw.write(Build.SERIAL);fw.write("\r\n"); - fw.write("TAGS=");fw.write(Build.TAGS);fw.write("\r\n"); - fw.write("TYPE=");fw.write(Build.TYPE);fw.write("\r\n"); - fw.write("USER=");fw.write(Build.USER);fw.write("\r\n"); - fw.write("CODENAME=");fw.write(Build.VERSION.CODENAME);fw.write("\r\n"); - fw.write("INCREMENTAL=");fw.write(Build.VERSION.INCREMENTAL);fw.write("\r\n"); - fw.write("RELEASE=");fw.write(Build.VERSION.RELEASE);fw.write("\r\n"); - fw.write("SDK=");fw.write(Integer.toString(Build.VERSION.SDK_INT));fw.write("\r\n"); - fw.write("MEMORY=");fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppAllocatedMemory(), false));fw.write("\r\n"); - fw.write("MEMORY_NATIVE=");fw.write(FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false));fw.write("\r\n"); - fw.write("MEMORY_MAX=");fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppMaxMemory(), false));fw.write("\r\n"); - fw.write("MEMORY_TOTAL=");fw.write(FileUtils.humanReadableByteCount(OSUtils.getTotalMemory(), false));fw.write("\r\n"); + private Crash() {} + + @NonNull + private static String avoidNull(String str) { + return null != str ? str : "null"; + } + + private static void collectInfo(Context context, FileWriter fw) throws IOException { + try { + PackageManager pm = context.getPackageManager(); + PackageInfo pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES); + if (pi != null) { + String versionName = pi.versionName == null ? "null" : pi.versionName; + String versionCode = String.valueOf(pi.versionCode); + fw.write("======== PackageInfo ========\r\n"); + fw.write("PackageName=");fw.write(pi.packageName);fw.write("\r\n"); + fw.write("VersionName=");fw.write(versionName);fw.write("\r\n"); + fw.write("VersionCode=");fw.write(versionCode);fw.write("\r\n"); + String signature = PackageUtils.getSignature(context, pi.packageName); + fw.write("Signature=");fw.write(null != signature ? signature : "null");fw.write("\r\n"); fw.write("\r\n"); + } + } catch (PackageManager.NameNotFoundException e) { + fw.write("======== PackageInfo ========\r\n"); + fw.write("Can't get package information\r\n"); + fw.write("\r\n"); } - public static void getThrowableInfo(Throwable t, FileWriter fw) { - PrintWriter printWriter = new PrintWriter(fw); - t.printStackTrace(printWriter); - Throwable cause = t.getCause(); - while (cause != null) { - cause.printStackTrace(printWriter); - cause = cause.getCause(); + // Runtime + String topActivityClazzName = "null"; + String topSceneClazzName = "null"; + try { + Activity topActivity = ((EhApplication) context.getApplicationContext()).getTopActivity(); + if (null != topActivity) { + topActivityClazzName = topActivity.getClass().getName(); + if (topActivity instanceof StageActivity) { + Class clazz = ((StageActivity) topActivity).getTopSceneClass(); + if (clazz != null) { + topSceneClazzName = clazz.getName(); + } } + } + } catch (Throwable e) { + // Ignore } - - public static void saveCrashInfo2File(Context context, Throwable ex) { - File dir = AppConfig.getExternalCrashDir(); - if (dir == null) { - return; - } - - String nowString = ReadableTime.getFilenamableTime(System.currentTimeMillis()); - String fileName = "crash-" + nowString + ".log"; - File file = new File(dir, fileName); - - FileWriter fw = null; - try { - fw = new FileWriter(file); - fw.write("TIME=");fw.write(nowString);fw.write("\r\n"); - fw.write("\r\n"); - collectInfo(context, fw); - fw.write("======== CrashInfo ========\r\n"); - getThrowableInfo(ex, fw); - fw.write("\r\n"); - - fw.flush(); - - Settings.putCrashFilename(fileName); - } catch (Exception e) { - file.delete(); - } finally { - IOUtils.closeQuietly(fw); - } - } - - public static boolean hasCrashFile() { - String filename = Settings.getCrashFilename(); - if (filename == null) { - return false; - } - - File dir = AppConfig.getExternalCrashDir(); - if (dir == null) { - return false; - } - - return new File(dir, filename).isFile(); + fw.write("======== Runtime ========\r\n"); + fw.write("TopActivity=");fw.write(avoidNull(topActivityClazzName));fw.write("\r\n"); + fw.write("TopScene=");fw.write(avoidNull(topSceneClazzName));fw.write("\r\n"); + fw.write("\r\n"); + + // Device info + fw.write("======== DeviceInfo ========\r\n"); + fw.write("BOARD=");fw.write(Build.BOARD);fw.write("\r\n"); + fw.write("BOOTLOADER=");fw.write(Build.BOOTLOADER);fw.write("\r\n"); + fw.write("CPU_ABI=");fw.write(Build.CPU_ABI);fw.write("\r\n"); + fw.write("CPU_ABI2=");fw.write(Build.CPU_ABI2);fw.write("\r\n"); + fw.write("DEVICE=");fw.write(Build.DEVICE);fw.write("\r\n"); + fw.write("DISPLAY=");fw.write(Build.DISPLAY);fw.write("\r\n"); + fw.write("FINGERPRINT=");fw.write(Build.FINGERPRINT);fw.write("\r\n"); + fw.write("HARDWARE=");fw.write(Build.HARDWARE);fw.write("\r\n"); + fw.write("HOST=");fw.write(Build.HOST);fw.write("\r\n"); + fw.write("ID=");fw.write(Build.ID);fw.write("\r\n"); + fw.write("MANUFACTURER=");fw.write(Build.MANUFACTURER);fw.write("\r\n"); + fw.write("MODEL=");fw.write(Build.MODEL);fw.write("\r\n"); + fw.write("PRODUCT=");fw.write(Build.PRODUCT);fw.write("\r\n"); + fw.write("RADIO=");fw.write(Build.getRadioVersion());fw.write("\r\n"); + fw.write("SERIAL=");fw.write(Build.SERIAL);fw.write("\r\n"); + fw.write("TAGS=");fw.write(Build.TAGS);fw.write("\r\n"); + fw.write("TYPE=");fw.write(Build.TYPE);fw.write("\r\n"); + fw.write("USER=");fw.write(Build.USER);fw.write("\r\n"); + fw.write("CODENAME=");fw.write(Build.VERSION.CODENAME);fw.write("\r\n"); + fw.write("INCREMENTAL=");fw.write(Build.VERSION.INCREMENTAL);fw.write("\r\n"); + fw.write("RELEASE=");fw.write(Build.VERSION.RELEASE);fw.write("\r\n"); + fw.write("SDK=");fw.write(Integer.toString(Build.VERSION.SDK_INT));fw.write("\r\n"); + fw.write("MEMORY=");fw.write( + FileUtils.humanReadableByteCount(OSUtils.getAppAllocatedMemory(), false));fw.write("\r\n"); + fw.write("MEMORY_NATIVE=");fw.write(FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false));fw.write("\r\n"); + fw.write("MEMORY_MAX=");fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppMaxMemory(), false));fw.write("\r\n"); + fw.write("MEMORY_TOTAL=");fw.write(FileUtils.humanReadableByteCount(OSUtils.getTotalMemory(), false));fw.write("\r\n"); + fw.write("\r\n"); + } + + private static void getThrowableInfo(Throwable t, FileWriter fw) { + PrintWriter printWriter = new PrintWriter(fw); + t.printStackTrace(printWriter); + Throwable cause = t.getCause(); + while (cause != null) { + cause.printStackTrace(printWriter); + cause = cause.getCause(); } + } - public static String getCrashContent() { - String filename = Settings.getCrashFilename(); - if (filename == null) { - return null; - } - - File dir = AppConfig.getExternalCrashDir(); - if (dir == null) { - return null; - } - - File file = new File(dir, filename); - - InputStream is = null; - try { - is = new FileInputStream(file); - return IOUtils.readString(is, "UTF-8"); - } catch (IOException e) { - return null; - } finally { - IOUtils.closeQuietly(is); - } + public static void saveCrashLog(Context context, Throwable t) { + File dir = AppConfig.getExternalCrashDir(); + if (dir == null) { + return; } - public static void resetCrashFile() { - Settings.putCrashFilename(null); + String nowString = ReadableTime.getFilenamableTime(System.currentTimeMillis()); + String fileName = "crash-" + nowString + ".log"; + File file = new File(dir, fileName); + + FileWriter fw = null; + try { + fw = new FileWriter(file); + fw.write("TIME=");fw.write(nowString);fw.write("\r\n"); + fw.write("\r\n"); + collectInfo(context, fw); + fw.write("======== CrashInfo ========\r\n"); + getThrowableInfo(t, fw); + fw.write("\r\n"); + fw.flush(); + } catch (Exception e) { + file.delete(); + } finally { + IOUtils.closeQuietly(fw); } + } } diff --git a/app/src/main/java/com/hippo/ehviewer/EhApplication.java b/app/src/main/java/com/hippo/ehviewer/EhApplication.java index 4eb273b9f..a91792385 100644 --- a/app/src/main/java/com/hippo/ehviewer/EhApplication.java +++ b/app/src/main/java/com/hippo/ehviewer/EhApplication.java @@ -16,21 +16,30 @@ package com.hippo.ehviewer; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.ComponentCallbacks2; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.os.AsyncTask; import android.os.Debug; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.util.LruCache; import android.util.Log; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.collection.LruCache; +import com.getkeepsafe.relinker.ReLinker; +import com.hippo.a7zip.A7Zip; +import com.hippo.a7zip.A7ZipExtractLite; import com.hippo.beerbelly.SimpleDiskCache; import com.hippo.conaco.Conaco; +import com.hippo.content.RecordingApplication; import com.hippo.ehviewer.client.EhClient; import com.hippo.ehviewer.client.EhCookieStore; +import com.hippo.ehviewer.client.EhDns; import com.hippo.ehviewer.client.EhEngine; import com.hippo.ehviewer.client.data.GalleryDetail; import com.hippo.ehviewer.download.DownloadManager; @@ -39,28 +48,28 @@ import com.hippo.image.Image; import com.hippo.image.ImageBitmap; import com.hippo.network.StatusCodeException; -import com.hippo.scene.SceneApplication; import com.hippo.text.Html; import com.hippo.unifile.UniFile; import com.hippo.util.BitmapUtils; +import com.hippo.util.ExceptionUtils; +import com.hippo.util.IoThreadPoolExecutor; import com.hippo.util.ReadableTime; import com.hippo.yorozuya.FileUtils; import com.hippo.yorozuya.IntIdGenerator; import com.hippo.yorozuya.OSUtils; import com.hippo.yorozuya.SimpleHandler; - import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.concurrent.TimeUnit; - import okhttp3.OkHttpClient; -public class EhApplication extends SceneApplication implements Thread.UncaughtExceptionHandler { +public class EhApplication extends RecordingApplication { private static final String TAG = EhApplication.class.getSimpleName(); + private static final String KEY_GLOBAL_STUFF_NEXT_ID = "global_stuff_next_id"; public static final boolean BETA = false; @@ -69,26 +78,48 @@ public class EhApplication extends SceneApplication implements Thread.UncaughtEx private static final boolean DEBUG_PRINT_IMAGE_COUNT = false; private static final long DEBUG_PRINT_INTERVAL = 3000L; - private Thread.UncaughtExceptionHandler mDefaultHandler; + private static EhApplication instance; private final IntIdGenerator mIdGenerator = new IntIdGenerator(); private final HashMap mGlobalStuffMap = new HashMap<>(); private EhCookieStore mEhCookieStore; private EhClient mEhClient; + private EhProxySelector mEhProxySelector; private OkHttpClient mOkHttpClient; private ImageBitmapHelper mImageBitmapHelper; private Conaco mConaco; private LruCache mGalleryDetailCache; private SimpleDiskCache mSpiderInfoCache; private DownloadManager mDownloadManager; + private Hosts mHosts; + private FavouriteStatusRouter mFavouriteStatusRouter; private final List mActivityList = new ArrayList<>(); + private boolean initialized = false; + + public static EhApplication getInstance() { + return instance; + } + + @SuppressLint("StaticFieldLeak") @Override public void onCreate() { - // Prepare to catch crash - mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); - Thread.setDefaultUncaughtExceptionHandler(this); + instance = this; + + Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler((t, e) -> { + try { + // Always save crash file if onCreate() is not done + if (!initialized || Settings.getSaveCrashLog()) { + Crash.saveCrashLog(instance, e); + } + } catch (Throwable ignored) { } + + if (handler != null) { + handler.uncaughtException(t, e); + } + }); super.onCreate(); @@ -102,6 +133,8 @@ public void onCreate() { EhDB.initialize(this); EhEngine.initialize(); BitmapUtils.initialize(this); + Image.initialize(this); + A7Zip.loadLibrary(A7ZipExtractLite.LIBRARY, libname -> ReLinker.loadLibrary(EhApplication.this, libname)); if (EhDB.needMerge()) { EhDB.mergeOldDB(this); @@ -111,16 +144,32 @@ public void onCreate() { Analytics.start(this); } - // Check no media file - UniFile downloadLocation = Settings.getDownloadLocation(); - if (Settings.getMediaScan()) { - CommonOperations.removeNoMediaFile(downloadLocation); - } else { - CommonOperations.ensureNoMediaFile(downloadLocation); - } + // Do io tasks in new thread + new AsyncTask() { + @Override + protected Void doInBackground(Void... voids) { + // Check no media file + try { + UniFile downloadLocation = Settings.getDownloadLocation(); + if (Settings.getMediaScan()) { + CommonOperations.removeNoMediaFile(downloadLocation); + } else { + CommonOperations.ensureNoMediaFile(downloadLocation); + } + } catch (Throwable t) { + ExceptionUtils.throwIfFatal(t); + } - // Clear temp dir - clearTempDir(); + // Clear temp files + try { + clearTempDir(); + } catch (Throwable t) { + ExceptionUtils.throwIfFatal(t); + } + + return null; + } + }.executeOnExecutor(IoThreadPoolExecutor.getInstance()); // Check app update update(); @@ -133,9 +182,13 @@ public void onCreate() { // Ignore } + mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0)); + if (DEBUG_PRINT_NATIVE_MEMORY || DEBUG_PRINT_IMAGE_COUNT) { debugPrint(); } + + initialized = true; } private void clearTempDir() { @@ -147,6 +200,9 @@ private void clearTempDir() { if (null != dir) { FileUtils.deleteContent(dir); } + + // Add .nomedia to external temp dir + CommonOperations.ensureNoMediaFile(UniFile.fromFile(AppConfig.getExternalTempDir())); } private void update() { @@ -193,6 +249,7 @@ public void run() { public int putGlobalStuff(@NonNull Object o) { int id = mIdGenerator.nextId(); mGlobalStuffMap.put(id, o); + Settings.putInt(KEY_GLOBAL_STUFF_NEXT_ID, mIdGenerator.nextId()); return id; } @@ -229,6 +286,15 @@ public static EhClient getEhClient(@NonNull Context context) { return application.mEhClient; } + @NonNull + public static EhProxySelector getEhProxySelector(@NonNull Context context) { + EhApplication application = ((EhApplication) context.getApplicationContext()); + if (application.mEhProxySelector == null) { + application.mEhProxySelector = new EhProxySelector(); + } + return application.mEhProxySelector; + } + @NonNull public static OkHttpClient getOkHttpClient(@NonNull Context context) { EhApplication application = ((EhApplication) context.getApplicationContext()); @@ -238,6 +304,8 @@ public static OkHttpClient getOkHttpClient(@NonNull Context context) { .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .cookieJar(getEhCookieStore(application)) + .dns(new EhDns(application)) + .proxySelector(getEhProxySelector(application)) .build(); } return application.mOkHttpClient; @@ -280,6 +348,12 @@ public static LruCache getGalleryDetailCache(@NonNull Conte if (application.mGalleryDetailCache == null) { // Max size 25, 3 min timeout application.mGalleryDetailCache = new LruCache<>(25); + getFavouriteStatusRouter().addListener((gid, slot) -> { + GalleryDetail gd = application.mGalleryDetailCache.get(gid); + if (gd != null) { + gd.favoriteSlot = slot; + } + }); } return application.mGalleryDetailCache; } @@ -294,6 +368,11 @@ public static SimpleDiskCache getSpiderInfoCache(@NonNull Context context) { return application.mSpiderInfoCache; } + @NonNull + public static DownloadManager getDownloadManager() { + return getDownloadManager(instance); + } + @NonNull public static DownloadManager getDownloadManager(@NonNull Context context) { EhApplication application = ((EhApplication) context.getApplicationContext()); @@ -303,32 +382,27 @@ public static DownloadManager getDownloadManager(@NonNull Context context) { return application.mDownloadManager; } - private boolean handleException(Throwable ex) { - if (ex == null) { - return false; - } - try { - ex.printStackTrace(); - Crash.saveCrashInfo2File(this, ex); - return true; - } catch (Throwable tr) { - return false; + @NonNull + public static Hosts getHosts(@NonNull Context context) { + EhApplication application = ((EhApplication) context.getApplicationContext()); + if (application.mHosts == null) { + application.mHosts = new Hosts(application, "hosts.db"); } + return application.mHosts; } - @Override - public void uncaughtException(Thread thread, Throwable ex) { - if (!handleException(ex) && mDefaultHandler != null) { - mDefaultHandler.uncaughtException(thread, ex); - } + @NonNull + public static FavouriteStatusRouter getFavouriteStatusRouter() { + return getFavouriteStatusRouter(getInstance()); + } - Activity activity = getTopActivity(); - if (activity != null) { - activity.finish(); + @NonNull + public static FavouriteStatusRouter getFavouriteStatusRouter(@NonNull Context context) { + EhApplication application = ((EhApplication) context.getApplicationContext()); + if (application.mFavouriteStatusRouter == null) { + application.mFavouriteStatusRouter = new FavouriteStatusRouter(); } - - android.os.Process.killProcess(android.os.Process.myPid()); - System.exit(1); + return application.mFavouriteStatusRouter; } @NonNull @@ -352,4 +426,36 @@ public Activity getTopActivity() { return null; } } + + // Avoid crash on some "energy saving" devices + @Override + public ComponentName startService(Intent service) { + try { + return super.startService(service); + } catch (Throwable t) { + ExceptionUtils.throwIfFatal(t); + return null; + } + } + + // Avoid crash on some "energy saving" devices + @Override + public boolean bindService(Intent service, ServiceConnection conn, int flags) { + try { + return super.bindService(service, conn, flags); + } catch (Throwable t) { + ExceptionUtils.throwIfFatal(t); + return false; + } + } + + // Avoid crash on some "energy saving" devices + @Override + public void unbindService(ServiceConnection conn) { + try { + super.unbindService(conn); + } catch (Throwable t) { + ExceptionUtils.throwIfFatal(t); + } + } } diff --git a/app/src/main/java/com/hippo/ehviewer/EhDB.java b/app/src/main/java/com/hippo/ehviewer/EhDB.java index 421d0e2ef..81d5868b4 100644 --- a/app/src/main/java/com/hippo/ehviewer/EhDB.java +++ b/app/src/main/java/com/hippo/ehviewer/EhDB.java @@ -20,10 +20,9 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.util.Log; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.hippo.ehviewer.client.data.GalleryInfo; import com.hippo.ehviewer.client.data.ListUrlBuilder; import com.hippo.ehviewer.dao.DaoMaster; @@ -43,12 +42,14 @@ import com.hippo.ehviewer.dao.QuickSearch; import com.hippo.ehviewer.dao.QuickSearchDao; import com.hippo.ehviewer.download.DownloadManager; +import com.hippo.util.ExceptionUtils; import com.hippo.util.SqlUtils; -import com.hippo.yorozuya.FileUtils; import com.hippo.yorozuya.IOUtils; import com.hippo.yorozuya.ObjectUtils; import com.hippo.yorozuya.collect.SparseJLArray; - +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.query.CloseableListIterator; +import de.greenrobot.dao.query.LazyList; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -58,8 +59,6 @@ import java.util.ArrayList; import java.util.List; -import de.greenrobot.dao.query.LazyList; - public class EhDB { private static final String TAG = EhDB.class.getSimpleName(); @@ -91,8 +90,36 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { private static void upgradeDB(SQLiteDatabase db, int oldVersion) { switch (oldVersion) { - case 1: // 1 to 2 + case 1: // 1 to 2, add FILTER FilterDao.createTable(db, true); + case 2: // 2 to 3, add ENABLE column to table FILTER + db.execSQL("CREATE TABLE " + "\"FILTER2\" (" + + "\"_id\" INTEGER PRIMARY KEY ," + + "\"MODE\" INTEGER NOT NULL ," + + "\"TEXT\" TEXT," + + "\"ENABLE\" INTEGER);"); + db.execSQL("INSERT INTO \"FILTER2\" (" + + "_id, MODE, TEXT, ENABLE)" + + "SELECT _id, MODE, TEXT, 1 FROM FILTER;"); + db.execSQL("DROP TABLE FILTER"); + db.execSQL("ALTER TABLE FILTER2 RENAME TO FILTER"); + case 3: // 3 to 4, add PAGE_FROM and PAGE_TO column to QUICK_SEARCH + db.execSQL("CREATE TABLE " + "\"QUICK_SEARCH2\" (" + + "\"_id\" INTEGER PRIMARY KEY ," + + "\"NAME\" TEXT," + + "\"MODE\" INTEGER NOT NULL ," + + "\"CATEGORY\" INTEGER NOT NULL ," + + "\"KEYWORD\" TEXT," + + "\"ADVANCE_SEARCH\" INTEGER NOT NULL ," + + "\"MIN_RATING\" INTEGER NOT NULL ," + + "\"PAGE_FROM\" INTEGER NOT NULL ," + + "\"PAGE_TO\" INTEGER NOT NULL ," + + "\"TIME\" INTEGER NOT NULL );"); + db.execSQL("INSERT INTO \"QUICK_SEARCH2\" (" + + "_id, NAME, MODE, CATEGORY, KEYWORD, ADVANCE_SEARCH, MIN_RATING, PAGE_FROM, PAGE_TO, TIME)" + + "SELECT _id, NAME, MODE, CATEGORY, KEYWORD, ADVANCE_SEARCH, MIN_RATING, -1, -1, TIME FROM QUICK_SEARCH;"); + db.execSQL("DROP TABLE QUICK_SEARCH"); + db.execSQL("ALTER TABLE QUICK_SEARCH2 RENAME TO QUICK_SEARCH"); } } @@ -143,7 +170,8 @@ public static void mergeOldDB(Context context) { SQLiteDatabase oldDB; try { oldDB = oldDBHelper.getReadableDatabase(); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); return; } @@ -165,7 +193,8 @@ public static void mergeOldDB(Context context) { try { // In 0.6.x version, NaN is stored gi.rating = cursor.getFloat(7); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); gi.rating = -1.0f; } @@ -176,7 +205,8 @@ public static void mergeOldDB(Context context) { } cursor.close(); } - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); // Ignore } @@ -206,7 +236,8 @@ public static void mergeOldDB(Context context) { } cursor.close(); } - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); // Ignore } @@ -242,7 +273,8 @@ public static void mergeOldDB(Context context) { } cursor.close(); } - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); // Ignore } @@ -283,7 +315,8 @@ public static void mergeOldDB(Context context) { } cursor.close(); } - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); // Ignore } @@ -312,13 +345,15 @@ public static void mergeOldDB(Context context) { } cursor.close(); } - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); // Ignore } try { oldDBHelper.close(); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); // Ignore } } @@ -606,30 +641,69 @@ public static synchronized void deleteFilter(Filter filter) { sDaoSession.getFilterDao().delete(filter); } - public static synchronized boolean exportDB(Context context, File file) { - File dbFile = context.getDatabasePath("eh.db"); - if (null == dbFile || !dbFile.isFile()) { - return false; - } - if (null == file || !FileUtils.ensureFile(file)) { + public static synchronized void triggerFilter(Filter filter) { + filter.setEnable(!filter.enable); + sDaoSession.getFilterDao().update(filter); + } + + private static boolean copyDao(AbstractDao from, AbstractDao to) { + try (CloseableListIterator iterator = from.queryBuilder().listIterator()) { + while (iterator.hasNext()) { + to.insert(iterator.next()); + } + } catch (IOException e) { return false; } - InputStream is = null; - OutputStream os = null; + return true; + } + + public static synchronized boolean exportDB(Context context, File file) { + final String ehExportName = "eh.export.db"; + + // Delete old export db + context.deleteDatabase(ehExportName); + + DBOpenHelper helper = new DBOpenHelper(context.getApplicationContext(), ehExportName, null); + try { - is = new FileInputStream(dbFile); - os = new FileOutputStream(file); - IOUtils.copy(is, os); - return true; - } catch (IOException e) { - e.printStackTrace(); + // Copy data to a export db + try (SQLiteDatabase db = helper.getWritableDatabase()) { + DaoMaster daoMaster = new DaoMaster(db); + DaoSession exportSession = daoMaster.newSession(); + if (!copyDao(sDaoSession.getDownloadsDao(), exportSession.getDownloadsDao())) return false; + if (!copyDao(sDaoSession.getDownloadLabelDao(), exportSession.getDownloadLabelDao())) return false; + if (!copyDao(sDaoSession.getDownloadDirnameDao(), exportSession.getDownloadDirnameDao())) return false; + if (!copyDao(sDaoSession.getHistoryDao(), exportSession.getHistoryDao())) return false; + if (!copyDao(sDaoSession.getQuickSearchDao(), exportSession.getQuickSearchDao())) return false; + if (!copyDao(sDaoSession.getLocalFavoritesDao(), exportSession.getLocalFavoritesDao())) return false; + if (!copyDao(sDaoSession.getBookmarksBao(), exportSession.getBookmarksBao())) return false; + if (!copyDao(sDaoSession.getFilterDao(), exportSession.getFilterDao())) return false; + } + + // Copy export db to data dir + File dbFile = context.getDatabasePath(ehExportName); + if (dbFile == null || !dbFile.isFile()) { + return false; + } + InputStream is = null; + OutputStream os = null; + try { + is = new FileInputStream(dbFile); + os = new FileOutputStream(file); + IOUtils.copy(is, os); + return true; + } catch (IOException e) { + e.printStackTrace(); + } finally { + IOUtils.closeQuietly(is); + IOUtils.closeQuietly(os); + } + // Delete failed file + file.delete(); + return false; } finally { - IOUtils.closeQuietly(is); - IOUtils.closeQuietly(os); + context.deleteDatabase(ehExportName); } - // Delete failed file - file.delete(); - return false; } /** @@ -708,7 +782,8 @@ public static synchronized String importDB(Context context, File file) { } return null; - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); // Ignore return context.getString(R.string.cant_read_the_file); } diff --git a/app/src/main/java/com/hippo/ehviewer/EhProxySelector.java b/app/src/main/java/com/hippo/ehviewer/EhProxySelector.java new file mode 100644 index 000000000..5a1177812 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/EhProxySelector.java @@ -0,0 +1,106 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer; + +import android.text.TextUtils; +import com.hippo.network.InetValidator; +import com.hippo.util.ExceptionUtils; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URI; +import java.util.Collections; +import java.util.List; + +public class EhProxySelector extends ProxySelector { + + public static final int TYPE_DIRECT = 0; + public static final int TYPE_SYSTEM = 1; + public static final int TYPE_HTTP = 2; + public static final int TYPE_SOCKS = 3; + + private ProxySelector delegation; + private ProxySelector alternative; + + EhProxySelector() { + alternative = ProxySelector.getDefault(); + if (alternative == null) { + alternative = new NullProxySelector(); + } + + updateProxy(); + } + + public void updateProxy() { + switch (Settings.getProxyType()) { + case TYPE_DIRECT: + delegation = new NullProxySelector(); + break; + default: + case TYPE_SYSTEM: + delegation = alternative; + break; + case TYPE_HTTP: + case TYPE_SOCKS: + delegation = null; + break; + } + } + + @Override + public List select(URI uri) { + int type = Settings.getProxyType(); + if (type == TYPE_HTTP || type == TYPE_SOCKS) { + try { + String ip = Settings.getProxyIp(); + int port = Settings.getProxyPort(); + if (!TextUtils.isEmpty(ip) && InetValidator.isValidInetPort(port)) { + InetAddress inetAddress = InetAddress.getByName(ip); + SocketAddress socketAddress = new InetSocketAddress(inetAddress, port); + return Collections.singletonList(new Proxy(type == TYPE_HTTP ? Proxy.Type.HTTP : Proxy.Type.SOCKS, socketAddress)); + } + } catch (Throwable t) { + ExceptionUtils.throwIfFatal(t); + } + } + + if (delegation != null) { + return delegation.select(uri); + } + + return alternative.select(uri); + } + + @Override + public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { + if (delegation != null) { + delegation.select(uri); + } + } + + private static class NullProxySelector extends ProxySelector { + @Override + public List select(URI uri) { + return Collections.singletonList(Proxy.NO_PROXY); + } + @Override + public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/FavouriteStatusRouter.java b/app/src/main/java/com/hippo/ehviewer/FavouriteStatusRouter.java new file mode 100644 index 000000000..e7bc4b9ad --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/FavouriteStatusRouter.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer; + +import android.annotation.SuppressLint; +import com.hippo.ehviewer.client.data.GalleryInfo; +import com.hippo.yorozuya.IntIdGenerator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class FavouriteStatusRouter { + + private static final String KEY_DATA_MAP_NEXT_ID = "data_map_next_id"; + + private final IntIdGenerator idGenerator = new IntIdGenerator(Settings.getInt(KEY_DATA_MAP_NEXT_ID, 0)); + @SuppressLint("UseSparseArrays") + private final HashMap> maps = new HashMap<>(); + + private List listeners = new ArrayList<>(); + + public int saveDataMap(Map map) { + int id = idGenerator.nextId(); + maps.put(id, map); + Settings.putInt(KEY_DATA_MAP_NEXT_ID, idGenerator.nextId()); + return id; + } + + public Map restoreDataMap(int id) { + return maps.remove(id); + } + + public void modifyFavourites(long gid, int slot) { + for (Map map : maps.values()) { + GalleryInfo info = map.get(gid); + if (info != null) { + info.favoriteSlot = slot; + } + } + + for (Listener listener : listeners) { + listener.onModifyFavourites(gid, slot); + } + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + public interface Listener { + void onModifyFavourites(long gid, int slot); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/GetText.java b/app/src/main/java/com/hippo/ehviewer/GetText.java index 02840d112..1ccd2d507 100644 --- a/app/src/main/java/com/hippo/ehviewer/GetText.java +++ b/app/src/main/java/com/hippo/ehviewer/GetText.java @@ -18,8 +18,8 @@ import android.content.Context; import android.content.res.Resources; -import android.support.annotation.NonNull; -import android.support.annotation.StringRes; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; public class GetText { diff --git a/app/src/main/java/com/hippo/ehviewer/Hosts.java b/app/src/main/java/com/hippo/ehviewer/Hosts.java new file mode 100644 index 000000000..76825a17b --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/Hosts.java @@ -0,0 +1,350 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer; + +/* + * Created by Hippo on 2018/3/21. + */ + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Pair; +import androidx.annotation.Nullable; +import com.hippo.database.MSQLiteBuilder; +import com.hippo.util.SqlUtils; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; + +public class Hosts { + + private static final int VERSION_1 = 1; + private static final String TABLE_HOSTS = "HOSTS"; + private static final String COLUMN_HOST = "HOST"; + private static final String COLUMN_IP = "IP"; + + private static final int DB_VERSION = VERSION_1; + + private final SQLiteOpenHelper helper; + private final SQLiteDatabase db; + + public Hosts(Context context, String name) { + helper = new MSQLiteBuilder() + .version(VERSION_1) + .createTable(TABLE_HOSTS) + .insertColumn(TABLE_HOSTS, COLUMN_HOST, String.class) + .insertColumn(TABLE_HOSTS, COLUMN_IP, String.class) + .build(context, name, DB_VERSION); + db = helper.getWritableDatabase(); + } + + /** + * Gets a InetAddress with the host. + */ + @Nullable + public InetAddress get(String host) { + if (!isValidHost(host)) { + return null; + } + + Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_HOSTS + " WHERE " + COLUMN_HOST + " = ?;", new String[] {host}); + try { + if (cursor.moveToNext()) { + String ip = SqlUtils.getString(cursor, COLUMN_IP, null); + return toInetAddress(host, ip); + } else { + return null; + } + } finally { + cursor.close(); + } + } + + private boolean contains(String host) { + Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_HOSTS + " WHERE " + COLUMN_HOST + " = ?;", new String[] {host}); + try { + return cursor.moveToNext(); + } finally { + cursor.close(); + } + } + + /** + * Puts the host-ip pair into this hosts. + */ + public boolean put(String host, String ip) { + if (!isValidHost(host) || !isValidIp(ip)) { + return false; + } + + ContentValues values = new ContentValues(); + values.put(COLUMN_HOST, host); + values.put(COLUMN_IP, ip); + + if (contains(host)) { + db.update(TABLE_HOSTS, values, COLUMN_HOST + " = ?", new String[] { host }); + } else { + db.insert(TABLE_HOSTS, null, values); + } + + return true; + } + + /** + * Puts delete the entry with the host. + */ + public void delete(String host) { + db.delete(TABLE_HOSTS, COLUMN_HOST + " = ?", new String[] { host }); + } + + /** + * Get all data from this host. + */ + public List> getAll() { + List> result = new ArrayList<>(); + + Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_HOSTS + ";", null); + try { + while (cursor.moveToNext()) { + String host = SqlUtils.getString(cursor, COLUMN_HOST, null); + String ip = SqlUtils.getString(cursor, COLUMN_IP, null); + + InetAddress inetAddress = toInetAddress(host, ip); + if (inetAddress == null) { + continue; + } + + result.add(new Pair<>(host, ip)); + } + } finally { + cursor.close(); + } + + return result; + } + + @Nullable + public static InetAddress toInetAddress(String host, String ip) { + if (!isValidHost(host)) { + return null; + } + + if (ip == null) { + return null; + } + + byte[] bytes = parseV4(ip); + if (bytes == null) { + bytes = parseV6(ip); + } + if (bytes == null) { + return null; + } + + try { + return InetAddress.getByAddress(host, bytes); + } catch (UnknownHostException e) { + return null; + } + } + + /** + * Returns true if the host is valid. + */ + public static boolean isValidHost(String host) { + if (host == null) { + return false; + } + + if (host.length() > 253) { + return false; + } + + int labelLength = 0; + for (int i = 0, n = host.length(); i < n; i++) { + char ch = host.charAt(i); + + if (ch == '.') { + if (labelLength < 1 || labelLength > 63) { + return false; + } + labelLength = 0; + } else { + labelLength++; + } + + if ((ch < 'a' || ch > 'z') && (ch < '0' || ch > '9') && ch != '-' && ch != '.') { + return false; + } + } + + if (labelLength < 1 || labelLength > 63) { + return false; + } + + return true; + } + + /** + * Returns true if the ip is valid. + */ + public static boolean isValidIp(String ip) { + return ip != null && (parseV4(ip) != null || parseV6(ip) != null); + } + + // org.xbill.DNS.Address.parseV4 + @Nullable + private static byte[] parseV4(String s) { + int numDigits; + int currentOctet; + byte [] values = new byte[4]; + int currentValue; + int length = s.length(); + + currentOctet = 0; + currentValue = 0; + numDigits = 0; + for (int i = 0; i < length; i++) { + char c = s.charAt(i); + if (c >= '0' && c <= '9') { + /* Can't have more than 3 digits per octet. */ + if (numDigits == 3) + return null; + /* Octets shouldn't start with 0, unless they are 0. */ + if (numDigits > 0 && currentValue == 0) + return null; + numDigits++; + currentValue *= 10; + currentValue += (c - '0'); + /* 255 is the maximum value for an octet. */ + if (currentValue > 255) + return null; + } else if (c == '.') { + /* Can't have more than 3 dots. */ + if (currentOctet == 3) + return null; + /* Two consecutive dots are bad. */ + if (numDigits == 0) + return null; + values[currentOctet++] = (byte) currentValue; + currentValue = 0; + numDigits = 0; + } else + return null; + } + /* Must have 4 octets. */ + if (currentOctet != 3) + return null; + /* The fourth octet can't be empty. */ + if (numDigits == 0) + return null; + values[currentOctet] = (byte) currentValue; + return values; + } + + // org.xbill.DNS.Address.parseV6 + @Nullable + private static byte[] parseV6(String s) { + int range = -1; + byte [] data = new byte[16]; + + String [] tokens = s.split(":", -1); + + int first = 0; + int last = tokens.length - 1; + + if (tokens[0].length() == 0) { + // If the first two tokens are empty, it means the string + // started with ::, which is fine. If only the first is + // empty, the string started with :, which is bad. + if (last - first > 0 && tokens[1].length() == 0) + first++; + else + return null; + } + + if (tokens[last].length() == 0) { + // If the last two tokens are empty, it means the string + // ended with ::, which is fine. If only the last is + // empty, the string ended with :, which is bad. + if (last - first > 0 && tokens[last - 1].length() == 0) + last--; + else + return null; + } + + if (last - first + 1 > 8) + return null; + + int i, j; + for (i = first, j = 0; i <= last; i++) { + if (tokens[i].length() == 0) { + if (range >= 0) + return null; + range = j; + continue; + } + + if (tokens[i].indexOf('.') >= 0) { + // An IPv4 address must be the last component + if (i < last) + return null; + // There can't have been more than 6 components. + if (i > 6) + return null; + byte [] v4addr = parseV4(tokens[i]); + if (v4addr == null) + return null; + for (int k = 0; k < 4; k++) + data[j++] = v4addr[k]; + break; + } + + try { + for (int k = 0; k < tokens[i].length(); k++) { + char c = tokens[i].charAt(k); + if (Character.digit(c, 16) < 0) + return null; + } + int x = Integer.parseInt(tokens[i], 16); + if (x > 0xFFFF || x < 0) + return null; + data[j++] = (byte)(x >>> 8); + data[j++] = (byte)(x & 0xFF); + } + catch (NumberFormatException e) { + return null; + } + } + + if (j < 16 && range < 0) + return null; + + if (range >= 0) { + int empty = 16 - j; + System.arraycopy(data, range, data, range + empty, j - range); + for (i = range; i < range + empty; i++) + data[i] = 0; + } + + return data; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ImageBitmapHelper.java b/app/src/main/java/com/hippo/ehviewer/ImageBitmapHelper.java index 519433741..c9040ed83 100644 --- a/app/src/main/java/com/hippo/ehviewer/ImageBitmapHelper.java +++ b/app/src/main/java/com/hippo/ehviewer/ImageBitmapHelper.java @@ -16,13 +16,11 @@ package com.hippo.ehviewer; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.hippo.conaco.ValueHelper; import com.hippo.image.ImageBitmap; import com.hippo.streampipe.InputStreamPipe; - import java.io.IOException; public class ImageBitmapHelper implements ValueHelper { diff --git a/app/src/main/java/com/hippo/ehviewer/Settings.java b/app/src/main/java/com/hippo/ehviewer/Settings.java index f41b95bcf..274318317 100755 --- a/app/src/main/java/com/hippo/ehviewer/Settings.java +++ b/app/src/main/java/com/hippo/ehviewer/Settings.java @@ -16,29 +16,28 @@ package com.hippo.ehviewer; -import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; import android.preference.PreferenceManager; -import android.support.annotation.DimenRes; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.util.Log; - +import androidx.annotation.DimenRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.hippo.ehviewer.client.EhConfig; import com.hippo.ehviewer.client.EhUtils; import com.hippo.ehviewer.client.data.FavListUrlBuilder; import com.hippo.ehviewer.ui.CommonOperations; +import com.hippo.ehviewer.ui.scene.GalleryListScene; import com.hippo.glgallery.GalleryView; import com.hippo.unifile.UniFile; +import com.hippo.util.ExceptionUtils; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.FileUtils; import com.hippo.yorozuya.MathUtils; import com.hippo.yorozuya.NumberUtils; - -import junit.framework.Assert; - import java.io.File; +import java.util.Locale; public class Settings { @@ -52,6 +51,16 @@ public static void initialize(Context context) { sContext = context.getApplicationContext(); sSettingsPre = PreferenceManager.getDefaultSharedPreferences(sContext); sEhConfig = loadEhConfig(); + fixDefaultValue(context); + } + + private static void fixDefaultValue(Context context) { + // Enable builtin hosts if the country is CN + if (!sSettingsPre.contains(KEY_BUILT_IN_HOSTS)) { + if ("CN".equals(Locale.getDefault().getCountry())) { + putBuiltInHosts(true); + } + } } private static EhConfig loadEhConfig() { @@ -237,6 +246,28 @@ public static void putQuickSearchTip(boolean value) { /******************** ****** Eh ********************/ + + public static final String KEY_THEME = "theme"; + public static final int THEME_LIGHT = 0; + public static final int THEME_DARK = 1; + public static final int THEME_BLACK = 2; + private static final int DEFAULT_THEME = THEME_LIGHT; + + public static int getTheme() { + return getIntFromStr(KEY_THEME, DEFAULT_THEME); + } + + public static void putTheme(int theme) { + putIntToStr(KEY_THEME, theme); + } + + public static final String KEY_APPLY_NAV_BAR_THEME_COLOR = "apply_nav_bar_theme_color"; + private static final boolean DEFAULT_APPLY_NAV_BAR_THEME_COLOR = true; + + public static boolean getApplyNavBarThemeColor() { + return getBoolean(KEY_APPLY_NAV_BAR_THEME_COLOR, DEFAULT_APPLY_NAV_BAR_THEME_COLOR); + } + public static final String KEY_GALLERY_SITE = "gallery_site"; private static final int DEFAULT_GALLERY_SITE = 1; @@ -248,6 +279,22 @@ public static void putGallerySite(int value) { putIntToStr(KEY_GALLERY_SITE, value); } + private static final String KEY_LAUNCH_PAGE = "launch_page"; + private static final int DEFAULT_LAUNCH_PAGE = 0; + + public static String getLaunchPageGalleryListSceneAction() { + int value = getIntFromStr(KEY_LAUNCH_PAGE, DEFAULT_LAUNCH_PAGE); + switch (value) { + default: + case 0: + return GalleryListScene.ACTION_HOMEPAGE; + case 1: + return GalleryListScene.ACTION_SUBSCRIPTION; + case 2: + return GalleryListScene.ACTION_WHATS_HOT; + } + } + public static final String KEY_LIST_MODE = "list_mode"; private static final int DEFAULT_LIST_MODE = 0; @@ -314,6 +361,20 @@ public static boolean getShowJpnTitle() { return getBoolean(KEY_SHOW_JPN_TITLE, DEFAULT_SHOW_JPN_TITLE); } + private static final String KEY_SHOW_GALLERY_PAGES = "show_gallery_pages"; + private static final boolean DEFAULT_SHOW_GALLERY_PAGES = false; + + public static boolean getShowGalleryPages() { + return getBoolean(KEY_SHOW_GALLERY_PAGES, DEFAULT_SHOW_GALLERY_PAGES); + } + + public static final String KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"; + private static final boolean DEFAULT_SHOW_TAG_TRANSLATIONS = false; + + public static boolean getShowTagTranslations() { + return getBoolean(KEY_SHOW_TAG_TRANSLATIONS, DEFAULT_SHOW_TAG_TRANSLATIONS); + } + public static final String KEY_DEFAULT_CATEGORIES = "default_categories"; public static final int DEFAULT_DEFAULT_CATEGORIES = EhUtils.ALL_CATEGORY; @@ -353,72 +414,6 @@ public static void putExcludedLanguages(String value) { putString(KEY_EXCLUDED_LANGUAGES, value); } - private static final String KEY_HATH_PROXY = "hath_proxy"; - private static final boolean DEFAULT_HATH_PROXY = false; - - public static boolean getHathProxy() { - return getBoolean(KEY_HATH_PROXY, DEFAULT_HATH_PROXY); - } - - public static void putHathProxy(boolean value) { - if (value) { - sEhConfig.hahClientIp = Settings.getHathIp(); - sEhConfig.hahClientPort = Settings.getHathPort(); - sEhConfig.hahClientPasskey = Settings.getHathPasskey(); - } else { - sEhConfig.hahClientIp = DEFAULT_HATH_IP; - sEhConfig.hahClientPort = DEFAULT_HATH_PORT; - sEhConfig.hahClientPasskey = DEFAULT_HATH_PASSKEY; - } - sEhConfig.setDirty(); - putBoolean(KEY_HATH_PROXY, value); - } - - private static final String KEY_HATH_IP = "hath_ip"; - private static final String DEFAULT_HATH_IP = null; - - public static String getHathIp() { - return getString(KEY_HATH_IP, DEFAULT_HATH_IP); - } - - public static void putHathIp(String value) { - if (Settings.getHathProxy()) { - sEhConfig.hahClientIp = value; - sEhConfig.setDirty(); - } - putString(KEY_HATH_IP, value); - } - - private static final String KEY_HATH_PORT = "hath_port"; - private static final int DEFAULT_HATH_PORT = -1; - - public static int getHathPort() { - return getInt(KEY_HATH_PORT, DEFAULT_HATH_PORT); - } - - public static void putHathPort(int value) { - if (Settings.getHathProxy()) { - sEhConfig.hahClientPort = value; - sEhConfig.setDirty(); - } - putInt(KEY_HATH_PORT, value); - } - - private static final String KEY_HATH_PASSKEY = "hath_passkey"; - private static final String DEFAULT_HATH_PASSKEY = null; - - public static String getHathPasskey() { - return getString(KEY_HATH_PASSKEY, DEFAULT_HATH_PASSKEY); - } - - public static void putHathPasskey(String value) { - if (Settings.getHathProxy()) { - sEhConfig.hahClientPasskey = value; - sEhConfig.setDirty(); - } - putString(KEY_HATH_PASSKEY, value); - } - private static final String KEY_CELLULAR_NETWORK_WARNING = "cellular_network_warning"; private static final boolean DEFAULT_CELLULAR_NETWORK_WARNING = false; @@ -520,6 +515,17 @@ public static void putShowBattery(boolean value) { putBoolean(KEY_SHOW_BATTERY, value); } + private static final String KEY_SHOW_PAGE_INTERVAL = "gallery_show_page_interval"; + private static final boolean DEFAULT_SHOW_PAGE_INTERVAL = true; + + public static boolean getShowPageInterval() { + return getBoolean(KEY_SHOW_PAGE_INTERVAL, DEFAULT_SHOW_PAGE_INTERVAL); + } + + public static void putShowPageInterval(boolean value) { + putBoolean(KEY_SHOW_PAGE_INTERVAL, value); + } + private static final String KEY_VOLUME_PAGE = "volume_page"; private static final boolean DEFAULT_VOLUME_PAGE = false; @@ -564,6 +570,19 @@ public static void putScreenLightness(int value) { putInt(KEY_SCREEN_LIGHTNESS, value); } + /******************** + ****** Privacy and Security + ********************/ + public static final String KEY_SEC_SECURITY = "enable_secure"; + public static final boolean VALUE_SEC_SECURITY = false; + + public static boolean getEnabledSecurity() { + return getBoolean(KEY_SEC_SECURITY, VALUE_SEC_SECURITY); + } + public static void putEnabledSecurity(boolean value) { + putBoolean(KEY_READING_FULLSCREEN, value); + } + /******************** ****** Download ********************/ @@ -584,7 +603,8 @@ public static UniFile getDownloadLocation() { builder.encodedQuery(getString(KEY_DOWNLOAD_SAVE_QUERY, null)); builder.encodedFragment(getString(KEY_DOWNLOAD_SAVE_FRAGMENT, null)); dir = UniFile.fromUri(sContext, builder.build()); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); // Ignore } return dir != null ? dir : UniFile.fromFile(AppConfig.getDefaultDownloadDir()); @@ -694,26 +714,42 @@ public static void putDownloadOriginImage(boolean value) { /******************** ****** Favorites ********************/ - public static final String KEY_FAV_CAT_0 = "fav_cat_0"; - public static final String KEY_FAV_CAT_1 = "fav_cat_1"; - public static final String KEY_FAV_CAT_2 = "fav_cat_2"; - public static final String KEY_FAV_CAT_3 = "fav_cat_3"; - public static final String KEY_FAV_CAT_4 = "fav_cat_4"; - public static final String KEY_FAV_CAT_5 = "fav_cat_5"; - public static final String KEY_FAV_CAT_6 = "fav_cat_6"; - public static final String KEY_FAV_CAT_7 = "fav_cat_7"; - public static final String KEY_FAV_CAT_8 = "fav_cat_8"; - public static final String KEY_FAV_CAT_9 = "fav_cat_9"; - public static final String DEFAULT_FAV_CAT_0 = "Favorites 0"; - public static final String DEFAULT_FAV_CAT_1 = "Favorites 1"; - public static final String DEFAULT_FAV_CAT_2 = "Favorites 2"; - public static final String DEFAULT_FAV_CAT_3 = "Favorites 3"; - public static final String DEFAULT_FAV_CAT_4 = "Favorites 4"; - public static final String DEFAULT_FAV_CAT_5 = "Favorites 5"; - public static final String DEFAULT_FAV_CAT_6 = "Favorites 6"; - public static final String DEFAULT_FAV_CAT_7 = "Favorites 7"; - public static final String DEFAULT_FAV_CAT_8 = "Favorites 8"; - public static final String DEFAULT_FAV_CAT_9 = "Favorites 9"; + private static final String KEY_FAV_CAT_0 = "fav_cat_0"; + private static final String KEY_FAV_CAT_1 = "fav_cat_1"; + private static final String KEY_FAV_CAT_2 = "fav_cat_2"; + private static final String KEY_FAV_CAT_3 = "fav_cat_3"; + private static final String KEY_FAV_CAT_4 = "fav_cat_4"; + private static final String KEY_FAV_CAT_5 = "fav_cat_5"; + private static final String KEY_FAV_CAT_6 = "fav_cat_6"; + private static final String KEY_FAV_CAT_7 = "fav_cat_7"; + private static final String KEY_FAV_CAT_8 = "fav_cat_8"; + private static final String KEY_FAV_CAT_9 = "fav_cat_9"; + private static final String DEFAULT_FAV_CAT_0 = "Favorites 0"; + private static final String DEFAULT_FAV_CAT_1 = "Favorites 1"; + private static final String DEFAULT_FAV_CAT_2 = "Favorites 2"; + private static final String DEFAULT_FAV_CAT_3 = "Favorites 3"; + private static final String DEFAULT_FAV_CAT_4 = "Favorites 4"; + private static final String DEFAULT_FAV_CAT_5 = "Favorites 5"; + private static final String DEFAULT_FAV_CAT_6 = "Favorites 6"; + private static final String DEFAULT_FAV_CAT_7 = "Favorites 7"; + private static final String DEFAULT_FAV_CAT_8 = "Favorites 8"; + private static final String DEFAULT_FAV_CAT_9 = "Favorites 9"; + + private static final String KEY_FAV_COUNT_0 = "fav_count_0"; + private static final String KEY_FAV_COUNT_1 = "fav_count_1"; + private static final String KEY_FAV_COUNT_2 = "fav_count_2"; + private static final String KEY_FAV_COUNT_3 = "fav_count_3"; + private static final String KEY_FAV_COUNT_4 = "fav_count_4"; + private static final String KEY_FAV_COUNT_5 = "fav_count_5"; + private static final String KEY_FAV_COUNT_6 = "fav_count_6"; + private static final String KEY_FAV_COUNT_7 = "fav_count_7"; + private static final String KEY_FAV_COUNT_8 = "fav_count_8"; + private static final String KEY_FAV_COUNT_9 = "fav_count_9"; + + private static final String KEY_FAV_LOCAL = "fav_local"; + private static final String KEY_FAV_CLOUD = "fav_cloud"; + + private static final int DEFAULT_FAV_COUNT = 0; public static String[] getFavCat() { String[] favCat = new String[10]; @@ -731,7 +767,7 @@ public static String[] getFavCat() { } public static void putFavCat(String[] value) { - Assert.assertEquals(10, value.length); + AssertUtils.assertEquals(10, value.length); sSettingsPre.edit() .putString(KEY_FAV_CAT_0, value[0]) .putString(KEY_FAV_CAT_1, value[1]) @@ -746,6 +782,53 @@ public static void putFavCat(String[] value) { .apply(); } + public static int[] getFavCount() { + int[] favCount = new int[10]; + favCount[0] = sSettingsPre.getInt(KEY_FAV_COUNT_0, DEFAULT_FAV_COUNT); + favCount[1] = sSettingsPre.getInt(KEY_FAV_COUNT_1, DEFAULT_FAV_COUNT); + favCount[2] = sSettingsPre.getInt(KEY_FAV_COUNT_2, DEFAULT_FAV_COUNT); + favCount[3] = sSettingsPre.getInt(KEY_FAV_COUNT_3, DEFAULT_FAV_COUNT); + favCount[4] = sSettingsPre.getInt(KEY_FAV_COUNT_4, DEFAULT_FAV_COUNT); + favCount[5] = sSettingsPre.getInt(KEY_FAV_COUNT_5, DEFAULT_FAV_COUNT); + favCount[6] = sSettingsPre.getInt(KEY_FAV_COUNT_6, DEFAULT_FAV_COUNT); + favCount[7] = sSettingsPre.getInt(KEY_FAV_COUNT_7, DEFAULT_FAV_COUNT); + favCount[8] = sSettingsPre.getInt(KEY_FAV_COUNT_8, DEFAULT_FAV_COUNT); + favCount[9] = sSettingsPre.getInt(KEY_FAV_COUNT_9, DEFAULT_FAV_COUNT); + return favCount; + } + + public static void putFavCount(int[] count) { + AssertUtils.assertEquals(10, count.length); + sSettingsPre.edit() + .putInt(KEY_FAV_COUNT_0, count[0]) + .putInt(KEY_FAV_COUNT_1, count[1]) + .putInt(KEY_FAV_COUNT_2, count[2]) + .putInt(KEY_FAV_COUNT_3, count[3]) + .putInt(KEY_FAV_COUNT_4, count[4]) + .putInt(KEY_FAV_COUNT_5, count[5]) + .putInt(KEY_FAV_COUNT_6, count[6]) + .putInt(KEY_FAV_COUNT_7, count[7]) + .putInt(KEY_FAV_COUNT_8, count[8]) + .putInt(KEY_FAV_COUNT_9, count[9]) + .apply(); + } + + public static int getFavLocalCount() { + return sSettingsPre.getInt(KEY_FAV_LOCAL, DEFAULT_FAV_COUNT); + } + + public static void putFavLocalCount(int count) { + sSettingsPre.edit().putInt(KEY_FAV_LOCAL, count).apply(); + } + + public static int getFavCloudCount() { + return sSettingsPre.getInt(KEY_FAV_CLOUD, DEFAULT_FAV_COUNT); + } + + public static void putFavCloudCount(int count) { + sSettingsPre.edit().putInt(KEY_FAV_CLOUD, count).apply(); + } + private static final String KEY_RECENT_FAV_CAT = "recent_fav_cat"; private static final int DEFAULT_RECENT_FAV_CAT = FavListUrlBuilder.FAV_CAT_ALL; @@ -862,6 +945,8 @@ private static boolean isValidUserID(@Nullable String userID) { ********************/ private static final String KEY_BETA_UPDATE_CHANNEL = "beta_update_channel"; private static final boolean DEFAULT_BETA_UPDATE_CHANNEL = EhApplication.BETA; + private static final String KEY_SKIP_UPDATE_VERSION = "skip_update_version"; + private static final int DEFAULT_SKIP_UPDATE_VERSION = 0; public static boolean getBetaUpdateChannel() { return getBoolean(KEY_BETA_UPDATE_CHANNEL, DEFAULT_BETA_UPDATE_CHANNEL); @@ -871,19 +956,12 @@ public static void putBetaUpdateChannel(boolean value) { putBoolean(KEY_BETA_UPDATE_CHANNEL, value); } - /******************** - ****** Crash - ********************/ - private static final String KEY_CRASH_FILENAME = "crash_filename"; - private static final String DEFAULT_CRASH_FILENAME = null; - - public static String getCrashFilename() { - return getString(KEY_CRASH_FILENAME, DEFAULT_CRASH_FILENAME); + public static int getSkipUpdateVersion() { + return getInt(KEY_SKIP_UPDATE_VERSION, DEFAULT_SKIP_UPDATE_VERSION); } - @SuppressLint("CommitPrefEdits") - public static void putCrashFilename(String value) { - sSettingsPre.edit().putString(KEY_CRASH_FILENAME, value).commit(); + public static void putSkipUpdateVersion(int value) { + putInt(KEY_SKIP_UPDATE_VERSION, value); } /******************** @@ -900,6 +978,13 @@ public static void putSaveParseErrorBody(boolean value) { putBoolean(KEY_SAVE_PARSE_ERROR_BODY, value); } + private static final String KEY_SAVE_CRASH_LOG = "save_crash_log"; + private static final boolean DEFAULT_SAVE_CRASH_LOG = false; + + public static boolean getSaveCrashLog() { + return getBoolean(KEY_SAVE_CRASH_LOG, DEFAULT_SAVE_CRASH_LOG); + } + public static final String KEY_SECURITY = "security"; public static final String DEFAULT_SECURITY = ""; @@ -928,6 +1013,61 @@ public static int getReadCacheSize() { return getIntFromStr(KEY_READ_CACHE_SIZE, DEFAULT_READ_CACHE_SIZE); } + public static final String KEY_BUILT_IN_HOSTS = "built_in_hosts"; + private static final boolean DEFAULT_BUILT_IN_HOSTS = false; + + public static boolean getBuiltInHosts() { + return getBoolean(KEY_BUILT_IN_HOSTS, DEFAULT_BUILT_IN_HOSTS); + } + + public static void putBuiltInHosts(boolean value) { + putBoolean(KEY_BUILT_IN_HOSTS, value); + } + + public static final String KEY_APP_LANGUAGE = "app_language"; + private static final String DEFAULT_APP_LANGUAGE = "system"; + + public static String getAppLanguage() { + return getString(KEY_APP_LANGUAGE, DEFAULT_APP_LANGUAGE); + } + + public static void putAppLanguage(String value) { + putString(KEY_APP_LANGUAGE, value); + } + + private static final String KEY_PROXY_TYPE = "proxy_type"; + private static final int DEFAULT_PROXY_TYPE = EhProxySelector.TYPE_SYSTEM; + + public static int getProxyType() { + return getInt(KEY_PROXY_TYPE, DEFAULT_PROXY_TYPE); + } + + public static void putProxyType(int value) { + putInt(KEY_PROXY_TYPE, value); + } + + private static final String KEY_PROXY_IP = "proxy_ip"; + private static final String DEFAULT_PROXY_IP = null; + + public static String getProxyIp() { + return getString(KEY_PROXY_IP, DEFAULT_PROXY_IP); + } + + public static void putProxyIp(String value) { + putString(KEY_PROXY_IP, value); + } + + private static final String KEY_PROXY_PORT = "proxy_port"; + private static final int DEFAULT_PROXY_PORT = -1; + + public static int getProxyPort() { + return getInt(KEY_PROXY_PORT, DEFAULT_PROXY_PORT); + } + + public static void putProxyPort(int value) { + putInt(KEY_PROXY_PORT, value); + } + /******************** ****** Guide ********************/ @@ -985,4 +1125,15 @@ public static boolean getGuideGallery() { public static void putGuideGallery(boolean value) { putBoolean(KEY_GUIDE_GALLERY, value); } + + private static final String KEY_CLIPBOARD_TEXT_HASH_CODE = "clipboard_text_hash_code"; + private static final int DEFAULT_CLIPBOARD_TEXT_HASH_CODE = 0; + + public static int getClipboardTextHashCode() { + return getInt(KEY_CLIPBOARD_TEXT_HASH_CODE, DEFAULT_CLIPBOARD_TEXT_HASH_CODE); + } + + public static void putClipboardTextHashCode(int value) { + putInt(KEY_CLIPBOARD_TEXT_HASH_CODE, value); + } } diff --git a/app/src/main/java/com/hippo/ehviewer/UrlOpener.java b/app/src/main/java/com/hippo/ehviewer/UrlOpener.java index 56a80f177..9b30f84ce 100644 --- a/app/src/main/java/com/hippo/ehviewer/UrlOpener.java +++ b/app/src/main/java/com/hippo/ehviewer/UrlOpener.java @@ -16,19 +16,18 @@ package com.hippo.ehviewer; -import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.provider.Browser; -import android.support.annotation.NonNull; import android.text.TextUtils; import android.widget.Toast; - +import androidx.annotation.NonNull; import com.hippo.ehviewer.client.EhUrlOpener; import com.hippo.ehviewer.ui.MainActivity; import com.hippo.scene.Announcer; import com.hippo.scene.StageActivity; +import com.hippo.util.ExceptionUtils; public final class UrlOpener { @@ -61,7 +60,8 @@ public static void openUrl(@NonNull Context context, String url, boolean ehUrl) intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); try { context.startActivity(intent); - } catch (ActivityNotFoundException e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); Toast.makeText(context, R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show(); } } diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhClient.java b/app/src/main/java/com/hippo/ehviewer/client/EhClient.java index 1ae3385be..32d06f287 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhClient.java +++ b/app/src/main/java/com/hippo/ehviewer/client/EhClient.java @@ -18,23 +18,15 @@ import android.content.Context; import android.os.AsyncTask; - import com.hippo.ehviewer.EhApplication; -import com.hippo.ehviewer.client.data.GalleryInfo; import com.hippo.ehviewer.client.exception.CancelledException; +import com.hippo.util.ExceptionUtils; +import com.hippo.util.IoThreadPoolExecutor; import com.hippo.yorozuya.SimpleHandler; -import com.hippo.yorozuya.thread.PriorityThreadFactory; - import java.io.File; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; - import okhttp3.Call; import okhttp3.OkHttpClient; @@ -44,7 +36,6 @@ public class EhClient { public static final int METHOD_SIGN_IN = 0; public static final int METHOD_GET_GALLERY_LIST = 1; - public static final int METHOD_FILL_GALLERY_LIST_BY_API = 2; public static final int METHOD_GET_GALLERY_DETAIL = 3; public static final int METHOD_GET_PREVIEW_SET = 4; public static final int METHOD_GET_RATE_GALLERY = 5; @@ -55,7 +46,6 @@ public class EhClient { public static final int METHOD_ADD_FAVORITES_RANGE = 10; public static final int METHOD_MODIFY_FAVORITES = 11; public static final int METHOD_GET_TORRENT_LIST = 12; - public static final int METHOD_GET_WHATS_HOT = 13; public static final int METHOD_GET_PROFILE = 14; public static final int METHOD_VOTE_COMMENT = 15; public static final int METHOD_IMAGE_SEARCH = 16; @@ -66,12 +56,7 @@ public class EhClient { private final OkHttpClient mOkHttpClient; public EhClient(Context context) { - int poolSize = 3; - BlockingQueue requestWorkQueue = new LinkedBlockingQueue<>(); - ThreadFactory threadFactory = new PriorityThreadFactory(TAG, - android.os.Process.THREAD_PRIORITY_BACKGROUND); - mRequestThreadPool = new ThreadPoolExecutor(poolSize, poolSize, - 1L, TimeUnit.SECONDS, requestWorkQueue, threadFactory); + mRequestThreadPool = IoThreadPoolExecutor.getInstance(); mOkHttpClient = EhApplication.getOkHttpClient(context); } @@ -153,11 +138,9 @@ protected Object doInBackground(Object... params) { try { switch (mMethod) { case METHOD_SIGN_IN: - return EhEngine.signIn(this, mOkHttpClient, (String) params[0], (String) params[1], (String) params[2], (String) params[3]); + return EhEngine.signIn(this, mOkHttpClient, (String) params[0], (String) params[1]); case METHOD_GET_GALLERY_LIST: return EhEngine.getGalleryList(this, mOkHttpClient, (String) params[0]); - case METHOD_FILL_GALLERY_LIST_BY_API: - return EhEngine.fillGalleryListByApi(this, mOkHttpClient, (List) params[0]); case METHOD_GET_GALLERY_DETAIL: return EhEngine.getGalleryDetail(this, mOkHttpClient, (String) params[0]); case METHOD_GET_PREVIEW_SET: @@ -165,7 +148,7 @@ protected Object doInBackground(Object... params) { case METHOD_GET_RATE_GALLERY: return EhEngine.rateGallery(this, mOkHttpClient, (Long) params[0], (String) params[1], (Long) params[2], (String) params[3], (Float) params[4]); case METHOD_GET_COMMENT_GALLERY: - return EhEngine.commentGallery(this, mOkHttpClient, (String) params[0], (String) params[1]); + return EhEngine.commentGallery(this, mOkHttpClient, (String) params[0], (String) params[1], (String) params[2]); case METHOD_GET_GALLERY_TOKEN: return EhEngine.getGalleryToken(this, mOkHttpClient, (Long) params[0], (String) params[1], (Integer) params[2]); case METHOD_GET_FAVORITES: @@ -177,9 +160,7 @@ protected Object doInBackground(Object... params) { case METHOD_MODIFY_FAVORITES: return EhEngine.modifyFavorites(this, mOkHttpClient, (String) params[0], (long[]) params[1], (Integer) params[2], (Boolean) params[3]); case METHOD_GET_TORRENT_LIST: - return EhEngine.getTorrentList(this, mOkHttpClient, (String) params[0]); - case METHOD_GET_WHATS_HOT: - return EhEngine.getWhatsHot(this, mOkHttpClient); + return EhEngine.getTorrentList(this, mOkHttpClient, (String) params[0], (Long) params[1], (String) params[2]); case METHOD_GET_PROFILE: return EhEngine.getProfile(this, mOkHttpClient); case METHOD_VOTE_COMMENT: @@ -187,13 +168,14 @@ protected Object doInBackground(Object... params) { case METHOD_IMAGE_SEARCH: return EhEngine.imageSearch(this, mOkHttpClient, (File) params[0], (Boolean) params[1], (Boolean) params[2], (Boolean) params[3]); case METHOD_ARCHIVE_LIST: - return EhEngine.getArchiveList(this, mOkHttpClient, (String) params[0]); + return EhEngine.getArchiveList(this, mOkHttpClient, (String) params[0], (Long) params[1], (String) params[2]); case METHOD_DOWNLOAD_ARCHIVE: return EhEngine.downloadArchive(this, mOkHttpClient, (Long) params[0], (String) params[1], (String) params[2], (String) params[3]); default: return new IllegalStateException("Can't detect method " + mMethod); } - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); return e; } } diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhConfig.java b/app/src/main/java/com/hippo/ehviewer/client/EhConfig.java index 600c5851c..026f93b39 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhConfig.java +++ b/app/src/main/java/com/hippo/ehviewer/client/EhConfig.java @@ -289,6 +289,7 @@ public class EhConfig implements Cloneable { public static final int ASIAN_PORN = 0x80; public static final int NON_H = 0x100; public static final int WESTERN = 0x200; + public static final int ALL_CATEGORY = 0x3ff; public static final int NAMESPACES_RECLASS = 0x1; public static final int NAMESPACES_LANGUAGE = 0x2; diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhCookieStore.java b/app/src/main/java/com/hippo/ehviewer/client/EhCookieStore.java index 089d43432..59f1daf14 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhCookieStore.java +++ b/app/src/main/java/com/hippo/ehviewer/client/EhCookieStore.java @@ -26,7 +26,6 @@ import okhttp3.Cookie; import okhttp3.HttpUrl; -import okhttp3.Request; public class EhCookieStore extends CookieRepository { @@ -86,41 +85,26 @@ public static Cookie newCookie(Cookie cookie, String newDomain, boolean forcePer } @Override - public List loadForRequest(HttpUrl url, Request request) { - List cookies = super.loadForRequest(url, request); - Object tag = request.tag(); + public List loadForRequest(HttpUrl url) { + List cookies = super.loadForRequest(url); boolean checkTips = domainMatch(url, EhUrl.DOMAIN_E); - boolean checkUconfig = (domainMatch(url, EhUrl.DOMAIN_E) || domainMatch(url, EhUrl.DOMAIN_EX)) && tag instanceof EhConfig; - if (checkTips || checkUconfig) { + if (checkTips) { List result = new ArrayList<>(cookies.size() + 1); // Add all but skip some for (Cookie cookie: cookies) { String name = cookie.name(); - if (checkTips && EhConfig.KEY_CONTENT_WARNING.equals(name)) { + if (EhConfig.KEY_CONTENT_WARNING.equals(name)) { continue; } - if (checkUconfig && EhConfig.KEY_UCONFIG.equals(name)) { + if (EhConfig.KEY_UCONFIG.equals(name)) { continue; } result.add(cookie); } // Add some - if (checkTips) { - result.add(sTipsCookie); - } - if (checkUconfig) { - EhConfig ehConfig = (EhConfig) tag; - Cookie uconfigCookie = new Cookie.Builder() - .name(EhConfig.KEY_UCONFIG) - .value(ehConfig.uconfig()) - .domain(url.host()) - .path("/") - .expiresAt(Long.MAX_VALUE) - .build(); - result.add(uconfigCookie); - } + result.add(sTipsCookie); return Collections.unmodifiableList(result); } else { return cookies; diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhDns.java b/app/src/main/java/com/hippo/ehviewer/client/EhDns.java new file mode 100644 index 000000000..f570fe00d --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/client/EhDns.java @@ -0,0 +1,90 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.client; + +/* + * Created by Hippo on 2018/3/23. + */ + +import android.content.Context; +import com.hippo.ehviewer.EhApplication; +import com.hippo.ehviewer.Hosts; +import com.hippo.ehviewer.Settings; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import okhttp3.Dns; + +public class EhDns implements Dns { + + private static final Map builtInHosts; + + static { + Map map = new HashMap<>(); + put(map, "e-hentai.org", "104.20.26.25"); + put(map, "repo.e-hentai.org", "94.100.29.73"); + put(map, "forums.e-hentai.org", "94.100.18.243"); + put(map, "ehgt.org", "81.171.14.118"); + put(map, "ul.ehgt.org", "94.100.24.82"); + put(map, "github.com", "192.30.255.112"); + put(map, "raw.githubusercontent.com", "151.101.0.133"); + builtInHosts = map; + } + + private static void put(Map map, String host, String ip) { + InetAddress address = Hosts.toInetAddress(host, ip); + if (address != null) { + map.put(host, address); + } + } + + private final Hosts hosts; + + public EhDns(Context context) { + hosts = EhApplication.getHosts(context); + } + + @Override + public List lookup(String hostname) throws UnknownHostException { + if (hostname == null) throw new UnknownHostException("hostname == null"); + + InetAddress inetAddress = hosts.get(hostname); + if (inetAddress != null) { + return Collections.singletonList(inetAddress); + } + + if (Settings.getBuiltInHosts()) { + inetAddress = builtInHosts.get(hostname); + if (inetAddress != null) { + return Collections.singletonList(inetAddress); + } + } + + try { + return Arrays.asList(InetAddress.getAllByName(hostname)); + } catch (NullPointerException e) { + UnknownHostException unknownHostException = + new UnknownHostException("Broken system behaviour for dns lookup of " + hostname); + unknownHostException.initCause(e); + throw unknownHostException; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhEngine.java b/app/src/main/java/com/hippo/ehviewer/client/EhEngine.java index f52595aad..7a48cf6d5 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhEngine.java +++ b/app/src/main/java/com/hippo/ehviewer/client/EhEngine.java @@ -16,16 +16,15 @@ package com.hippo.ehviewer.client; -import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import android.util.Pair; - +import androidx.annotation.Nullable; import com.hippo.ehviewer.AppConfig; import com.hippo.ehviewer.GetText; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; -import com.hippo.ehviewer.client.data.GalleryComment; +import com.hippo.ehviewer.client.data.GalleryCommentList; import com.hippo.ehviewer.client.data.GalleryDetail; import com.hippo.ehviewer.client.data.GalleryInfo; import com.hippo.ehviewer.client.data.PreviewSet; @@ -39,6 +38,7 @@ import com.hippo.ehviewer.client.parser.GalleryApiParser; import com.hippo.ehviewer.client.parser.GalleryDetailParser; import com.hippo.ehviewer.client.parser.GalleryListParser; +import com.hippo.ehviewer.client.parser.GalleryPageApiParser; import com.hippo.ehviewer.client.parser.GalleryPageParser; import com.hippo.ehviewer.client.parser.GalleryTokenApiParser; import com.hippo.ehviewer.client.parser.ProfileParser; @@ -46,22 +46,14 @@ import com.hippo.ehviewer.client.parser.SignInParser; import com.hippo.ehviewer.client.parser.TorrentParser; import com.hippo.ehviewer.client.parser.VoteCommentParser; -import com.hippo.ehviewer.client.parser.WhatsHotParser; import com.hippo.network.StatusCodeException; - -import junit.framework.Assert; - -import org.json.JSONArray; -import org.json.JSONObject; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; - +import com.hippo.util.ExceptionUtils; +import com.hippo.yorozuya.AssertUtils; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; - import okhttp3.Call; import okhttp3.FormBody; import okhttp3.Headers; @@ -71,6 +63,11 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; +import org.json.JSONArray; +import org.json.JSONObject; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.select.Elements; public class EhEngine { @@ -80,6 +77,8 @@ public class EhEngine { private static final String SAD_PANDA_TYPE = "image/gif"; private static final String SAD_PANDA_LENGTH = "9615"; + private static final String KOKOMADE_URL = "https://exhentai.org/img/kokomade.jpg"; + public static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json; charset=utf-8"); private static final MediaType MEDIA_TYPE_JPEG = MediaType.parse("image/jpeg"); @@ -91,8 +90,8 @@ public static void initialize() { sEhFilter = EhFilter.getInstance(); } - private static void throwException(Call call, int code, @Nullable Headers headers, - @Nullable String body, Exception e) throws Exception { + private static void doThrowException(Call call, int code, @Nullable Headers headers, + @Nullable String body, Throwable e) throws Throwable { if (call.isCanceled()) { throw new CancelledException(); } @@ -104,9 +103,16 @@ private static void throwException(Call call, int code, @Nullable Headers header throw new EhException("Sad Panda"); } + // Check kokomade + if (body != null && body.contains(KOKOMADE_URL)) { + throw new EhException("今回はここまで\n\n" + GetText.getString(R.string.kokomade_tip)); + } + if (e instanceof ParseException) { - if (body != null && !body.contains("<")){ + if (body != null && !body.contains("<")) { throw new EhException(body); + } else if (TextUtils.isEmpty(body)) { + throw new EhException(GetText.getString(R.string.error_empty_html)); } else { if (Settings.getSaveParseErrorBody()) { AppConfig.saveParseErrorBody((ParseException) e); @@ -118,23 +124,35 @@ private static void throwException(Call call, int code, @Nullable Headers header if (code >= 400) { throw new StatusCodeException(code); } + + if (e != null) { + throw e; + } + } + + private static void throwException(Call call, int code, @Nullable Headers headers, + @Nullable String body, Throwable e) throws Throwable { + try { + doThrowException(call, code, headers, body, e); + } catch (Throwable error) { + error.printStackTrace(); + throw error; + } } public static String signIn(@Nullable EhClient.Task task, OkHttpClient okHttpClient, - String username, String password, String recaptchaChallenge, String recaptchaResponse) throws Exception { + String username, String password) throws Throwable { FormBody.Builder builder = new FormBody.Builder() .add("UserName", username) .add("PassWord", password) .add("submit", "Log me in") .add("CookieDate", "1") .add("temporary_https", "off"); - if (!TextUtils.isEmpty(recaptchaChallenge) && !TextUtils.isEmpty(recaptchaResponse)) { - builder.add("recaptcha_challenge_field", recaptchaChallenge); - builder.add("recaptcha_response_field", recaptchaResponse); - } String url = EhUrl.API_SIGN_IN; + String referer = "https://forums.e-hentai.org/index.php?act=Login&CODE=00"; + String origin = "https://forums.e-hentai.org"; Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()) + Request request = new EhRequestBuilder(url, referer, origin) .post(builder.build()) .build(); Call call = okHttpClient.newCall(request); @@ -153,16 +171,71 @@ public static String signIn(@Nullable EhClient.Task task, OkHttpClient okHttpCli headers = response.headers(); body = response.body().string(); return SignInParser.parse(body); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } } + private static void fillGalleryList(@Nullable EhClient.Task task, OkHttpClient okHttpClient, List list, String url, boolean filter) throws Throwable { + // Filter title and uploader + if (filter) { + for (int i = 0, n = list.size(); i < n; i++) { + GalleryInfo info = list.get(i); + if (!sEhFilter.filterTitle(info) || !sEhFilter.filterUploader(info)) { + list.remove(i); + i--; + n--; + } + } + } + + boolean hasTags = false; + boolean hasPages = false; + boolean hasRated = false; + for (GalleryInfo gi : list) { + if (gi.simpleTags != null) { + hasTags = true; + } + if (gi.pages != 0) { + hasPages = true; + } + if (gi.rated) { + hasRated = true; + } + } + + boolean needApi = (filter && sEhFilter.needTags() && !hasTags) || + (Settings.getShowGalleryPages() && !hasPages) || + hasRated; + if (needApi) { + fillGalleryListByApi(task, okHttpClient, list, url); + } + + // Filter tag + if (filter) { + for (int i = 0, n = list.size(); i < n; i++) { + GalleryInfo info = list.get(i); + // Thumbnail mode need filter uploader again + if (!sEhFilter.filterUploader(info) || !sEhFilter.filterTag(info) || !sEhFilter.filterTagNamespace(info)) { + list.remove(i); + i--; + n--; + } + } + } + + for (GalleryInfo info : list) { + info.thumb = EhUrl.getFixedPreviewThumbUrl(info.thumb); + } + } + public static GalleryListParser.Result getGalleryList(@Nullable EhClient.Task task, OkHttpClient okHttpClient, - String url) throws Exception { + String url) throws Throwable { + String referer = EhUrl.getReferer(); Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()).build(); + Request request = new EhRequestBuilder(url, referer).build(); Call call = okHttpClient.newCall(request); // Put call @@ -180,54 +253,27 @@ public static GalleryListParser.Result getGalleryList(@Nullable EhClient.Task ta headers = response.headers(); body = response.body().string(); result = GalleryListParser.parse(body); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } - // Filter title and uploader - List list = result.galleryInfoList; - for (int i = 0, n = list.size(); i < n; i++) { - GalleryInfo info = list.get(i); - if (!sEhFilter.filterTitle(info) || !sEhFilter.filterUploader(info)) { - list.remove(i); - i--; - n--; - } - } - - if (list.size() > 0 && (Settings.getShowJpnTitle() || sEhFilter.needCallApi())) { - // Fill by api - fillGalleryListByApi(task, okHttpClient, list); - - // Filter tag - for (int i = 0, n = list.size(); i < n; i++) { - GalleryInfo info = list.get(i); - if (!sEhFilter.filterTag(info) || !sEhFilter.filterTagNamespace(info)) { - list.remove(i); - i--; - n--; - } - } - } - - for (GalleryInfo info : list) { - info.thumb = EhUrl.getFixedPreviewThumbUrl(info.thumb); - } + fillGalleryList(task, okHttpClient, result.galleryInfoList, url, true); return result; } // At least, GalleryInfo contain valid gid and token public static List fillGalleryListByApi(@Nullable EhClient.Task task, OkHttpClient okHttpClient, - List galleryInfoList) throws Exception { + List galleryInfoList, String referer) throws Throwable { // We can only request 25 items one time at most final int MAX_REQUEST_SIZE = 25; List requestItems = new ArrayList<>(MAX_REQUEST_SIZE); for (int i = 0, size = galleryInfoList.size(); i < size; i++) { requestItems.add(galleryInfoList.get(i)); if (requestItems.size() == MAX_REQUEST_SIZE || i == size - 1) { - doFillGalleryListByApi(task, okHttpClient, requestItems); + doFillGalleryListByApi(task, okHttpClient, requestItems, referer); requestItems.clear(); } } @@ -235,7 +281,7 @@ public static List fillGalleryListByApi(@Nullable EhClient.Task tas } private static void doFillGalleryListByApi(@Nullable EhClient.Task task, OkHttpClient okHttpClient, - List galleryInfoList) throws Exception { + List galleryInfoList, String referer) throws Throwable { JSONObject json = new JSONObject(); json.put("method", "gdata"); JSONArray ja = new JSONArray(); @@ -249,8 +295,9 @@ private static void doFillGalleryListByApi(@Nullable EhClient.Task task, OkHttpC json.put("gidlist", ja); json.put("namespace", 1); String url = EhUrl.getApiUrl(); + String origin = EhUrl.getOrigin(); Log.d(TAG, url); - Request request = new EhRequestBuilder(url) + Request request = new EhRequestBuilder(url, referer, origin) .post(RequestBody.create(MEDIA_TYPE_JSON, json.toString())) .build(); Call call = okHttpClient.newCall(request); @@ -269,16 +316,18 @@ private static void doFillGalleryListByApi(@Nullable EhClient.Task task, OkHttpC headers = response.headers(); body = response.body().string(); GalleryApiParser.parse(body, galleryInfoList); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } } public static GalleryDetail getGalleryDetail(@Nullable EhClient.Task task, OkHttpClient okHttpClient, - String url) throws Exception { + String url) throws Throwable { + String referer = EhUrl.getReferer(); Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()).build(); + Request request = new EhRequestBuilder(url, referer).build(); Call call = okHttpClient.newCall(request); // Put call @@ -295,7 +344,8 @@ public static GalleryDetail getGalleryDetail(@Nullable EhClient.Task task, OkHtt headers = response.headers(); body = response.body().string(); return GalleryDetailParser.parse(body); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } @@ -303,9 +353,10 @@ public static GalleryDetail getGalleryDetail(@Nullable EhClient.Task task, OkHtt public static Pair getPreviewSet( - @Nullable EhClient.Task task, OkHttpClient okHttpClient, String url) throws Exception { + @Nullable EhClient.Task task, OkHttpClient okHttpClient, String url) throws Throwable { + String referer = EhUrl.getReferer(); Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()).build(); + Request request = new EhRequestBuilder(url, referer).build(); Call call = okHttpClient.newCall(request); // Put call @@ -323,7 +374,8 @@ public static Pair getPreviewSet( body = response.body().string(); return Pair.create(GalleryDetailParser.parsePreviewSet(body), GalleryDetailParser.parsePreviewPages(body)); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } @@ -331,7 +383,7 @@ public static Pair getPreviewSet( public static RateGalleryParser.Result rateGallery(@Nullable EhClient.Task task, OkHttpClient okHttpClient, long apiUid, String apiKey, long gid, - String token, float rating) throws Exception { + String token, float rating) throws Throwable { final JSONObject json = new JSONObject(); json.put("method", "rategallery"); json.put("apiuid", apiUid); @@ -341,8 +393,10 @@ public static RateGalleryParser.Result rateGallery(@Nullable EhClient.Task task, json.put("rating", (int) Math.ceil(rating * 2)); final RequestBody requestBody = RequestBody.create(MEDIA_TYPE_JSON, json.toString()); String url = EhUrl.getApiUrl(); + String referer = EhUrl.getGalleryDetailUrl(gid, token); + String origin = EhUrl.getOrigin(); Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()) + Request request = new EhRequestBuilder(url, referer, origin) .post(requestBody) .build(); Call call = okHttpClient.newCall(request); @@ -361,19 +415,25 @@ public static RateGalleryParser.Result rateGallery(@Nullable EhClient.Task task, headers = response.headers(); body = response.body().string(); return RateGalleryParser.parse(body); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } } - public static GalleryComment[] commentGallery(@Nullable EhClient.Task task, - OkHttpClient okHttpClient, String url, String comment) throws Exception { - FormBody.Builder builder = new FormBody.Builder() - .add("commenttext", comment) - .add("postcomment", "Post New"); + public static GalleryCommentList commentGallery(@Nullable EhClient.Task task, + OkHttpClient okHttpClient, String url, String comment, String id) throws Throwable { + FormBody.Builder builder = new FormBody.Builder(); + if (id == null) { + builder.add("commenttext_new", comment); + } else { + builder.add("commenttext_edit", comment); + builder.add("edit_comment", id); + } + String origin = EhUrl.getOrigin(); Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()) + Request request = new EhRequestBuilder(url, url, origin) .post(builder.build()) .build(); Call call = okHttpClient.newCall(request); @@ -392,23 +452,32 @@ public static GalleryComment[] commentGallery(@Nullable EhClient.Task task, headers = response.headers(); body = response.body().string(); Document document = Jsoup.parse(body); + + Elements elements = document.select("#chd + p"); + if (elements.size() > 0) { + throw new EhException(elements.get(0).text()); + } + return GalleryDetailParser.parseComments(document); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } } public static String getGalleryToken(@Nullable EhClient.Task task, OkHttpClient okHttpClient, - long gid, String gtoken, int page) throws Exception { + long gid, String gtoken, int page) throws Throwable { JSONObject json = new JSONObject() .put("method", "gtoken") .put("pagelist", new JSONArray().put( new JSONArray().put(gid).put(gtoken).put(page + 1))); final RequestBody requestBody = RequestBody.create(MEDIA_TYPE_JSON, json.toString()); String url = EhUrl.getApiUrl(); + String referer = EhUrl.getReferer(); + String origin = EhUrl.getOrigin(); Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()) + Request request = new EhRequestBuilder(url, referer, origin) .post(requestBody) .build(); Call call = okHttpClient.newCall(request); @@ -427,16 +496,18 @@ public static String getGalleryToken(@Nullable EhClient.Task task, OkHttpClient headers = response.headers(); body = response.body().string(); return GalleryTokenApiParser.parse(body); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } } public static FavoritesParser.Result getFavorites(@Nullable EhClient.Task task, OkHttpClient okHttpClient, - String url, boolean callApi) throws Exception { + String url, boolean callApi) throws Throwable { + String referer = EhUrl.getReferer(); Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()).build(); + Request request = new EhRequestBuilder(url, referer).build(); Call call = okHttpClient.newCall(request); // Put call @@ -454,18 +525,13 @@ public static FavoritesParser.Result getFavorites(@Nullable EhClient.Task task, headers = response.headers(); body = response.body().string(); result = FavoritesParser.parse(body); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } - if (callApi && result.galleryInfoList.size() > 0) { - fillGalleryListByApi(task, okHttpClient, result.galleryInfoList); - } - - for (GalleryInfo info : result.galleryInfoList) { - info.thumb = EhUrl.getFixedPreviewThumbUrl(info.thumb); - } + fillGalleryList(task, okHttpClient, result.galleryInfoList, url, false); return result; } @@ -475,7 +541,7 @@ public static FavoritesParser.Result getFavorites(@Nullable EhClient.Task task, * @param note max 250 characters */ public static Void addFavorites(@Nullable EhClient.Task task, OkHttpClient okHttpClient, - long gid, String token, int dstCat, String note) throws Exception { + long gid, String token, int dstCat, String note) throws Throwable { String catStr; if (dstCat == -1) { catStr = "favdel"; @@ -491,8 +557,9 @@ public static Void addFavorites(@Nullable EhClient.Task task, OkHttpClient okHtt builder.add("submit", "Apply Changes"); builder.add("update", "1"); String url = EhUrl.getAddFavorites(gid, token); + String origin = EhUrl.getOrigin(); Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()) + Request request = new EhRequestBuilder(url, url, origin) .post(builder.build()) .build(); Call call = okHttpClient.newCall(request); @@ -511,7 +578,8 @@ public static Void addFavorites(@Nullable EhClient.Task task, OkHttpClient okHtt headers = response.headers(); body = response.body().string(); throwException(call, code, headers, body, null); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } @@ -520,8 +588,8 @@ public static Void addFavorites(@Nullable EhClient.Task task, OkHttpClient okHtt } public static Void addFavoritesRange(@Nullable EhClient.Task task, OkHttpClient okHttpClient, - long[] gidArray, String[] tokenArray, int dstCat) throws Exception { - Assert.assertEquals(gidArray.length, tokenArray.length); + long[] gidArray, String[] tokenArray, int dstCat) throws Throwable { + AssertUtils.assertEquals(gidArray.length, tokenArray.length); for (int i = 0, n = gidArray.length; i < n; i++) { addFavorites(task, okHttpClient, gidArray[i], tokenArray[i], dstCat, null); } @@ -529,7 +597,7 @@ public static Void addFavoritesRange(@Nullable EhClient.Task task, OkHttpClient } public static FavoritesParser.Result modifyFavorites(@Nullable EhClient.Task task, OkHttpClient okHttpClient, - String url, long[] gidArray, int dstCat, boolean callApi) throws Exception { + String url, long[] gidArray, int dstCat, boolean callApi) throws Throwable { String catStr; if (dstCat == -1) { catStr = "delete"; @@ -544,8 +612,9 @@ public static FavoritesParser.Result modifyFavorites(@Nullable EhClient.Task tas builder.add("modifygids[]", Long.toString(gid)); } builder.add("apply", "Apply"); + String origin = EhUrl.getOrigin(); Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()) + Request request = new EhRequestBuilder(url, url, origin) .post(builder.build()) .build(); Call call = okHttpClient.newCall(request); @@ -565,26 +634,22 @@ public static FavoritesParser.Result modifyFavorites(@Nullable EhClient.Task tas headers = response.headers(); body = response.body().string(); result = FavoritesParser.parse(body); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } - if (callApi && result.galleryInfoList.size() > 0) { - fillGalleryListByApi(task, okHttpClient, result.galleryInfoList); - } - - for (GalleryInfo info : result.galleryInfoList) { - info.thumb = EhUrl.getFixedPreviewThumbUrl(info.thumb); - } + fillGalleryList(task, okHttpClient, result.galleryInfoList, url, false); return result; } public static Pair[] getTorrentList(@Nullable EhClient.Task task, OkHttpClient okHttpClient, - String url) throws Exception { + String url, long gid, String token) throws Throwable { + String referer = EhUrl.getGalleryDetailUrl(gid, token); Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()).build(); + Request request = new EhRequestBuilder(url, referer).build(); Call call = okHttpClient.newCall(request); // Put call @@ -602,7 +667,8 @@ public static Pair[] getTorrentList(@Nullable EhClient.Task task headers = response.headers(); body = response.body().string(); result = TorrentParser.parse(body); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } @@ -611,9 +677,10 @@ public static Pair[] getTorrentList(@Nullable EhClient.Task task } public static Pair[]> getArchiveList(@Nullable EhClient.Task task, OkHttpClient okHttpClient, - String url) throws Exception { + String url, long gid, String token) throws Throwable { + String referer = EhUrl.getGalleryDetailUrl(gid, token); Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()).build(); + Request request = new EhRequestBuilder(url, referer).build(); Call call = okHttpClient.newCall(request); // Put call @@ -631,7 +698,8 @@ public static Pair[]> getArchiveList(@Nullable EhCl headers = response.headers(); body = response.body().string(); result = ArchiveParser.parse(body); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } @@ -640,7 +708,7 @@ public static Pair[]> getArchiveList(@Nullable EhCl } public static Void downloadArchive(@Nullable EhClient.Task task, OkHttpClient okHttpClient, - long gid, String token, String or, String res) throws Exception { + long gid, String token, String or, String res) throws Throwable { if (or == null || or.length() == 0) { throw new EhException("Invalid form param or: " + or); } @@ -650,8 +718,10 @@ public static Void downloadArchive(@Nullable EhClient.Task task, OkHttpClient ok FormBody.Builder builder = new FormBody.Builder(); builder.add("hathdl_xres", res); String url = EhUrl.getDownloadArchive(gid, token, or); + String referer = EhUrl.getGalleryDetailUrl(gid, token); + String origin = EhUrl.getOrigin(); Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()) + Request request = new EhRequestBuilder(url, referer, origin) .post(builder.build()) .build(); Call call = okHttpClient.newCall(request); @@ -670,7 +740,8 @@ public static Void downloadArchive(@Nullable EhClient.Task task, OkHttpClient ok headers = response.headers(); body = response.body().string(); throwException(call, code, headers, body, null); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } @@ -683,49 +754,10 @@ public static Void downloadArchive(@Nullable EhClient.Task task, OkHttpClient ok return null; } - public static List getWhatsHot(@Nullable EhClient.Task task, - OkHttpClient okHttpClient) throws Exception { - String url = EhUrl.HOST_E; - Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()).build(); - Call call = okHttpClient.newCall(request); - - // Put call - if (null != task) { - task.setCall(call); - } - - String body = null; - Headers headers = null; - List list; - int code = -1; - try { - Response response = call.execute(); - code = response.code(); - headers = response.headers(); - body = response.body().string(); - list = WhatsHotParser.parse(body); - } catch (Exception e) { - throwException(call, code, headers, body, e); - throw e; - } - - if (list.size() > 0) { - // Fill by api - fillGalleryListByApi(task, okHttpClient, list); - } - - for (GalleryInfo info : list) { - info.thumb = EhUrl.getFixedPreviewThumbUrl(info.thumb); - } - - return list; - } - private static ProfileParser.Result getProfileInternal(@Nullable EhClient.Task task, - OkHttpClient okHttpClient, String url) throws Exception { + OkHttpClient okHttpClient, String url, String referer) throws Throwable { Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()).build(); + Request request = new EhRequestBuilder(url, referer).build(); Call call = okHttpClient.newCall(request); // Put call @@ -742,17 +774,18 @@ private static ProfileParser.Result getProfileInternal(@Nullable EhClient.Task t headers = response.headers(); body = response.body().string(); return ProfileParser.parse(body); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } } public static ProfileParser.Result getProfile(@Nullable EhClient.Task task, - OkHttpClient okHttpClient) throws Exception { + OkHttpClient okHttpClient) throws Throwable { String url = EhUrl.URL_FORUMS; Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()).build(); + Request request = new EhRequestBuilder(url, null).build(); Call call = okHttpClient.newCall(request); // Put call @@ -768,15 +801,16 @@ public static ProfileParser.Result getProfile(@Nullable EhClient.Task task, code = response.code(); headers = response.headers(); body = response.body().string(); - return getProfileInternal(task, okHttpClient, ForumsParser.parse(body)); - } catch (Exception e) { + return getProfileInternal(task, okHttpClient, ForumsParser.parse(body), url); + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } } public static VoteCommentParser.Result voteComment(@Nullable EhClient.Task task, OkHttpClient okHttpClient, - long apiUid, String apiKey, long gid, String token, long commentId, int commentVote) throws Exception { + long apiUid, String apiKey, long gid, String token, long commentId, int commentVote) throws Throwable { final JSONObject json = new JSONObject(); json.put("method", "votecomment"); json.put("apiuid", apiUid); @@ -787,8 +821,10 @@ public static VoteCommentParser.Result voteComment(@Nullable EhClient.Task task, json.put("comment_vote", commentVote); final RequestBody requestBody = RequestBody.create(MEDIA_TYPE_JSON, json.toString()); String url = EhUrl.getApiUrl(); + String referer = EhUrl.getReferer(); + String origin = EhUrl.getOrigin(); Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()) + Request request = new EhRequestBuilder(url, referer, origin) .post(requestBody) .build(); Call call = okHttpClient.newCall(request); @@ -807,7 +843,8 @@ public static VoteCommentParser.Result voteComment(@Nullable EhClient.Task task, headers = response.headers(); body = response.body().string(); return VoteCommentParser.parse(body, commentVote); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } @@ -817,7 +854,7 @@ public static VoteCommentParser.Result voteComment(@Nullable EhClient.Task task, * @param image Must be jpeg */ public static GalleryListParser.Result imageSearch(@Nullable EhClient.Task task, OkHttpClient okHttpClient, - File image, boolean uss, boolean osc, boolean se) throws Exception { + File image, boolean uss, boolean osc, boolean se) throws Throwable { MultipartBody.Builder builder = new MultipartBody.Builder(); builder.setType(MultipartBody.FORM); builder.addPart( @@ -847,8 +884,10 @@ public static GalleryListParser.Result imageSearch(@Nullable EhClient.Task task, RequestBody.create(null, "File Search") ); String url = EhUrl.getImageSearchUrl(); + String referer = EhUrl.getReferer(); + String origin = EhUrl.getOrigin(); Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()) + Request request = new EhRequestBuilder(url, referer, origin) .post(builder.build()) .build(); Call call = okHttpClient.newCall(request); @@ -871,48 +910,64 @@ public static GalleryListParser.Result imageSearch(@Nullable EhClient.Task task, headers = response.headers(); body = response.body().string(); result = GalleryListParser.parse(body); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } - // Filter title and uploader - List list = result.galleryInfoList; - for (int i = 0, n = list.size(); i < n; i++) { - GalleryInfo info = list.get(i); - if (!sEhFilter.filterTitle(info) || !sEhFilter.filterUploader(info)) { - list.remove(i); - i--; - n--; - } - } + fillGalleryList(task, okHttpClient, result.galleryInfoList, url, true); - if (list.size() > 0 && (Settings.getShowJpnTitle() || sEhFilter.needCallApi())) { - // Fill by api - fillGalleryListByApi(task, okHttpClient, list); + return result; + } - // Filter tag - for (int i = 0, n = list.size(); i < n; i++) { - GalleryInfo info = list.get(i); - if (!sEhFilter.filterTag(info) || !sEhFilter.filterTagNamespace(info)) { - list.remove(i); - i--; - n--; - } - } - } + public static GalleryPageParser.Result getGalleryPage(@Nullable EhClient.Task task, + OkHttpClient okHttpClient, String url, long gid, String token) throws Throwable { + String referer = EhUrl.getGalleryDetailUrl(gid, token); + Log.d(TAG, url); + Request request = new EhRequestBuilder(url, referer).build(); + Call call = okHttpClient.newCall(request); - for (GalleryInfo info : list) { - info.thumb = EhUrl.getFixedPreviewThumbUrl(info.thumb); + // Put call + if (null != task) { + task.setCall(call); } - return result; + String body = null; + Headers headers = null; + int code = -1; + try { + Response response = call.execute(); + code = response.code(); + headers = response.headers(); + body = response.body().string(); + return GalleryPageParser.parse(body); + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); + throwException(call, code, headers, body, e); + throw e; + } } - public static GalleryPageParser.Result getGalleryPage(@Nullable EhClient.Task task, - OkHttpClient okHttpClient, String url) throws Exception { + public static GalleryPageApiParser.Result getGalleryPageApi(@Nullable EhClient.Task task, + OkHttpClient okHttpClient, long gid, int index, String pToken, String showKey, String previousPToken) throws Throwable { + final JSONObject json = new JSONObject(); + json.put("method", "showpage"); + json.put("gid", gid); + json.put("page", index + 1); + json.put("imgkey", pToken); + json.put("showkey", showKey); + final RequestBody requestBody = RequestBody.create(MEDIA_TYPE_JSON, json.toString()); + String url = EhUrl.getApiUrl(); + String referer = null; + if (index > 0 && previousPToken != null) { + referer = EhUrl.getPageUrl(gid, index - 1, previousPToken); + } + String origin = EhUrl.getOrigin(); Log.d(TAG, url); - Request request = new EhRequestBuilder(url, null != task ? task.getEhConfig() : Settings.getEhConfig()).build(); + Request request = new EhRequestBuilder(url, referer, origin) + .post(requestBody) + .build(); Call call = okHttpClient.newCall(request); // Put call @@ -928,8 +983,9 @@ public static GalleryPageParser.Result getGalleryPage(@Nullable EhClient.Task ta code = response.code(); headers = response.headers(); body = response.body().string(); - return GalleryPageParser.parse(body); - } catch (Exception e) { + return GalleryPageApiParser.parse(body); + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throwException(call, code, headers, body, e); throw e; } diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhFilter.java b/app/src/main/java/com/hippo/ehviewer/client/EhFilter.java index 62e19e543..f73531a40 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhFilter.java +++ b/app/src/main/java/com/hippo/ehviewer/client/EhFilter.java @@ -92,6 +92,8 @@ public List getTagNamespaceFilterList() { } public synchronized void addFilter(Filter filter) { + // enable filter by default before it is added to database + filter.enable = true; EhDB.addFilter(filter); switch (filter.mode) { @@ -116,6 +118,10 @@ public synchronized void addFilter(Filter filter) { } } + public synchronized void triggerFilter(Filter filter) { + EhDB.triggerFilter(filter); + } + public synchronized void deleteFilter(Filter filter) { EhDB.deleteFilter(filter); @@ -138,7 +144,7 @@ public synchronized void deleteFilter(Filter filter) { } } - public synchronized boolean needCallApi() { + public synchronized boolean needTags() { return 0 != mTagFilterList.size() || 0 != mTagNamespaceFilterList.size(); } @@ -151,9 +157,8 @@ public synchronized boolean filterTitle(GalleryInfo info) { String title = info.title; List filters = mTitleFilterList; if (null != title && filters.size() > 0) { - title = title.toLowerCase(); for (int i = 0, n = filters.size(); i < n; i++) { - if (title.contains(filters.get(i).text)) { + if (filters.get(i).enable && title.toLowerCase().contains(filters.get(i).text)) { return false; } } @@ -172,7 +177,7 @@ public synchronized boolean filterUploader(GalleryInfo info) { List filters = mUploaderFilterList; if (null != uploader && filters.size() > 0) { for (int i = 0, n = filters.size(); i < n; i++) { - if (uploader.equals(filters.get(i).text)) { + if (filters.get(i).enable && uploader.equals(filters.get(i).text)) { return false; } } @@ -229,7 +234,7 @@ public synchronized boolean filterTag(GalleryInfo info) { if (null != tags && filters.size() > 0) { for (String tag: tags) { for (int i = 0, n = filters.size(); i < n; i++) { - if (matchTag(tag, filters.get(i).text)) { + if (filters.get(i).enable && matchTag(tag, filters.get(i).text)) { return false; } } @@ -264,7 +269,7 @@ public synchronized boolean filterTagNamespace(GalleryInfo info) { if (null != tags && filters.size() > 0) { for (String tag: tags) { for (int i = 0, n = filters.size(); i < n; i++) { - if (matchTagNamespace(tag, filters.get(i).text)) { + if (filters.get(i).enable && matchTagNamespace(tag, filters.get(i).text)) { return false; } } diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhRequestBuilder.java b/app/src/main/java/com/hippo/ehviewer/client/EhRequestBuilder.java index 754cc789e..5e81f4979 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhRequestBuilder.java +++ b/app/src/main/java/com/hippo/ehviewer/client/EhRequestBuilder.java @@ -16,19 +16,21 @@ package com.hippo.ehviewer.client; -import com.hippo.ehviewer.Settings; import com.hippo.okhttp.ChromeRequestBuilder; -import java.net.MalformedURLException; - public class EhRequestBuilder extends ChromeRequestBuilder { - public EhRequestBuilder(String url) throws MalformedURLException { - this(url, Settings.getEhConfig()); + public EhRequestBuilder(String url, String referer) { + this(url, referer, null); } - public EhRequestBuilder(String url, EhConfig ehConfig) throws MalformedURLException { + public EhRequestBuilder(String url, String referer, String origin) { super(url); - tag(ehConfig); + if (referer != null) { + addHeader("Referer", referer); + } + if (origin != null) { + addHeader("Origin", origin); + } } } diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhTagDatabase.java b/app/src/main/java/com/hippo/ehviewer/client/EhTagDatabase.java new file mode 100644 index 000000000..f09dfca74 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/client/EhTagDatabase.java @@ -0,0 +1,357 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.client; + +import android.content.Context; +import android.util.Base64; +import androidx.annotation.Nullable; +import com.hippo.ehviewer.AppConfig; +import com.hippo.ehviewer.EhApplication; +import com.hippo.ehviewer.R; +import com.hippo.util.ExceptionUtils; +import com.hippo.util.IoThreadPoolExecutor; +import com.hippo.util.TextUrl; +import com.hippo.yorozuya.FileUtils; +import com.hippo.yorozuya.IOUtils; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import okhttp3.Call; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.BufferedSource; +import okio.Okio; + +public class EhTagDatabase { + + private final String name; + private final byte[] tags; + + public EhTagDatabase(String name, BufferedSource source) throws IOException { + this.name = name; + int totalBytes = source.readInt(); + tags = new byte[totalBytes]; + source.readFully(tags); + } + + public String getTranslation(String tag) { + return search(tags, tag.getBytes(TextUrl.UTF_8)); + } + + @Nullable + private String search(byte[] tags, byte[] tag) { + int low = 0; + int high = tags.length; + while (low < high) { + int start = (low + high) / 2; + // Look for the starting '\n' + while (start > -1 && tags[start] != '\n') { + start--; + } + start++; + + // Look for the middle '\r'. + int middle = 1; + while (tags[start + middle] != '\r') { + middle++; + } + + // Look for the ending '\n' + int end = middle + 1; + while (tags[start + end] != '\n') { + end++; + } + + int compare; + int tagIndex = 0; + int curIndex = start; + + for (;;) { + int tagByte = tag[tagIndex] & 0xff; + int curByte = tags[curIndex] & 0xff; + compare = tagByte - curByte; + if (compare != 0) { + break; + } + + tagIndex++; + curIndex++; + if (tagIndex == tag.length && curIndex == start + middle) { + break; + } + if (tagIndex == tag.length) { + compare = -1; + break; + } + if (curIndex == start + middle) { + compare = 1; + break; + } + } + + if (compare < 0) { + high = start - 1; + } else if (compare > 0) { + low = start + end + 1; + } else { + byte[] bytes = Base64.decode(tags, start + middle + 1, end - middle - 1, Base64.DEFAULT); + return new String(bytes, TextUrl.UTF_8); + } + } + return null; + } + + + private static final Map NAMESPACE_TO_PREFIX = new HashMap<>(); + + static { + NAMESPACE_TO_PREFIX.put("artist", "a:"); + NAMESPACE_TO_PREFIX.put("character", "c:"); + NAMESPACE_TO_PREFIX.put("female", "f:"); + NAMESPACE_TO_PREFIX.put("group", "g:"); + NAMESPACE_TO_PREFIX.put("language", "l:"); + NAMESPACE_TO_PREFIX.put("male", "m:"); + NAMESPACE_TO_PREFIX.put("misc", ""); + NAMESPACE_TO_PREFIX.put("parody", "p:"); + NAMESPACE_TO_PREFIX.put("reclass", "r:"); + } + + private static volatile EhTagDatabase instance; + // TODO more lock for different language + private static Lock lock = new ReentrantLock(); + + @Nullable + public static EhTagDatabase getInstance(Context context) { + if (isPossible(context)) { + return instance; + } else { + instance = null; + return null; + } + } + + @Nullable + public static String namespaceToPrefix(String namespace) { + return NAMESPACE_TO_PREFIX.get(namespace); + } + + private static String[] getMetadata(Context context) { + String[] metadata = context.getResources().getStringArray(R.array.tag_translation_metadata); + if (metadata.length == 4) { + return metadata; + } else { + return null; + } + } + + public static boolean isPossible(Context context) { + return getMetadata(context) != null; + } + + @Nullable + private static byte[] getFileContent(File file, int length) { + try (BufferedSource source = Okio.buffer(Okio.source(file))) { + byte[] content = new byte[length]; + source.readFully(content); + return content; + } catch (IOException e) { + return null; + } + } + + @Nullable + private static byte[] getFileSha1(File file) { + try (InputStream is = new FileInputStream(file)) { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + int n; + byte[] buffer = new byte[4 * 1024]; + while ((n = is.read(buffer)) != -1) { + digest.update(buffer, 0, n); + } + return digest.digest(); + } catch (IOException e) { + return null; + } catch (NoSuchAlgorithmException e) { + return null; + } + } + + private static boolean equals(byte[] b1, byte[] b2) { + if (b1 == null && b2 == null) { + return true; + } + if (b1 == null || b2 == null) { + return false; + } + + if (b1.length != b2.length) { + return false; + } + + for (int i = 0; i < b1.length; i++) { + if (b1[i] != b2[i]) { + return false; + } + } + + return true; + } + + private static boolean checkData(File sha1File, File dataFile) { + byte[] s1 = getFileContent(sha1File, 20); + if (s1 == null) { + return false; + } + + byte[] s2 = getFileSha1(dataFile); + if (s2 == null) { + return false; + } + + return equals(s1, s2); + } + + private static boolean save(OkHttpClient client, String url, File file) { + Request request = new Request.Builder().url(url).build(); + Call call = client.newCall(request); + try (Response response = call.execute()) { + if (!response.isSuccessful()) { + return false; + } + ResponseBody body = response.body(); + if (body == null) { + return false; + } + + try (InputStream is = body.byteStream(); OutputStream os = new FileOutputStream(file)) { + IOUtils.copy(is, os); + } + + return true; + } catch (Throwable t) { + ExceptionUtils.throwIfFatal(t); + return false; + } + } + + public static void update(Context context) { + String[] urls = getMetadata(context); + if (urls == null || urls.length != 4) { + // Clear tags if it's not possible + instance = null; + return; + } + + String sha1Name = urls[0]; + String sha1Url = urls[1]; + String dataName = urls[2]; + String dataUrl = urls[3]; + + // Clear tags if name if different + EhTagDatabase tmp = instance; + if (tmp != null && !tmp.name.equals(dataName)) { + instance = null; + } + + IoThreadPoolExecutor.getInstance().execute(() -> { + if (!lock.tryLock()) { + return; + } + + try { + File dir = AppConfig.getFilesDir("tag-translations"); + if (dir == null) { + return; + } + + // Check current sha1 and current data + File sha1File = new File(dir, sha1Name); + File dataFile = new File(dir, dataName); + if (!checkData(sha1File, dataFile)) { + FileUtils.delete(sha1File); + FileUtils.delete(dataFile); + } + + // Read current EhTagDatabase + if (instance == null && dataFile.exists()) { + try (BufferedSource source = Okio.buffer(Okio.source(dataFile))) { + instance = new EhTagDatabase(dataName, source); + } catch (IOException e) { + FileUtils.delete(sha1File); + FileUtils.delete(dataFile); + } + } + + OkHttpClient client = EhApplication.getOkHttpClient(EhApplication.getInstance()); + + // Save new sha1 + File tempSha1File = new File(dir, sha1Name + ".tmp"); + if (!save(client, sha1Url, tempSha1File)) { + FileUtils.delete(tempSha1File); + return; + } + + // Check new sha1 and current data + if (checkData(tempSha1File, dataFile)) { + // The data is the same + FileUtils.delete(tempSha1File); + return; + } + + // Save new data + File tempDataFile = new File(dir, dataName + ".tmp"); + if (!save(client, dataUrl, tempDataFile)) { + FileUtils.delete(tempDataFile); + return; + } + + // Check new sha1 and new data + if (!checkData(tempSha1File, tempDataFile)) { + FileUtils.delete(tempSha1File); + FileUtils.delete(tempDataFile); + return; + } + + // Replace current sha1 and current data with new sha1 and new data + FileUtils.delete(sha1File); + FileUtils.delete(dataFile); + tempSha1File.renameTo(sha1File); + tempDataFile.renameTo(dataFile); + + // Read new EhTagDatabase + try (BufferedSource source = Okio.buffer(Okio.source(dataFile))) { + instance = new EhTagDatabase(dataName, source); + } catch (IOException e) { + // Ignore + } + } finally { + lock.unlock(); + } + }); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhUrl.java b/app/src/main/java/com/hippo/ehviewer/client/EhUrl.java index d7e1bf2b8..81eff50ec 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhUrl.java +++ b/app/src/main/java/com/hippo/ehviewer/client/EhUrl.java @@ -16,14 +16,11 @@ package com.hippo.ehviewer.client; -import android.support.annotation.NonNull; - +import androidx.annotation.NonNull; import com.hippo.ehviewer.Settings; import com.hippo.network.UrlBuilder; - import java.util.List; import java.util.ListIterator; - import okhttp3.HttpUrl; public class EhUrl { @@ -43,6 +40,9 @@ public class EhUrl { public static final String API_E = HOST_E + "api.php"; public static final String API_EX = HOST_EX + "api.php"; + public static final String URL_POPULAR_E = "https://e-hentai.org/popular"; + public static final String URL_POPULAR_EX = "https://exhentai.org/popular"; + public static final String URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php"; public static final String URL_IMAGE_SEARCH_EX = "https://exhentai.org/upload/image_lookup.php"; @@ -52,7 +52,22 @@ public class EhUrl { public static final String URL_FAVORITES_EX = HOST_EX + "favorites.php"; public static final String URL_FORUMS = "https://forums.e-hentai.org/"; - private static final String URL_PREFIX_THUMB_E = "https://ehgt.org/t/"; + public static final String REFERER_EX = "https://" + DOMAIN_EX; + public static final String REFERER_E = "https://" + DOMAIN_E; + + public static final String ORIGIN_EX = REFERER_EX; + public static final String ORIGIN_E = REFERER_E; + + public static final String URL_UCONFIG_E = HOST_E + "uconfig.php"; + public static final String URL_UCONFIG_EX = HOST_EX + "uconfig.php"; + + public static final String URL_MY_TAGS_E = HOST_E + "mytags"; + public static final String URL_MY_TAGS_EX = HOST_EX + "mytags"; + + public static final String URL_WATCHED_E = HOST_E + "watched"; + public static final String URL_WATCHED_EX = HOST_EX + "watched"; + + private static final String URL_PREFIX_THUMB_E = "https://ehgt.org/"; private static final String URL_PREFIX_THUMB_EX = "https://exhentai.org/t/"; public static String getGalleryDetailUrl(long gid, String token) { @@ -89,6 +104,46 @@ public static String getApiUrl() { } } + public static String getReferer() { + switch (Settings.getGallerySite()) { + default: + case SITE_E: + return REFERER_E; + case SITE_EX: + return REFERER_EX; + } + } + + public static String getOrigin() { + switch (Settings.getGallerySite()) { + default: + case SITE_E: + return ORIGIN_E; + case SITE_EX: + return ORIGIN_EX; + } + } + + public static String getUConfigUrl() { + switch (Settings.getGallerySite()) { + default: + case SITE_E: + return URL_UCONFIG_E; + case SITE_EX: + return URL_UCONFIG_EX; + } + } + + public static String getMyTagsUrl() { + switch (Settings.getGallerySite()) { + default: + case SITE_E: + return URL_MY_TAGS_E; + case SITE_EX: + return URL_MY_TAGS_EX; + } + } + public static String getGalleryDetailUrl(long gid, String token, int index, boolean allComment) { UrlBuilder builder = new UrlBuilder(getHost() + "g/" + gid + '/' + token + '/'); if (index != 0) { @@ -116,6 +171,17 @@ public static String getTagDefinitionUrl(String tag) { return "https://ehwiki.org/wiki/" + tag.replace(' ', '_'); } + @NonNull + public static String getPopularUrl() { + switch (Settings.getGallerySite()) { + default: + case SITE_E: + return URL_POPULAR_E; + case SITE_EX: + return URL_POPULAR_EX; + } + } + @NonNull public static String getImageSearchUrl() { switch (Settings.getGallerySite()) { @@ -127,6 +193,17 @@ public static String getImageSearchUrl() { } } + @NonNull + public static String getWatchedUrl() { + switch (Settings.getGallerySite()) { + default: + case SITE_E: + return URL_WATCHED_E; + case SITE_EX: + return URL_WATCHED_EX; + } + } + public static String getThumbUrlPrefix() { switch (Settings.getGallerySite()) { default: diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhUrlOpener.java b/app/src/main/java/com/hippo/ehviewer/client/EhUrlOpener.java index 25ce4d795..a5dbdbdb3 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhUrlOpener.java +++ b/app/src/main/java/com/hippo/ehviewer/client/EhUrlOpener.java @@ -17,10 +17,9 @@ package com.hippo.ehviewer.client; import android.os.Bundle; -import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; - +import androidx.annotation.Nullable; import com.hippo.ehviewer.client.data.ListUrlBuilder; import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser; import com.hippo.ehviewer.client.parser.GalleryListUrlParser; diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhUtils.java b/app/src/main/java/com/hippo/ehviewer/client/EhUtils.java index 2d06243c3..2230e0220 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhUtils.java +++ b/app/src/main/java/com/hippo/ehviewer/client/EhUtils.java @@ -18,13 +18,11 @@ import android.content.Context; import android.graphics.Color; -import android.support.annotation.Nullable; import android.text.TextUtils; - +import androidx.annotation.Nullable; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.client.data.GalleryInfo; - import java.util.regex.Pattern; public class EhUtils { @@ -72,9 +70,9 @@ public class EhUtils { new String[] { "misc" }, new String[] { "doujinshi" }, new String[] { "manga" }, - new String[] { "artistcg", "Artist CG Sets" }, - new String[] { "gamecg", "Game CG Sets" }, - new String[] { "imageset", "Image Sets" }, + new String[] { "artistcg", "Artist CG Sets", "Artist CG" }, + new String[] { "gamecg", "Game CG Sets", "Game CG" }, + new String[] { "imageset", "Image Sets", "Image Set" }, new String[] { "cosplay" }, new String[] { "asianporn", "Asian Porn" }, new String[] { "non-h" }, diff --git a/app/src/main/java/com/hippo/ehviewer/client/data/FavListUrlBuilder.java b/app/src/main/java/com/hippo/ehviewer/client/data/FavListUrlBuilder.java index 519b06c3d..68349846d 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/data/FavListUrlBuilder.java +++ b/app/src/main/java/com/hippo/ehviewer/client/data/FavListUrlBuilder.java @@ -69,11 +69,18 @@ public String build() { UrlBuilder ub = new UrlBuilder(EhUrl.getFavoritesUrl()); if (isValidFavCat(mFavCat)) { ub.addQuery("favcat", Integer.toString(mFavCat)); + } else if (mFavCat == FAV_CAT_ALL) { + ub.addQuery("favcat", "all"); } if (!TextUtils.isEmpty(mKeyword)) { try { ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8")); - ub.addQuery("f_apply", "Search+Favorites"); + // Name + ub.addQuery("sn", "on"); + // Tags + ub.addQuery("st", "on"); + // Note + ub.addQuery("sf", "on"); } catch (UnsupportedEncodingException e) { Log.e(TAG, "Can't URLEncoder.encode " + mKeyword); } diff --git a/app/src/main/java/com/hippo/ehviewer/client/data/GalleryApiInfo.java b/app/src/main/java/com/hippo/ehviewer/client/data/GalleryApiInfo.java index f6e143684..2286faf5f 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/data/GalleryApiInfo.java +++ b/app/src/main/java/com/hippo/ehviewer/client/data/GalleryApiInfo.java @@ -18,7 +18,7 @@ import android.os.Parcel; import android.os.Parcelable; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; public class GalleryApiInfo implements Parcelable { diff --git a/app/src/main/java/com/hippo/ehviewer/client/data/GalleryComment.java b/app/src/main/java/com/hippo/ehviewer/client/data/GalleryComment.java index 66ddf2889..04296bbf7 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/data/GalleryComment.java +++ b/app/src/main/java/com/hippo/ehviewer/client/data/GalleryComment.java @@ -24,12 +24,16 @@ public class GalleryComment implements Parcelable { // 0 for uploader comment. can't vote public long id; public int score; - public boolean voteUp; - public boolean voteDown; + public boolean editable; + public boolean voteUpAble; + public boolean voteUpEd; + public boolean voteDownAble; + public boolean voteDownEd; public String voteState; public long time; public String user; public String comment; + public long lastEdited; @Override public int describeContents() { @@ -40,12 +44,16 @@ public int describeContents() { public void writeToParcel(Parcel dest, int flags) { dest.writeLong(this.id); dest.writeInt(this.score); - dest.writeByte(voteUp ? (byte) 1 : (byte) 0); - dest.writeByte(voteDown ? (byte) 1 : (byte) 0); + dest.writeByte(editable ? (byte) 1 : (byte) 0); + dest.writeByte(voteUpAble ? (byte) 1 : (byte) 0); + dest.writeByte(voteUpEd ? (byte) 1 : (byte) 0); + dest.writeByte(voteDownAble ? (byte) 1 : (byte) 0); + dest.writeByte(voteDownEd ? (byte) 1 : (byte) 0); dest.writeString(this.voteState); dest.writeLong(this.time); dest.writeString(this.user); dest.writeString(this.comment); + dest.writeLong(this.lastEdited); } public GalleryComment() { @@ -54,12 +62,16 @@ public GalleryComment() { protected GalleryComment(Parcel in) { this.id = in.readLong(); this.score = in.readInt(); - this.voteUp = in.readByte() != 0; - this.voteDown = in.readByte() != 0; + this.editable = in.readByte() != 0; + this.voteUpAble = in.readByte() != 0; + this.voteUpEd = in.readByte() != 0; + this.voteDownAble = in.readByte() != 0; + this.voteDownEd = in.readByte() != 0; this.voteState = in.readString(); this.time = in.readLong(); this.user = in.readString(); this.comment = in.readString(); + this.lastEdited = in.readLong(); } public static final Creator CREATOR = new Creator() { diff --git a/app/src/main/java/com/hippo/ehviewer/client/data/GalleryCommentList.java b/app/src/main/java/com/hippo/ehviewer/client/data/GalleryCommentList.java new file mode 100644 index 000000000..23370155f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/client/data/GalleryCommentList.java @@ -0,0 +1,65 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.client.data; + +import android.os.Parcel; +import android.os.Parcelable; +import java.util.Arrays; + +public class GalleryCommentList implements Parcelable { + + public GalleryComment[] comments; + public boolean hasMore; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelableArray(comments, flags); + dest.writeByte(hasMore ? (byte) 1 : (byte) 0); + } + + public GalleryCommentList(GalleryComment[] comments, boolean hasMore) { + this.comments = comments; + this.hasMore = hasMore; + } + + protected GalleryCommentList(Parcel in) { + Parcelable[] array = in.readParcelableArray(getClass().getClassLoader()); + if (array != null) { + comments = Arrays.copyOf(array, array.length, GalleryComment[].class); + } else { + comments = null; + } + hasMore = in.readByte() != 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public GalleryCommentList createFromParcel(Parcel in) { + return new GalleryCommentList(in); + } + + @Override + public GalleryCommentList[] newArray(int size) { + return new GalleryCommentList[size]; + } + }; +} diff --git a/app/src/main/java/com/hippo/ehviewer/client/data/GalleryDetail.java b/app/src/main/java/com/hippo/ehviewer/client/data/GalleryDetail.java index 5b11dfbf8..71e93615b 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/data/GalleryDetail.java +++ b/app/src/main/java/com/hippo/ehviewer/client/data/GalleryDetail.java @@ -37,7 +37,7 @@ public class GalleryDetail extends GalleryInfo { public boolean isFavorited; public int ratingCount; public GalleryTagGroup[] tags; - public GalleryComment[] comments; + public GalleryCommentList comments; public int previewPages; public PreviewSet previewSet; @@ -60,8 +60,8 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeInt(this.favoriteCount); dest.writeByte(isFavorited ? (byte) 1 : (byte) 0); dest.writeInt(this.ratingCount); - dest.writeParcelableArray(this.tags, 0); - dest.writeParcelableArray(this.comments, 0); + dest.writeParcelableArray(this.tags, flags); + dest.writeParcelable(this.comments, flags); dest.writeInt(this.previewPages); dest.writeParcelable(previewSet, flags); } @@ -88,12 +88,7 @@ protected GalleryDetail(Parcel in) { } else { this.tags = null; } - array = in.readParcelableArray(GalleryComment.class.getClassLoader()); - if (array != null) { - this.comments = Arrays.copyOf(array, array.length, GalleryComment[].class); - } else { - this.comments = null; - } + this.comments = in.readParcelable(getClass().getClassLoader()); this.previewPages = in.readInt(); this.previewSet = in.readParcelable(PreviewSet.class.getClassLoader()); } diff --git a/app/src/main/java/com/hippo/ehviewer/client/data/GalleryInfo.java b/app/src/main/java/com/hippo/ehviewer/client/data/GalleryInfo.java index f47fbb5d2..a83ec3d76 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/data/GalleryInfo.java +++ b/app/src/main/java/com/hippo/ehviewer/client/data/GalleryInfo.java @@ -18,8 +18,7 @@ import android.os.Parcel; import android.os.Parcelable; -import android.support.annotation.Nullable; - +import androidx.annotation.Nullable; import java.util.regex.Pattern; public class GalleryInfo implements Parcelable { @@ -62,21 +61,38 @@ public class GalleryInfo implements Parcelable { }; public static final Pattern[] S_LANG_PATTERNS = { - Pattern.compile("[(\\[]eng(?:lish)?[)\\]]", Pattern.CASE_INSENSITIVE), - // [((\[]ch(?:inese)?[))\]]|[汉漢]化|中[国國][语語]|中文 - Pattern.compile("[(\uFF08\\[]ch(?:inese)?[)\uFF09\\]]|[汉漢]化|中[国國][语語]|中文", Pattern.CASE_INSENSITIVE), - Pattern.compile("[(\\[]spanish[)\\]]|[(\\[]Español[)\\]]", Pattern.CASE_INSENSITIVE), - Pattern.compile("[(\\[]korean?[)\\]]", Pattern.CASE_INSENSITIVE), - Pattern.compile("[(\\[]rus(?:sian)?[)\\]]", Pattern.CASE_INSENSITIVE), - Pattern.compile("[(\\[]fr(?:ench)?[)\\]]", Pattern.CASE_INSENSITIVE), - Pattern.compile("[(\\[]portuguese", Pattern.CASE_INSENSITIVE), - Pattern.compile("[(\\[]thai(?: ภาษาไทย)?[)\\]]|แปลไทย", Pattern.CASE_INSENSITIVE), - Pattern.compile("[(\\[]german[)\\]]", Pattern.CASE_INSENSITIVE), - Pattern.compile("[(\\[]italiano?[)\\]]", Pattern.CASE_INSENSITIVE), - Pattern.compile("[(\\[]vietnamese(?: Tiếng Việt)?[)\\]]", Pattern.CASE_INSENSITIVE), - Pattern.compile("[(\\[]polish[)\\]]", Pattern.CASE_INSENSITIVE), - Pattern.compile("[(\\[]hun(?:garian)?[)\\]]", Pattern.CASE_INSENSITIVE), - Pattern.compile("[(\\[]dutch[)\\]]", Pattern.CASE_INSENSITIVE), + Pattern.compile("[(\\[]eng(?:lish)?[)\\]]|英訳", Pattern.CASE_INSENSITIVE), + // [((\[]ch(?:inese)?[))\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳 + Pattern.compile("[(\uFF08\\[]ch(?:inese)?[)\uFF09\\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳", Pattern.CASE_INSENSITIVE), + Pattern.compile("[(\\[]spanish[)\\]]|[(\\[]Español[)\\]]|スペイン翻訳", Pattern.CASE_INSENSITIVE), + Pattern.compile("[(\\[]korean?[)\\]]|韓国翻訳", Pattern.CASE_INSENSITIVE), + Pattern.compile("[(\\[]rus(?:sian)?[)\\]]|ロシア翻訳", Pattern.CASE_INSENSITIVE), + Pattern.compile("[(\\[]fr(?:ench)?[)\\]]|フランス翻訳", Pattern.CASE_INSENSITIVE), + Pattern.compile("[(\\[]portuguese|ポルトガル翻訳", Pattern.CASE_INSENSITIVE), + Pattern.compile("[(\\[]thai(?: ภาษาไทย)?[)\\]]|แปลไทย|タイ翻訳", Pattern.CASE_INSENSITIVE), + Pattern.compile("[(\\[]german[)\\]]|ドイツ翻訳", Pattern.CASE_INSENSITIVE), + Pattern.compile("[(\\[]italiano?[)\\]]|イタリア翻訳", Pattern.CASE_INSENSITIVE), + Pattern.compile("[(\\[]vietnamese(?: Tiếng Việt)?[)\\]]|ベトナム翻訳", Pattern.CASE_INSENSITIVE), + Pattern.compile("[(\\[]polish[)\\]]|ポーランド翻訳", Pattern.CASE_INSENSITIVE), + Pattern.compile("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", Pattern.CASE_INSENSITIVE), + Pattern.compile("[(\\[]dutch[)\\]]|オランダ翻訳", Pattern.CASE_INSENSITIVE), + }; + + public static final String[] S_LANG_TAGS = { + "language:english", + "language:chinese", + "language:spanish", + "language:korean", + "language:russian", + "language:french", + "language:portuguese", + "language:thai", + "language:german", + "language:italian", + "language:vietnamese", + "language:polish", + "language:hungarian", + "language:dutch", }; public long gid ; @@ -88,8 +104,10 @@ public class GalleryInfo implements Parcelable { public String posted; public String uploader; public float rating; + public boolean rated; @Nullable public String[] simpleTags; + public int pages; public int thumbWidth; public int thumbHeight; @@ -103,7 +121,30 @@ public class GalleryInfo implements Parcelable { */ public String simpleLanguage; + public int favoriteSlot = -2; + public String favoriteName; + public final void generateSLang() { + if (simpleTags != null) { + generateSLangFromTags(); + } + if (simpleLanguage == null && title != null) { + generateSLangFromTitle(); + } + } + + private void generateSLangFromTags() { + for (String tag : simpleTags) { + for (int i = 0; i < S_LANGS.length; i++) { + if (S_LANG_TAGS[i].equals(tag)) { + simpleLanguage = S_LANGS[i]; + return; + } + } + } + } + + private void generateSLangFromTitle() { for (int i = 0; i < S_LANGS.length; i++) { if (S_LANG_PATTERNS[i].matcher(title).find()) { simpleLanguage = S_LANGS[i]; @@ -129,6 +170,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeString(this.posted); dest.writeString(this.uploader); dest.writeFloat(this.rating); + dest.writeByte(this.rated ? (byte) 1 : (byte) 0); dest.writeString(this.simpleLanguage); dest.writeStringArray(this.simpleTags); dest.writeInt(this.thumbWidth); @@ -136,6 +178,8 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeInt(this.spanSize); dest.writeInt(this.spanIndex); dest.writeInt(this.spanGroupIndex); + dest.writeInt(this.favoriteSlot); + dest.writeString(this.favoriteName); } public GalleryInfo() {} @@ -150,6 +194,7 @@ protected GalleryInfo(Parcel in) { this.posted = in.readString(); this.uploader = in.readString(); this.rating = in.readFloat(); + this.rated = in.readByte() != 0; this.simpleLanguage = in.readString(); this.simpleTags = in.createStringArray(); this.thumbWidth = in.readInt(); @@ -157,6 +202,8 @@ protected GalleryInfo(Parcel in) { this.spanSize = in.readInt(); this.spanIndex = in.readInt(); this.spanGroupIndex = in.readInt(); + this.favoriteSlot = in.readInt(); + this.favoriteName = in.readString(); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { diff --git a/app/src/main/java/com/hippo/ehviewer/client/data/ListUrlBuilder.java b/app/src/main/java/com/hippo/ehviewer/client/data/ListUrlBuilder.java index 69f22dde0..20056c6d1 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/data/ListUrlBuilder.java +++ b/app/src/main/java/com/hippo/ehviewer/client/data/ListUrlBuilder.java @@ -18,18 +18,17 @@ import android.os.Parcel; import android.os.Parcelable; -import android.support.annotation.IntDef; -import android.support.annotation.Nullable; import android.text.TextUtils; - +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.hippo.ehviewer.client.EhConfig; import com.hippo.ehviewer.client.EhUrl; import com.hippo.ehviewer.client.EhUtils; import com.hippo.ehviewer.dao.QuickSearch; import com.hippo.ehviewer.widget.AdvanceSearchTable; import com.hippo.network.UrlBuilder; +import com.hippo.yorozuya.NumberUtils; import com.hippo.yorozuya.StringUtils; - import java.io.UnsupportedEncodingException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -38,7 +37,7 @@ public class ListUrlBuilder implements Cloneable, Parcelable { - @IntDef({MODE_NORMAL, MODE_UPLOADER, MODE_TAG, MODE_WHATS_HOT, MODE_IMAGE_SEARCH}) + @IntDef({MODE_NORMAL, MODE_UPLOADER, MODE_TAG, MODE_WHATS_HOT, MODE_IMAGE_SEARCH, MODE_SUBSCRIPTION}) @Retention(RetentionPolicy.SOURCE) private @interface Mode {} @@ -48,6 +47,7 @@ public class ListUrlBuilder implements Cloneable, Parcelable { public static final int MODE_TAG = 0x2; public static final int MODE_WHATS_HOT = 0x3; public static final int MODE_IMAGE_SEARCH = 0x4; + public static final int MODE_SUBSCRIPTION = 0x5; public static final int DEFAULT_ADVANCE = AdvanceSearchTable.SNAME | AdvanceSearchTable.STAGS; public static final int DEFAULT_MIN_RATING = 2; @@ -62,6 +62,8 @@ public class ListUrlBuilder implements Cloneable, Parcelable { private int mAdvanceSearch = -1; private int mMinRating = -1; + private int mPageFrom = -1; + private int mPageTo = -1; private String mImagePath; private boolean mUseSimilarityScan; @@ -78,6 +80,8 @@ public void reset() { mKeyword = null; mAdvanceSearch = -1; mMinRating = -1; + mPageFrom = -1; + mPageTo = -1; mImagePath = null; mUseSimilarityScan = false; mOnlySearchCovers = false; @@ -142,6 +146,22 @@ public void setMinRating(int minRating) { mMinRating = minRating; } + public int getPageFrom() { + return mPageFrom; + } + + public void setPageFrom(int pageFrom) { + mPageFrom = pageFrom; + } + + public int getPageTo() { + return mPageTo; + } + + public void setPageTo(int pageTo) { + mPageTo = pageTo; + } + @Nullable public String getImagePath() { return mImagePath; @@ -186,6 +206,8 @@ public void set(ListUrlBuilder lub) { mKeyword = lub.mKeyword; mAdvanceSearch = lub.mAdvanceSearch; mMinRating = lub.mMinRating; + mPageFrom = lub.mPageFrom; + mPageTo = lub.mPageTo; mImagePath = lub.mImagePath; mUseSimilarityScan = lub.mUseSimilarityScan; mOnlySearchCovers = lub.mOnlySearchCovers; @@ -198,6 +220,8 @@ public void set(QuickSearch q) { mKeyword = q.keyword; mAdvanceSearch = q.advanceSearch; mMinRating = q.minRating; + mPageFrom = q.pageFrom; + mPageTo = q.pageTo; mImagePath = null; mUseSimilarityScan = false; mOnlySearchCovers = false; @@ -211,6 +235,8 @@ public QuickSearch toQuickSearch() { q.keyword = mKeyword; q.advanceSearch = mAdvanceSearch; q.minRating = mMinRating; + q.pageFrom = mPageFrom; + q.pageTo = mPageTo; return q; } @@ -234,6 +260,12 @@ public boolean equalsQuickSearch(QuickSearch q) { if (q.minRating != mMinRating) { return false; } + if (q.pageFrom != mPageFrom) { + return false; + } + if (q.pageTo != mPageTo) { + return false; + } return true; } @@ -250,124 +282,166 @@ public void setQuery(String query) { } String[] querys = StringUtils.split(query, '&'); - boolean apply = false; int category = 0; String keyword = null; boolean enableAdvanceSearch = false; int advanceSearch = 0; boolean enableMinRating = false; - int minRating = 0; - for (int i = 0, size = querys.length; i < size; i++) { - String str = querys[i]; + int minRating = -1; + boolean enablePage = false; + int pageFrom = -1; + int pageTo = -1; + for (String str : querys) { int index = str.indexOf('='); if (index < 0) { continue; } String key = str.substring(0, index); String value = str.substring(index + 1); - if ("f_doujinshi".equals(key)) { - if ("1".equals(value)) { - category |= EhConfig.DOUJINSHI; - } - } else if ("f_manga".equals(key)) { - if ("1".equals(value)) { - category |= EhConfig.MANGA; - } - } else if ("f_artistcg".equals(key)) { - if ("1".equals(value)) { - category |= EhConfig.ARTIST_CG; - } - } else if ("f_gamecg".equals(key)) { - if ("1".equals(value)) { - category |= EhConfig.GAME_CG; - } - } else if ("f_western".equals(key)) { - if ("1".equals(value)) { - category |= EhConfig.WESTERN; - } - } else if ("f_non-h".equals(key)) { - if ("1".equals(value)) { - category |= EhConfig.NON_H; - } - } else if ("f_imageset".equals(key)) { - if ("1".equals(value)) { - category |= EhConfig.IMAGE_SET; - } - } else if ("f_cosplay".equals(key)) { - if ("1".equals(value)) { - category |= EhConfig.COSPLAY; - } - } else if ("f_asianporn".equals(key)) { - if ("1".equals(value)) { - category |= EhConfig.ASIAN_PORN; - } - } else if ("f_misc".equals(key)) { - if ("1".equals(value)) { - category |= EhConfig.MISC; - } - } else if ("f_search".equals(key)) { - try { - keyword = URLDecoder.decode(value, "utf-8"); - } catch (UnsupportedEncodingException e) { - // Ignore - } - } else if ("advsearch".equals(key)) { - if ("1".equals(value)) { - enableAdvanceSearch = true; - } - } else if ("f_sname".equals(key)) { - if ("on".equals(value)) { - advanceSearch |= AdvanceSearchTable.SNAME; - } - } else if ("f_stags".equals(key)) { - if ("on".equals(value)) { - advanceSearch |= AdvanceSearchTable.STAGS; - } - } else if ("f_sdesc".equals(key)) { - if ("on".equals(value)) { - advanceSearch |= AdvanceSearchTable.SDESC; - } - } else if ("f_storr".equals(key)) { - if ("on".equals(value)) { - advanceSearch |= AdvanceSearchTable.STORR; - } - } else if ("f_sto".equals(key)) { - if ("on".equals(value)) { - advanceSearch |= AdvanceSearchTable.STO; - } - } else if ("f_sdt1".equals(key)) { - if ("on".equals(value)) { - advanceSearch |= AdvanceSearchTable.SDT1; - } - } else if ("f_sdt2".equals(key)) { - if ("on".equals(value)) { - advanceSearch |= AdvanceSearchTable.SDT2; - } - } else if ("f_sh".equals(key)) { - if ("on".equals(value)) { - advanceSearch |= AdvanceSearchTable.SH; - } - } else if ("f_sr".equals(key)) { - if ("on".equals(value)) { - enableMinRating = true; - } - } else if ("f_srdd".equals(key)) { - try { - minRating = Integer.parseInt(value); - } catch (NumberFormatException e) { - // Ignore - } - } else if ("f_apply".equals(key)) { - if ("Apply+Filter".equals(value)) { - apply = true; - } + switch (key) { + case "f_cats": + int cats = NumberUtils.parseIntSafely(value, EhConfig.ALL_CATEGORY); + category |= (~cats) & EhConfig.ALL_CATEGORY; + break; + case "f_doujinshi": + if ("1".equals(value)) { + category |= EhConfig.DOUJINSHI; + } + break; + case "f_manga": + if ("1".equals(value)) { + category |= EhConfig.MANGA; + } + break; + case "f_artistcg": + if ("1".equals(value)) { + category |= EhConfig.ARTIST_CG; + } + break; + case "f_gamecg": + if ("1".equals(value)) { + category |= EhConfig.GAME_CG; + } + break; + case "f_western": + if ("1".equals(value)) { + category |= EhConfig.WESTERN; + } + break; + case "f_non-h": + if ("1".equals(value)) { + category |= EhConfig.NON_H; + } + break; + case "f_imageset": + if ("1".equals(value)) { + category |= EhConfig.IMAGE_SET; + } + break; + case "f_cosplay": + if ("1".equals(value)) { + category |= EhConfig.COSPLAY; + } + break; + case "f_asianporn": + if ("1".equals(value)) { + category |= EhConfig.ASIAN_PORN; + } + break; + case "f_misc": + if ("1".equals(value)) { + category |= EhConfig.MISC; + } + break; + case "f_search": + try { + keyword = URLDecoder.decode(value, "utf-8"); + } catch (UnsupportedEncodingException | IllegalArgumentException e) { + // Ignore + } + break; + case "advsearch": + if ("1".equals(value)) { + enableAdvanceSearch = true; + } + break; + case "f_sname": + if ("on".equals(value)) { + advanceSearch |= AdvanceSearchTable.SNAME; + } + break; + case "f_stags": + if ("on".equals(value)) { + advanceSearch |= AdvanceSearchTable.STAGS; + } + break; + case "f_sdesc": + if ("on".equals(value)) { + advanceSearch |= AdvanceSearchTable.SDESC; + } + break; + case "f_storr": + if ("on".equals(value)) { + advanceSearch |= AdvanceSearchTable.STORR; + } + break; + case "f_sto": + if ("on".equals(value)) { + advanceSearch |= AdvanceSearchTable.STO; + } + break; + case "f_sdt1": + if ("on".equals(value)) { + advanceSearch |= AdvanceSearchTable.SDT1; + } + break; + case "f_sdt2": + if ("on".equals(value)) { + advanceSearch |= AdvanceSearchTable.SDT2; + } + break; + case "f_sh": + if ("on".equals(value)) { + advanceSearch |= AdvanceSearchTable.SH; + } + break; + case "f_sfl": + if ("on".equals(value)) { + advanceSearch |= AdvanceSearchTable.SFL; + } + break; + case "f_sfu": + if ("on".equals(value)) { + advanceSearch |= AdvanceSearchTable.SFU; + } + break; + case "f_sft": + if ("on".equals(value)) { + advanceSearch |= AdvanceSearchTable.SFT; + } + break; + case "f_sr": + if ("on".equals(value)) { + enableMinRating = true; + } + break; + case "f_srdd": + minRating = NumberUtils.parseIntSafely(value, -1); + break; + case "f_sp": + if ("on".equals(value)) { + enablePage = true; + } + break; + case "f_spf": + pageFrom = NumberUtils.parseIntSafely(value, -1); + break; + case "f_spt": + pageTo = NumberUtils.parseIntSafely(value, -1); + break; } } - if (!apply) { - return; - } - mCategory = category; mKeyword = keyword; if (enableAdvanceSearch) { @@ -377,6 +451,13 @@ public void setQuery(String query) { } else { mMinRating = -1; } + if (enablePage) { + mPageFrom = pageFrom; + mPageTo = pageTo; + } else { + mPageFrom = -1; + mPageTo = -1; + } } else { mAdvanceSearch = -1; } @@ -385,29 +466,28 @@ public void setQuery(String query) { public String build() { switch (mMode) { default: - case MODE_NORMAL: { - boolean filter = false; - UrlBuilder ub = new UrlBuilder(EhUrl.getHost()); + case MODE_NORMAL: + case MODE_SUBSCRIPTION: { + String url; + if (mMode == MODE_NORMAL) { + url = EhUrl.getHost(); + } else { + url = EhUrl.getWatchedUrl(); + } + + UrlBuilder ub = new UrlBuilder(url); if (mCategory != EhUtils.NONE) { - ub.addQuery("f_doujinshi", ((mCategory & EhConfig.DOUJINSHI) == 0) ? "0" : "1"); - ub.addQuery("f_manga", ((mCategory & EhConfig.MANGA) == 0) ? "0" : "1"); - ub.addQuery("f_artistcg", ((mCategory & EhConfig.ARTIST_CG) == 0) ? "0" : "1"); - ub.addQuery("f_gamecg", ((mCategory & EhConfig.GAME_CG) == 0) ? "0" : "1"); - ub.addQuery("f_western", ((mCategory & EhConfig.WESTERN) == 0) ? "0" : "1"); - ub.addQuery("f_non-h", ((mCategory & EhConfig.NON_H) == 0) ? "0" : "1"); - ub.addQuery("f_imageset", ((mCategory & EhConfig.IMAGE_SET) == 0) ? "0" : "1"); - ub.addQuery("f_cosplay", ((mCategory & EhConfig.COSPLAY) == 0) ? "0" : "1"); - ub.addQuery("f_asianporn", ((mCategory & EhConfig.ASIAN_PORN) == 0) ? "0" : "1"); - ub.addQuery("f_misc", ((mCategory & EhConfig.MISC) == 0) ? "0" : "1"); - filter = true; + ub.addQuery("f_cats", (~mCategory) & EhConfig.ALL_CATEGORY); } // Search key if (mKeyword != null) { - try { - ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8")); - filter = true; - } catch (UnsupportedEncodingException e) { - // Empty + String keyword = mKeyword.trim(); + if (!keyword.isEmpty()) { + try { + ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8")); + } catch (UnsupportedEncodingException e) { + // Empty + } } } // Page index @@ -425,16 +505,20 @@ public String build() { if((mAdvanceSearch & AdvanceSearchTable.SDT1) != 0) ub.addQuery("f_sdt1", "on"); if((mAdvanceSearch & AdvanceSearchTable.SDT2) != 0) ub.addQuery("f_sdt2", "on"); if((mAdvanceSearch & AdvanceSearchTable.SH) != 0) ub.addQuery("f_sh", "on"); + if((mAdvanceSearch & AdvanceSearchTable.SFL) != 0) ub.addQuery("f_sfl", "on"); + if((mAdvanceSearch & AdvanceSearchTable.SFU) != 0) ub.addQuery("f_sfu", "on"); + if((mAdvanceSearch & AdvanceSearchTable.SFT) != 0) ub.addQuery("f_sft", "on"); // Set min star if (mMinRating != -1) { ub.addQuery("f_sr", "on"); ub.addQuery("f_srdd", mMinRating); } - filter = true; - } - // Add filter foot - if (filter) { - ub.addQuery("f_apply", "Apply+Filter"); + // Pages + if (mPageFrom != -1 || mPageTo != -1) { + ub.addQuery("f_sp", "on"); + ub.addQuery("f_spf", mPageFrom != -1 ? Integer.toString(mPageFrom) : ""); + ub.addQuery("f_spt", mPageTo != -1 ? Integer.toString(mPageTo) : ""); + } } return ub.build(); } @@ -464,6 +548,8 @@ public String build() { } return sb.toString(); } + case MODE_WHATS_HOT: + return EhUrl.getPopularUrl(); case MODE_IMAGE_SEARCH: return EhUrl.getImageSearchUrl(); } @@ -482,6 +568,8 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeString(this.mKeyword); dest.writeInt(this.mAdvanceSearch); dest.writeInt(this.mMinRating); + dest.writeInt(this.mPageFrom); + dest.writeInt(this.mPageTo); dest.writeString(this.mImagePath); dest.writeByte(mUseSimilarityScan ? (byte) 1 : (byte) 0); dest.writeByte(mOnlySearchCovers ? (byte) 1 : (byte) 0); @@ -499,6 +587,8 @@ protected ListUrlBuilder(Parcel in) { this.mKeyword = in.readString(); this.mAdvanceSearch = in.readInt(); this.mMinRating = in.readInt(); + this.mPageFrom = in.readInt(); + this.mPageTo = in.readInt(); this.mImagePath = in.readString(); this.mUseSimilarityScan = in.readByte() != 0; this.mOnlySearchCovers = in.readByte() != 0; diff --git a/app/src/main/java/com/hippo/ehviewer/client/exception/EhException.java b/app/src/main/java/com/hippo/ehviewer/client/exception/EhException.java index 6aeb355b2..914bdbadf 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/exception/EhException.java +++ b/app/src/main/java/com/hippo/ehviewer/client/exception/EhException.java @@ -21,4 +21,8 @@ public class EhException extends Exception { public EhException(String detailMessage) { super(detailMessage); } + + public EhException(String detailMessage, Throwable cause) { + super(detailMessage, cause); + } } diff --git a/app/src/main/java/com/hippo/ehviewer/client/exception/ParseException.java b/app/src/main/java/com/hippo/ehviewer/client/exception/ParseException.java index 4b662fba5..963ed12c1 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/exception/ParseException.java +++ b/app/src/main/java/com/hippo/ehviewer/client/exception/ParseException.java @@ -25,6 +25,11 @@ public ParseException(String detailMessage, String body) { mBody = body; } + public ParseException(String detailMessage, String body, Throwable cause) { + super(detailMessage, cause); + mBody = body; + } + public String getBody() { return mBody; } diff --git a/app/src/main/java/com/hippo/ehviewer/client/parser/FavoritesParser.java b/app/src/main/java/com/hippo/ehviewer/client/parser/FavoritesParser.java index 61092df9f..b6450b752 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/parser/FavoritesParser.java +++ b/app/src/main/java/com/hippo/ehviewer/client/parser/FavoritesParser.java @@ -21,23 +21,22 @@ import com.hippo.ehviewer.client.data.GalleryInfo; import com.hippo.ehviewer.client.exception.EhException; import com.hippo.ehviewer.client.exception.ParseException; +import com.hippo.util.ExceptionUtils; import com.hippo.util.JsoupUtils; - -import junit.framework.Assert; - +import com.hippo.yorozuya.AssertUtils; +import java.util.List; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; -import java.util.List; - public class FavoritesParser { public static class Result { public String[] catArray; // Size 10 public int[] countArray; // Size 10 public int pages; + public int nextPage; public List galleryInfoList; } @@ -54,14 +53,15 @@ public static Result parse(String body) throws Exception { //noinspection ConstantConditions Elements fps = ido.getElementsByClass("fp"); // Last one is "fp fps" - Assert.assertEquals(11, fps.size()); + AssertUtils.assertEquals(11, fps.size()); for (int i = 0; i < 10; i++) { Element fp = fps.get(i); - countArray[i] = ParserUtils.parseInt(fp.child(0).text()); + countArray[i] = ParserUtils.parseInt(fp.child(0).text(), 0); catArray[i] = ParserUtils.trim(fp.child(2).text()); } - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); e.printStackTrace(); throw new ParseException("Parse favorites error", body); } @@ -72,6 +72,7 @@ public static Result parse(String body) throws Exception { re.catArray = catArray; re.countArray = countArray; re.pages = result.pages; + re.nextPage = result.nextPage; re.galleryInfoList = result.galleryInfoList; return re; diff --git a/app/src/main/java/com/hippo/ehviewer/client/parser/ForumsParser.java b/app/src/main/java/com/hippo/ehviewer/client/parser/ForumsParser.java index 3f77b2180..2e591d362 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/parser/ForumsParser.java +++ b/app/src/main/java/com/hippo/ehviewer/client/parser/ForumsParser.java @@ -18,7 +18,7 @@ import com.hippo.ehviewer.client.EhUrl; import com.hippo.ehviewer.client.exception.ParseException; - +import com.hippo.util.ExceptionUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; @@ -31,7 +31,8 @@ public static String parse(String body) throws ParseException { Element userlinks = d.getElementById("userlinks"); Element child = userlinks.child(0).child(0).child(0); return child.attr("href"); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throw new ParseException("Parse forums error", body); } } diff --git a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryApiParser.java b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryApiParser.java index cd0897233..1d243d955 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryApiParser.java +++ b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryApiParser.java @@ -19,6 +19,7 @@ import com.hippo.ehviewer.client.EhUtils; import com.hippo.ehviewer.client.data.GalleryInfo; +import com.hippo.yorozuya.NumberUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -43,8 +44,8 @@ public static void parse(String body, List galleryInfoList) throws gi.category = EhUtils.getCategory(g.getString("category")); gi.thumb = EhUtils.handleThumbUrlResolution(g.getString("thumb")); gi.uploader = g.getString("uploader"); - gi.posted = ParserUtils.formatDate(ParserUtils.parseLong(g.getString("posted")) * 1000); - gi.rating = Float.parseFloat(g.getString("rating")); + gi.posted = ParserUtils.formatDate(ParserUtils.parseLong(g.getString("posted"), 0) * 1000); + gi.rating = NumberUtils.parseFloatSafely(g.getString("rating"), 0.0f); // tags JSONArray tagJa = g.getJSONArray("tags"); int tagLength = tagJa.length(); @@ -53,6 +54,8 @@ public static void parse(String body, List galleryInfoList) throws tags[j] = tagJa.getString(j); } gi.simpleTags = tags; + gi.pages = NumberUtils.parseIntSafely(g.getString("filecount"), 0); + gi.generateSLang(); } } diff --git a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailParser.java b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailParser.java index 93ff9eb14..27f9a6717 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailParser.java +++ b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailParser.java @@ -16,14 +16,13 @@ package com.hippo.ehviewer.client.parser; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.client.EhUrl; import com.hippo.ehviewer.client.EhUtils; import com.hippo.ehviewer.client.data.GalleryComment; +import com.hippo.ehviewer.client.data.GalleryCommentList; import com.hippo.ehviewer.client.data.GalleryDetail; import com.hippo.ehviewer.client.data.GalleryTagGroup; import com.hippo.ehviewer.client.data.LargePreviewSet; @@ -33,15 +32,11 @@ import com.hippo.ehviewer.client.exception.OffensiveException; import com.hippo.ehviewer.client.exception.ParseException; import com.hippo.ehviewer.client.exception.PiningException; +import com.hippo.util.ExceptionUtils; import com.hippo.util.JsoupUtils; +import com.hippo.util.MutableBoolean; import com.hippo.yorozuya.NumberUtils; import com.hippo.yorozuya.StringUtils; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -52,6 +47,13 @@ import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; +import org.jsoup.select.Elements; +import org.jsoup.select.NodeTraversor; +import org.jsoup.select.NodeVisitor; public class GalleryDetailParser { @@ -69,7 +71,7 @@ public class GalleryDetailParser { private static final Pattern PATTERN_LARGE_PREVIEW = Pattern.compile("
\"([\\d,]+)\".+?src=\"(.+?)\"");= 0) { - tag = tag.substring(0, index); + tag = tag.substring(0, index).trim(); } group.addTag(tag); } return group.size() > 0 ? group : null; - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); e.printStackTrace(); return null; } @@ -323,16 +346,27 @@ public static GalleryTagGroup[] parseTagGroups(Document document) { try { Element taglist = document.getElementById("taglist"); Elements tagGroups = taglist.child(0).child(0).children(); + return parseTagGroups(tagGroups); + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); + e.printStackTrace(); + return EMPTY_GALLERY_TAG_GROUP_ARRAY; + } + } - List list = new ArrayList<>(tagGroups.size()); - for (int i = 0, n = tagGroups.size(); i < n; i++) { - GalleryTagGroup group = parseTagGroup(tagGroups.get(i)); + @NonNull + public static GalleryTagGroup[] parseTagGroups(Elements trs) { + try { + List list = new ArrayList<>(trs.size()); + for (int i = 0, n = trs.size(); i < n; i++) { + GalleryTagGroup group = parseTagGroup(trs.get(i)); if (null != group) { list.add(group); } } return list.toArray(new GalleryTagGroup[list.size()]); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); e.printStackTrace(); return EMPTY_GALLERY_TAG_GROUP_ARRAY; } @@ -372,13 +406,23 @@ public static GalleryComment parseComment(Element element) { Element a = element.previousElementSibling(); String name = a.attr("name"); comment.id = Integer.parseInt(StringUtils.trim(name).substring(1)); - // Vote up and vote down + // Editable, vote up and vote down Element c4 = JsoupUtils.getElementByClass(element, "c4"); if (null != c4) { - Elements es = c4.children(); - if (2 == es.size()) { - comment.voteUp = !TextUtils.isEmpty(StringUtils.trim(es.get(0).attr("style"))); - comment.voteDown = !TextUtils.isEmpty(StringUtils.trim(es.get(1).attr("style"))); + for (Element e : c4.children()) { + switch (e.text()) { + case "Vote+": + comment.voteUpAble = true; + comment.voteUpEd = !StringUtils.trim(e.attr("style")).isEmpty(); + break; + case "Vote-": + comment.voteDownAble = true; + comment.voteDownEd = !StringUtils.trim(e.attr("style")).isEmpty(); + break; + case "Edit": + comment.editable = true; + break; + } } } // Vote state @@ -397,14 +441,23 @@ public static GalleryComment parseComment(Element element) { // time Element c3 = JsoupUtils.getElementByClass(element, "c3"); String temp = c3.ownText(); - temp = temp.substring("Posted on ".length(), temp.length() - " by:  ".length()); + temp = temp.substring("Posted on ".length(), temp.length() - " by:".length()); comment.time = WEB_COMMENT_DATE_FORMAT.parse(temp).getTime(); // user comment.user = c3.child(0).text(); // comment comment.comment = JsoupUtils.getElementByClass(element, "c6").html(); + // last edited + Element c8 = JsoupUtils.getElementByClass(element, "c8"); + if (c8 != null) { + Element e = c8.children().first(); + if (e != null) { + comment.lastEdited = WEB_COMMENT_DATE_FORMAT.parse(temp).getTime(); + } + } return comment; - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); e.printStackTrace(); return null; } @@ -414,7 +467,7 @@ public static GalleryComment parseComment(Element element) { * Parse comments with html parser */ @NonNull - public static GalleryComment[] parseComments(Document document) { + public static GalleryCommentList parseComments(Document document) { try { Element cdiv = document.getElementById("cdiv"); Elements c1s = cdiv.getElementsByClass("c1"); @@ -426,8 +479,24 @@ public static GalleryComment[] parseComments(Document document) { list.add(comment); } } - return list.toArray(new GalleryComment[list.size()]); - } catch (Exception e) { + + Element chd = cdiv.getElementById("chd"); + MutableBoolean hasMore = new MutableBoolean(false); + NodeTraversor.traverse(new NodeVisitor() { + @Override + public void head(Node node, int depth) { + if (node instanceof Element && ((Element) node).text().equals("click to show all")) { + hasMore.value = true; + } + } + + @Override + public void tail(Node node, int depth) { } + }, chd); + + return new GalleryCommentList(list.toArray(new GalleryComment[list.size()]), hasMore.value); + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); e.printStackTrace(); return EMPTY_GALLERY_COMMENT_ARRAY; } @@ -466,7 +535,8 @@ public static int parsePreviewPages(Document document, String body) throws Parse try { Elements elements = document.getElementsByClass("ptt").first().child(0).child(0).children(); return Integer.parseInt(elements.get(elements.size() - 2).text()); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); e.printStackTrace(); throw new ParseException("Can't parse preview pages", body); } @@ -479,7 +549,7 @@ public static int parsePreviewPages(String body) throws ParseException { Matcher m = PATTERN_PREVIEW_PAGES.matcher(body); int previewPages = -1; if (m.find()) { - previewPages = ParserUtils.parseInt(m.group(1)); + previewPages = ParserUtils.parseInt(m.group(1), -1); } if (previewPages <= 0) { @@ -493,12 +563,18 @@ public static int parsePreviewPages(String body) throws ParseException { * Parse pages with regular expressions */ public static int parsePages(String body) throws ParseException { + int pages = -1; + Matcher m = PATTERN_PAGES.matcher(body); if (m.find()) { - return ParserUtils.parseInt(m.group(1)); - } else { + pages = ParserUtils.parseInt(m.group(1), -1); + } + + if (pages < 0) { throw new ParseException("Parse pages error", body); } + + return pages; } public static PreviewSet parsePreviewSet(Document d, String body) throws ParseException { @@ -541,7 +617,8 @@ private static LargePreviewSet parseLargePreviewSet(Document d, String body) thr largePreviewSet.addItem(index, imageUrl, pageUrl); } return largePreviewSet; - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); e.printStackTrace(); throw new ParseException("Can't parse large preview", body); } @@ -555,7 +632,10 @@ private static LargePreviewSet parseLargePreviewSet(String body) throws ParseExc LargePreviewSet largePreviewSet = new LargePreviewSet(); while (m.find()) { - int index = ParserUtils.parseInt(m.group(2)) - 1; + int index = ParserUtils.parseInt(m.group(2), 0) - 1; + if (index < 0) { + continue; + } String imageUrl = ParserUtils.trim(m.group(3)); String pageUrl = ParserUtils.trim(m.group(1)); if (Settings.getFixThumbUrl()) { @@ -578,10 +658,23 @@ private static NormalPreviewSet parseNormalPreviewSet(String body) throws ParseE Matcher m = PATTERN_NORMAL_PREVIEW.matcher(body); NormalPreviewSet normalPreviewSet = new NormalPreviewSet(); while (m.find()) { - normalPreviewSet.addItem(ParserUtils.parseInt(m.group(6)) - 1, - ParserUtils.trim(m.group(3)), ParserUtils.parseInt((m.group(4))), 0, - ParserUtils.parseInt(m.group(1)), ParserUtils.parseInt(m.group(2)), - ParserUtils.trim(m.group(5))); + int position = ParserUtils.parseInt(m.group(6), 0) - 1; + if (position < 0) { + continue; + } + String imageUrl = ParserUtils.trim(m.group(3)); + int xOffset = ParserUtils.parseInt(m.group(4), 0); + int yOffset = 0; + int width = ParserUtils.parseInt(m.group(1), 0); + if (width <= 0) { + continue; + } + int height = ParserUtils.parseInt(m.group(2), 0); + if (height <= 0) { + continue; + } + String pageUrl = ParserUtils.trim(m.group(5)); + normalPreviewSet.addItem(position, imageUrl, xOffset, yOffset, width, height, pageUrl); } if (normalPreviewSet.size() == 0) { diff --git a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailUrlParser.java b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailUrlParser.java index cb2592680..8bd928963 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailUrlParser.java +++ b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailUrlParser.java @@ -16,10 +16,9 @@ package com.hippo.ehviewer.client.parser; -import android.support.annotation.Nullable; - +import androidx.annotation.Nullable; import com.hippo.ehviewer.client.EhUrl; - +import com.hippo.yorozuya.NumberUtils; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -29,20 +28,32 @@ */ public final class GalleryDetailUrlParser { - public static final Pattern URL_PATTERN = Pattern.compile("https?://(?:" + - EhUrl.DOMAIN_EX + "|" + EhUrl.DOMAIN_E + "|" + EhUrl.DOMAIN_LOFI + ")/g/(\\d+)/(\\w+)"); + private static final Pattern URL_STRICT_PATTERN = Pattern.compile( + "https?://(?:" + EhUrl.DOMAIN_EX + "|" + EhUrl.DOMAIN_E + "|" + EhUrl.DOMAIN_LOFI + ")/(?:g|mpv)/(\\d+)/([0-9a-f]{10})"); + + private static final Pattern URL_PATTERN = Pattern.compile( + "(\\d+)/([0-9a-f]{10})(?:[^0-9a-f]|$)"); @Nullable public static Result parse(String url) { + return parse(url, true); + } + + @Nullable + public static Result parse(String url, boolean strict) { if (url == null) { return null; } - Matcher m = URL_PATTERN.matcher(url); + Pattern pattern = strict ? URL_STRICT_PATTERN : URL_PATTERN; + Matcher m = pattern.matcher(url); if (m.find()) { Result result = new Result(); - result.gid = Long.parseLong(m.group(1)); + result.gid = NumberUtils.parseLongSafely(m.group(1), -1L); result.token = m.group(2); + if (result.gid < 0) { + return null; + } return result; } else { return null; diff --git a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryListParser.java b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryListParser.java index 7e5ed6e54..0a4e5d12c 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryListParser.java +++ b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryListParser.java @@ -16,37 +16,54 @@ package com.hippo.ehviewer.client.parser; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import android.text.TextUtils; import android.util.Log; - +import androidx.annotation.NonNull; +import com.hippo.ehviewer.EhDB; import com.hippo.ehviewer.client.EhUtils; import com.hippo.ehviewer.client.data.GalleryInfo; +import com.hippo.ehviewer.client.data.GalleryTagGroup; import com.hippo.ehviewer.client.exception.ParseException; +import com.hippo.util.ExceptionUtils; import com.hippo.util.JsoupUtils; import com.hippo.yorozuya.NumberUtils; -import com.hippo.yorozuya.StringUtils; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; public class GalleryListParser { private static final String TAG = GalleryListParser.class.getSimpleName(); private static final Pattern PATTERN_RATING = Pattern.compile("\\d+px"); - private static final Pattern PATTERN_THUMB_SIZE = Pattern.compile("height:(\\d+)px; width:(\\d+)px"); + private static final Pattern PATTERN_THUMB_SIZE = Pattern.compile("height:(\\d+)px;width:(\\d+)px"); + private static final Pattern PATTERN_FAVORITE_SLOT = Pattern.compile("background-color:rgba\\((\\d+),(\\d+),(\\d+),"); + private static final Pattern PATTERN_PAGES = Pattern.compile("(\\d+) page"); + private static final Pattern PATTERN_NEXT_PAGE = Pattern.compile("page=(\\d+)"); + + private static final String[][] FAVORITE_SLOT_RGB = new String[][] { + new String[] { "0", "0", "0"}, + new String[] { "240", "0", "0"}, + new String[] { "240", "160", "0"}, + new String[] { "208", "208", "0"}, + new String[] { "0", "128", "0"}, + new String[] { "144", "240", "64"}, + new String[] { "64", "176", "240"}, + new String[] { "0", "0", "240"}, + new String[] { "80", "0", "128"}, + new String[] { "224", "128", "224"}, + }; public static class Result { public int pages; + public int nextPage; + public boolean noWatchedTags; public List galleryInfoList; } @@ -54,25 +71,25 @@ private static int parsePages(Document d, String body) throws ParseException { try { Elements es = d.getElementsByClass("ptt").first().child(0).child(0).children(); return Integer.parseInt(es.get(es.size() - 2).text().trim()); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throw new ParseException("Can't parse gallery list pages", body); } } private static String parseRating(String ratingStyle) { Matcher m = PATTERN_RATING.matcher(ratingStyle); - int num1; - int num2; + int num1 = Integer.MIN_VALUE; + int num2 = Integer.MIN_VALUE; int rate = 5; String re; if (m.find()) { - num1 = ParserUtils.parseInt(m.group().replace("px", "")); - } else { - return null; + num1 = ParserUtils.parseInt(m.group().replace("px", ""), Integer.MIN_VALUE); } if (m.find()) { - num2 = ParserUtils.parseInt(m.group().replace("px", "")); - } else { + num2 = ParserUtils.parseInt(m.group().replace("px", ""), Integer.MIN_VALUE); + } + if (num1 == Integer.MIN_VALUE || num2 == Integer.MIN_VALUE) { return null; } rate = rate - num1 / 16; @@ -85,96 +102,191 @@ private static String parseRating(String ratingStyle) { return re; } - @Nullable + private static int parseFavoriteSlot(String style) { + Matcher m = PATTERN_FAVORITE_SLOT.matcher(style); + if (m.find()) { + String r = m.group(1); + String g = m.group(2); + String b = m.group(3); + int slot = 0; + for (String[] rgb : FAVORITE_SLOT_RGB) { + if (r.equals(rgb[0]) && g.equals(rgb[1]) && b.equals(rgb[2])) { + return slot; + } + slot++; + } + } + return -2; + } + private static GalleryInfo parseGalleryInfo(Element e) { GalleryInfo gi = new GalleryInfo(); - // Get category - Element ic = JsoupUtils.getElementByClass(e, "ic"); - if (null != ic) { - gi.category = EhUtils.getCategory(ic.attr("alt").trim()); - } else { - Log.w(TAG, "Can't parse gallery info category"); - gi.category = EhUtils.UNKNOWN; + + // Title, gid, token (required), tags + Element glname = JsoupUtils.getElementByClass(e, "glname"); + if (glname != null) { + Element a = JsoupUtils.getElementByTag(glname, "a"); + if (a == null) { + Element parent = glname.parent(); + if (parent != null && "a".equals(parent.tagName())) { + a = parent; + } + } + if (a != null) { + GalleryDetailUrlParser.Result result = GalleryDetailUrlParser.parse(a.attr("href")); + if (result != null) { + gi.gid = result.gid; + gi.token = result.token; + } + } + + Element child = glname; + Elements children = glname.children(); + while (children.size() != 0) { + child = children.get(0); + children = child.children(); + } + gi.title = child.text().trim(); + + Element tbody = JsoupUtils.getElementByTag(glname, "tbody"); + if (tbody != null) { + ArrayList tags = new ArrayList<>(); + GalleryTagGroup[] groups = GalleryDetailParser.parseTagGroups(tbody.children()); + for (GalleryTagGroup group : groups) { + for (int j = 0; j < group.size(); j++) { + tags.add(group.groupName + ":" + group.getTagAt(j)); + } + } + gi.simpleTags = tags.toArray(new String[tags.size()]); + } } - // Posted - Element itd = JsoupUtils.getElementByClass(e, "itd"); - if (null != itd) { - gi.posted = itd.text().trim(); - } else { - Log.w(TAG, "Can't parse gallery info posted"); - gi.posted = ""; + if (gi.title == null) { + return null; + } + + // Category + gi.category = EhUtils.UNKNOWN; + Element ce = JsoupUtils.getElementByClass(e, "cn"); + if (ce == null) { + ce = JsoupUtils.getElementByClass(e, "cs"); } + if (ce != null) { + gi.category = EhUtils.getCategory(ce.text()); + } + // Thumb - Element it2 = JsoupUtils.getElementByClass(e, "it2"); - if (null != it2) { - // Thumb size - Matcher m = PATTERN_THUMB_SIZE.matcher(it2.attr("style")); - if (m.find()) { - gi.thumbWidth = NumberUtils.parseIntSafely(m.group(2), 0); - gi.thumbHeight = NumberUtils.parseIntSafely(m.group(1), 0); - } else { - Log.w(TAG, "Can't parse gallery info thumb size"); - gi.thumbWidth = 0; - gi.thumbHeight = 0; + Element glthumb = JsoupUtils.getElementByClass(e, "glthumb"); + if (glthumb != null) { + Element img = glthumb.select("div:nth-child(1)>img").first(); + if (img != null) { + // Thumb size + Matcher m = PATTERN_THUMB_SIZE.matcher(img.attr("style")); + if (m.find()) { + gi.thumbWidth = NumberUtils.parseIntSafely(m.group(2), 0); + gi.thumbHeight = NumberUtils.parseIntSafely(m.group(1), 0); + } else { + Log.w(TAG, "Can't parse gallery info thumb size"); + gi.thumbWidth = 0; + gi.thumbHeight = 0; + } + // Thumb url + String url = img.attr("data-src"); + if (TextUtils.isEmpty(url)) { + url = img.attr("src"); + } + if (TextUtils.isEmpty(url)) { + url = null; + } + gi.thumb = EhUtils.handleThumbUrlResolution(url); } - // Thumb url - Elements es = it2.children(); - if (null != es && es.size() >= 1) { - gi.thumb = EhUtils.handleThumbUrlResolution(es.get(0).attr("src")); - } else { - String html = it2.html(); - int index1 = html.indexOf('~'); - int index2 = StringUtils.ordinalIndexOf(html, '~', 2); - if (index1 < index2) { - gi.thumb = EhUtils.handleThumbUrlResolution( - "http://" +StringUtils.replace(html.substring(index1 + 1, index2), "~", "/")); - } else { - Log.w(TAG, "Can't parse gallery info thumb url"); - gi.thumb = ""; + // Pages + Element div = glthumb.select("div:nth-child(2)>div:nth-child(2)>div:nth-child(2)").first(); + if (div != null) { + Matcher matcher = PATTERN_PAGES.matcher(div.text()); + if (matcher.find()) { + gi.pages = NumberUtils.parseIntSafely(matcher.group(1), 0); } } - } else { - Log.w(TAG, "Can't parse gallery info thumb"); - gi.thumbWidth = 0; - gi.thumbHeight = 0; - gi.thumb = ""; } - // Title (required) - Element it5 = JsoupUtils.getElementByClass(e, "it5"); - if (null == it5) { - Log.e(TAG, "Can't parse gallery info title, step 1"); - return null; + // Try extended and thumbnail version + if (gi.thumb == null) { + Element gl = JsoupUtils.getElementByClass(e, "gl1e"); + if (gl == null) { + gl = JsoupUtils.getElementByClass(e, "gl3t"); + } + if (gl != null) { + Element img = JsoupUtils.getElementByTag(gl, "img"); + if (img != null) { + // Thumb size + Matcher m = PATTERN_THUMB_SIZE.matcher(img.attr("style")); + if (m.find()) { + gi.thumbWidth = NumberUtils.parseIntSafely(m.group(2), 0); + gi.thumbHeight = NumberUtils.parseIntSafely(m.group(1), 0); + } else { + Log.w(TAG, "Can't parse gallery info thumb size"); + gi.thumbWidth = 0; + gi.thumbHeight = 0; + } + gi.thumb = EhUtils.handleThumbUrlResolution(img.attr("src")); + } + } } - Elements es = it5.children(); - if (null == es || es.size() <= 0) { - Log.e(TAG, "Can't parse gallery info title, step 2"); - return null; + + // Posted + gi.favoriteSlot = -2; + Element posted = e.getElementById("posted_" + gi.gid); + if (posted != null) { + gi.posted = posted.text().trim(); + gi.favoriteSlot = parseFavoriteSlot(posted.attr("style")); } - Element a = es.get(0); - GalleryDetailUrlParser.Result result = GalleryDetailUrlParser.parse(a.attr("href")); - if (null == result) { - Log.e(TAG, "Can't parse gallery info title, step 3"); - return null; + if (gi.favoriteSlot == -2) { + gi.favoriteSlot = EhDB.containLocalFavorites(gi.gid) ? -1 : -2; } - gi.gid = result.gid; - gi.token = result.token; - gi.title = a.text().trim(); + // Rating - Element it4r = JsoupUtils.getElementByClass(e, "it4r"); - if (null != it4r) { - gi.rating = NumberUtils.parseFloatSafely(parseRating(it4r.attr("style")), -1.0f); - } else { - Log.w(TAG, "Can't parse gallery info rating"); - gi.rating = -1.0f; - } - // Uploader - Element itu = JsoupUtils.getElementByClass(e, "itu"); - if (null != itu) { - gi.uploader = itu.text().trim(); - } else { - Log.w(TAG, "Can't parse gallery info uploader"); - gi.uploader = ""; + Element ir = JsoupUtils.getElementByClass(e, "ir"); + if (ir != null) { + gi.rating = NumberUtils.parseFloatSafely(parseRating(ir.attr("style")), -1.0f); + // TODO The gallery may be rated even if it doesn't has one of these classes + gi.rated = ir.hasClass("irr") || ir.hasClass("irg") || ir.hasClass("irb"); + } + + // Uploader and pages + Element gl = JsoupUtils.getElementByClass(e, "glhide"); + int uploaderIndex = 0; + int pagesIndex = 1; + if (gl == null) { + // For extended + gl = JsoupUtils.getElementByClass(e, "gl3e"); + uploaderIndex = 3; + pagesIndex = 4; + } + if (gl != null) { + Elements children = gl.children(); + if (children.size() > uploaderIndex) { + Element a = children.get(uploaderIndex).children().first(); + if (a != null) { + gi.uploader = a.text().trim(); + } + } + if (children.size() > pagesIndex) { + Matcher matcher = PATTERN_PAGES.matcher(children.get(pagesIndex).text()); + if (matcher.find()) { + gi.pages = NumberUtils.parseIntSafely(matcher.group(1), 0); + } + } + } + // For thumbnail + Element gl5t = JsoupUtils.getElementByClass(e, "gl5t"); + if (gl5t != null) { + Element div = gl5t.select("div:nth-child(2)>div:nth-child(2)").first(); + if (div != null) { + Matcher matcher = PATTERN_PAGES.matcher(div.text()); + if (matcher.find()) { + gi.pages = NumberUtils.parseIntSafely(matcher.group(1), 0); + } + } } gi.generateSLang(); @@ -187,29 +299,59 @@ public static Result parse(@NonNull String body) throws Exception { Document d = Jsoup.parse(body); try { - result.pages = parsePages(d, body); - } catch (ParseException e) { + Element ptt = d.getElementsByClass("ptt").first(); + Elements es = ptt.child(0).child(0).children(); + result.pages = Integer.parseInt(es.get(es.size() - 2).text().trim()); + + Element e = es.get(es.size() - 1); + if (e != null) { + e = e.children().first(); + if (e != null) { + String href = e.attr("href"); + Matcher matcher = PATTERN_NEXT_PAGE.matcher(href); + if (matcher.find()) { + result.nextPage = NumberUtils.parseIntSafely(matcher.group(1), 0); + } + } + } + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); + result.noWatchedTags = body.contains("

You do not have any watched tags"); if (body.contains("No hits found

")) { result.pages = 0; //noinspection unchecked result.galleryInfoList = Collections.EMPTY_LIST; return result; + } else if (d.getElementsByClass("ptt").isEmpty()) { + result.pages = 1; } else { - throw e; + result.pages = Integer.MAX_VALUE; } } try { - Elements es = d.getElementsByClass("itg").first().child(0).children(); - List list = new ArrayList<>(es.size() - 1); - for (int i = 1; i < es.size(); i++) { // First one is table header, skip it + Element itg = d.getElementsByClass("itg").first(); + Elements es; + if ("table".equalsIgnoreCase(itg.tagName())) { + es = itg.child(0).children(); + } else { + es = itg.children(); + } + List list = new ArrayList<>(es.size()); + // First one is table header, skip it + for (int i = 0; i < es.size(); i++) { GalleryInfo gi = parseGalleryInfo(es.get(i)); if (null != gi) { list.add(gi); } } + if (list.isEmpty()) { + throw new ParseException("No gallery", body); + } result.galleryInfoList = list; - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); + e.printStackTrace(); throw new ParseException("Can't parse gallery list", body); } diff --git a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryPageApiParser.java b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryPageApiParser.java new file mode 100644 index 000000000..6180f2267 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryPageApiParser.java @@ -0,0 +1,74 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.client.parser; + +import android.text.TextUtils; +import com.hippo.ehviewer.client.exception.ParseException; +import com.hippo.yorozuya.StringUtils; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.json.JSONException; +import org.json.JSONObject; + +public class GalleryPageApiParser { + + private static final Pattern PATTERN_IMAGE_URL = Pattern.compile("]*src=\"([^\"]+)\" style"); + private static final Pattern PATTERN_SKIP_HATH_KEY = Pattern.compile("onclick=\"return nl\\('([^\\)]+)'\\)"); + private static final Pattern PATTERN_ORIGIN_IMAGE_URL = Pattern.compile(""); + + public static Result parse(String body) throws ParseException { + try { + Matcher m; + Result result = new Result(); + + JSONObject jo = new JSONObject(body); + if (jo.has("error")) { + throw new ParseException(jo.getString("error"), body); + } + + String i3 = jo.getString("i3"); + m = PATTERN_IMAGE_URL.matcher(i3); + if (m.find()) { + result.imageUrl = StringUtils.unescapeXml(StringUtils.trim(m.group(1))); + } + String i6 = jo.getString("i6"); + m = PATTERN_SKIP_HATH_KEY.matcher(i6); + if (m.find()) { + result.skipHathKey = StringUtils.unescapeXml(StringUtils.trim(m.group(1))); + } + String i7 = jo.getString("i7"); + m = PATTERN_ORIGIN_IMAGE_URL.matcher(i7); + if (m.find()) { + result.originImageUrl = StringUtils.unescapeXml(m.group(1)) + "fullimg.php" + StringUtils.unescapeXml(m.group(2)); + } + + if (!TextUtils.isEmpty(result.imageUrl)) { + return result; + } else { + throw new ParseException("Parse image url and skip hath key error", body); + } + } catch (JSONException e) { + throw new ParseException("Can't parse json", body, e); + } + } + + public static class Result { + public String imageUrl; + public String skipHathKey; + public String originImageUrl; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryPageParser.java b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryPageParser.java index 8efc3861c..f612d5a03 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryPageParser.java +++ b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryPageParser.java @@ -17,10 +17,8 @@ package com.hippo.ehviewer.client.parser; import android.text.TextUtils; - import com.hippo.ehviewer.client.exception.ParseException; import com.hippo.yorozuya.StringUtils; - import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -29,6 +27,8 @@ public class GalleryPageParser { private static final Pattern PATTERN_IMAGE_URL = Pattern.compile("]*src=\"([^\"]+)\" style"); private static final Pattern PATTERN_SKIP_HATH_KEY = Pattern.compile("onclick=\"return nl\\('([^\\)]+)'\\)"); private static final Pattern PATTERN_ORIGIN_IMAGE_URL = Pattern.compile(""); + // TODO Not sure about the size of show keys + private static final Pattern PATTERN_SHOW_KEY = Pattern.compile("var showkey=\"([0-9a-z]+)\";"); public static Result parse(String body) throws ParseException { Matcher m; @@ -45,11 +45,15 @@ public static Result parse(String body) throws ParseException { if (m.find()) { result.originImageUrl = StringUtils.unescapeXml(m.group(1)) + "fullimg.php" + StringUtils.unescapeXml(m.group(2)); } + m = PATTERN_SHOW_KEY.matcher(body); + if (m.find()) { + result.showKey = m.group(1); + } - if (!TextUtils.isEmpty(result.imageUrl)) { + if (!TextUtils.isEmpty(result.imageUrl) && !TextUtils.isEmpty(result.showKey)) { return result; } else { - throw new ParseException("Parse image url and skip hath key error", body); + throw new ParseException("Parse image url and show error", body); } } @@ -57,5 +61,6 @@ public static class Result { public String imageUrl; public String skipHathKey; public String originImageUrl; + public String showKey; } } diff --git a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryPageUrlParser.java b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryPageUrlParser.java index 41330f4ee..0e746d7ce 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryPageUrlParser.java +++ b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryPageUrlParser.java @@ -16,8 +16,9 @@ package com.hippo.ehviewer.client.parser; +import androidx.annotation.Nullable; import com.hippo.ehviewer.client.EhUrl; - +import com.hippo.yorozuya.NumberUtils; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,20 +27,33 @@ */ public final class GalleryPageUrlParser { - public static final Pattern URL_PATTERN = Pattern.compile("https?://(?:" + - EhUrl.DOMAIN_EX + "|" + EhUrl.DOMAIN_E + "|" + EhUrl.DOMAIN_LOFI + ")/s/(\\w+)/(\\d+)-(\\d+)"); + private static final Pattern URL_STRICT_PATTERN = Pattern.compile( + "https?://(?:" + EhUrl.DOMAIN_EX + "|" + EhUrl.DOMAIN_E + "|" + EhUrl.DOMAIN_LOFI + ")/s/([0-9a-f]{10})/(\\d+)-(\\d+)"); + + private static final Pattern URL_PATTERN = Pattern.compile( + "([0-9a-f]{10})/(\\d+)-(\\d+)"); + @Nullable public static Result parse(String url) { + return parse(url, true); + } + + @Nullable + public static Result parse(String url, boolean strict) { if (url == null) { return null; } - Matcher m = URL_PATTERN.matcher(url); + Pattern pattern = strict ? URL_STRICT_PATTERN : URL_PATTERN; + Matcher m = pattern.matcher(url); if (m.find()) { Result result = new Result(); - result.gid = Long.parseLong(m.group(2)); + result.gid = NumberUtils.parseLongSafely(m.group(2), -1L); result.pToken = m.group(1); - result.page = Integer.parseInt(m.group(3)) - 1; + result.page = NumberUtils.parseIntSafely(m.group(3), 0) - 1; + if (result.gid < 0 || result.page < 0) { + return null; + } return result; } else { return null; diff --git a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryTokenApiParser.java b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryTokenApiParser.java index a0589825f..774e988bf 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryTokenApiParser.java +++ b/app/src/main/java/com/hippo/ehviewer/client/parser/GalleryTokenApiParser.java @@ -17,7 +17,7 @@ package com.hippo.ehviewer.client.parser; import com.hippo.ehviewer.client.exception.EhException; - +import com.hippo.util.ExceptionUtils; import org.json.JSONObject; public class GalleryTokenApiParser { @@ -36,7 +36,8 @@ public static String parse(String body) throws Exception { JSONObject jo = new JSONObject(body).getJSONArray("tokenlist").getJSONObject(0); try { return jo.getString("token"); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throw new EhException(jo.getString("error")); } } diff --git a/app/src/main/java/com/hippo/ehviewer/client/parser/ParserUtils.java b/app/src/main/java/com/hippo/ehviewer/client/parser/ParserUtils.java index adcdb9a8f..b72c3f965 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/parser/ParserUtils.java +++ b/app/src/main/java/com/hippo/ehviewer/client/parser/ParserUtils.java @@ -16,6 +16,7 @@ package com.hippo.ehviewer.client.parser; +import com.hippo.yorozuya.NumberUtils; import com.hippo.yorozuya.StringUtils; import java.text.DateFormat; @@ -39,11 +40,15 @@ public static String trim(String str) { return StringUtils.unescapeXml(str).trim(); } - public static int parseInt(String str) { - return Integer.parseInt(trim(str).replace(",", "")); + public static int parseInt(String str, int defValue) { + return NumberUtils.parseIntSafely(trim(str).replace(",", ""), defValue); } - public static long parseLong(String str) { - return Long.parseLong(trim(str).replace(",", "")); + public static long parseLong(String str, long defValue) { + return NumberUtils.parseLongSafely(trim(str).replace(",", ""), defValue); + } + + public static float parseFloat(String str, float defValue) { + return NumberUtils.parseFloatSafely(trim(str).replace(",", ""), defValue); } } diff --git a/app/src/main/java/com/hippo/ehviewer/client/parser/ProfileParser.java b/app/src/main/java/com/hippo/ehviewer/client/parser/ProfileParser.java index df093bf83..37dc5661b 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/parser/ProfileParser.java +++ b/app/src/main/java/com/hippo/ehviewer/client/parser/ProfileParser.java @@ -18,10 +18,9 @@ import android.text.TextUtils; import android.util.Log; - import com.hippo.ehviewer.client.EhUrl; import com.hippo.ehviewer.client.exception.ParseException; - +import com.hippo.util.ExceptionUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; @@ -43,11 +42,13 @@ public static Result parse(String body) throws ParseException { } else if (!result.avatar.startsWith("http")) { result.avatar = EhUrl.URL_FORUMS + result.avatar; } - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); Log.i(TAG, "No avatar"); } return result; - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); throw new ParseException("Parse forums error", body); } } diff --git a/app/src/main/java/com/hippo/ehviewer/client/parser/WhatsHotParser.java b/app/src/main/java/com/hippo/ehviewer/client/parser/WhatsHotParser.java deleted file mode 100644 index 2289053d0..000000000 --- a/app/src/main/java/com/hippo/ehviewer/client/parser/WhatsHotParser.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.ehviewer.client.parser; - -import com.hippo.ehviewer.client.EhUtils; -import com.hippo.ehviewer.client.data.GalleryInfo; -import com.hippo.ehviewer.client.exception.ParseException; -import com.hippo.util.JsoupUtils; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -import java.util.ArrayList; -import java.util.List; - -public class WhatsHotParser { - - @SuppressWarnings("ConstantConditions") - public static List parse(String body) throws ParseException { - try { - List galleryInfoList = new ArrayList<>(15); - Document d = Jsoup.parse(body); - Element pp = d.getElementById("pp"); - Elements id1List = pp.getElementsByClass("id1"); - for (int i = 0, n = id1List.size(); i < n; i++) { - GalleryInfo galleryInfo = new GalleryInfo(); - Element id1 = id1List.get(i); - Element id3 = JsoupUtils.getElementByClass(id1, "id3"); - Element temp = JsoupUtils.getElementByTag(id3, "a"); - String url = temp.attr("href"); - GalleryDetailUrlParser.Result result = GalleryDetailUrlParser.parse(url); - galleryInfo.gid = result.gid; - galleryInfo.token = result.token; - temp = JsoupUtils.getElementByTag(temp, "img"); - galleryInfo.thumb = EhUtils.handleThumbUrlResolution(temp.attr("src")); - galleryInfo.title = temp.attr("title"); - galleryInfo.generateSLang(); - galleryInfoList.add(galleryInfo); - } - return galleryInfoList; - } catch (Exception e) { - throw new ParseException("Parse whats hot error", body); - } - } -} diff --git a/app/src/main/java/com/hippo/ehviewer/download/DownloadManager.java b/app/src/main/java/com/hippo/ehviewer/download/DownloadManager.java index 55655e689..d054ae2e6 100644 --- a/app/src/main/java/com/hippo/ehviewer/download/DownloadManager.java +++ b/app/src/main/java/com/hippo/ehviewer/download/DownloadManager.java @@ -16,17 +16,22 @@ package com.hippo.ehviewer.download; +import android.annotation.SuppressLint; import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import android.os.AsyncTask; import android.util.Log; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.hippo.ehviewer.EhDB; import com.hippo.ehviewer.client.data.GalleryInfo; import com.hippo.ehviewer.dao.DownloadInfo; import com.hippo.ehviewer.dao.DownloadLabel; +import com.hippo.ehviewer.spider.SpiderDen; +import com.hippo.ehviewer.spider.SpiderInfo; import com.hippo.ehviewer.spider.SpiderQueen; import com.hippo.image.Image; +import com.hippo.unifile.UniFile; +import com.hippo.util.IoThreadPoolExecutor; import com.hippo.yorozuya.ConcurrentPool; import com.hippo.yorozuya.MathUtils; import com.hippo.yorozuya.ObjectUtils; @@ -34,7 +39,7 @@ import com.hippo.yorozuya.collect.LongList; import com.hippo.yorozuya.collect.SparseIJArray; import com.hippo.yorozuya.collect.SparseJLArray; - +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -289,6 +294,9 @@ void startDownload(GalleryInfo galleryInfo, @Nullable String label) { } // Make sure download is running ensureDownload(); + + // Add it to history + EhDB.putHistoryInfo(info); } } @@ -560,6 +568,52 @@ public void deleteRangeDownload(LongList gidList) { for (DownloadInfoListener l: mDownloadInfoListeners) { l.onReload(); } + + // Ensure download + ensureDownload(); + } + + @SuppressLint("StaticFieldLeak") + public void resetAllReadingProgress() { + LinkedList list = new LinkedList<>(mAllInfoList); + + new AsyncTask() { + @Override + protected Void doInBackground(Void... voids) { + GalleryInfo galleryInfo = new GalleryInfo(); + for (DownloadInfo downloadInfo : list) { + galleryInfo.gid = downloadInfo.gid; + galleryInfo.token = downloadInfo.token; + galleryInfo.title = downloadInfo.title; + galleryInfo.thumb = downloadInfo.thumb; + galleryInfo.category = downloadInfo.category; + galleryInfo.posted = downloadInfo.posted; + galleryInfo.uploader = downloadInfo.uploader; + galleryInfo.rating = downloadInfo.rating; + + UniFile downloadDir = SpiderDen.getGalleryDownloadDir(galleryInfo); + if (downloadDir == null) { + continue; + } + UniFile file = downloadDir.findFile(".ehviewer"); + if (file == null) { + continue; + } + SpiderInfo spiderInfo = SpiderInfo.read(file); + if (spiderInfo == null) { + continue; + } + spiderInfo.startPage = 0; + + try { + spiderInfo.write(file.openOutputStream()); + } catch (IOException e) { + Log.e(TAG, "Can't write SpiderInfo", e); + } + } + return null; + } + }.executeOnExecutor(IoThreadPoolExecutor.getInstance()); } // Update in DB diff --git a/app/src/main/java/com/hippo/ehviewer/download/DownloadService.java b/app/src/main/java/com/hippo/ehviewer/download/DownloadService.java index 64c31b52f..63e83ff31 100644 --- a/app/src/main/java/com/hippo/ehviewer/download/DownloadService.java +++ b/app/src/main/java/com/hippo/ehviewer/download/DownloadService.java @@ -16,20 +16,21 @@ package com.hippo.ehviewer.download; +import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.graphics.BitmapFactory; +import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.SystemClock; -import android.support.annotation.IntDef; -import android.support.annotation.Nullable; -import android.support.v4.app.NotificationCompat; import android.util.Log; - +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.R; import com.hippo.ehviewer.client.EhUtils; @@ -44,7 +45,6 @@ import com.hippo.yorozuya.collect.LongList; import com.hippo.yorozuya.collect.SparseJBArray; import com.hippo.yorozuya.collect.SparseJLArray; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -90,6 +90,8 @@ public class DownloadService extends Service implements DownloadManager.Download private static int sFinishedCount; private static int sDownloadedCount; + private String CHANNEL_ID; + public static void clear() { sFailedCount = 0; sFinishedCount = 0; @@ -102,7 +104,12 @@ public static void clear() { public void onCreate() { super.onCreate(); + CHANNEL_ID = getPackageName()+".download"; mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){ + mNotifyManager.createNotificationChannel(new NotificationChannel(CHANNEL_ID, getString(R.string.download_service), + NotificationManager.IMPORTANCE_LOW)); + } mDownloadManager = EhApplication.getDownloadManager(getApplicationContext()); mDownloadManager.setDownloadListener(this); } @@ -208,14 +215,15 @@ private void ensureDownloadingBuilder() { stopAllIntent.setAction(ACTION_STOP_ALL); PendingIntent piStopAll = PendingIntent.getService(this, 0, stopAllIntent, 0); - mDownloadingBuilder = new NotificationCompat.Builder(getApplicationContext()) + mDownloadingBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID) .setSmallIcon(android.R.drawable.stat_sys_download) .setOngoing(true) .setAutoCancel(false) .setCategory(NotificationCompat.CATEGORY_PROGRESS) .setColor(getResources().getColor(R.color.colorPrimary)) .addAction(R.drawable.ic_pause_x24, getString(R.string.stat_download_action_stop_all), piStopAll) - .setShowWhen(false); + .setShowWhen(false) + .setChannelId(CHANNEL_ID); mDownloadingDelay = new NotificationDelay(this, mNotifyManager, mDownloadingBuilder, ID_DOWNLOADING); } @@ -238,14 +246,15 @@ private void ensureDownloadedBuilder() { PendingIntent piActivity = PendingIntent.getActivity(DownloadService.this, 0, activityIntent, PendingIntent.FLAG_UPDATE_CURRENT); - mDownloadedBuilder = new NotificationCompat.Builder(getApplicationContext()) + mDownloadedBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID) .setSmallIcon(android.R.drawable.stat_sys_download_done) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) .setContentTitle(getString(R.string.stat_download_done_title)) .setDeleteIntent(piClear) .setOngoing(false) .setAutoCancel(true) - .setContentIntent(piActivity); + .setContentIntent(piActivity) + .setChannelId(CHANNEL_ID); mDownloadedDelay = new NotificationDelay(this, mNotifyManager, mDownloadedBuilder, ID_DOWNLOADED); } @@ -255,14 +264,15 @@ private void ensure509Builder() { return; } - m509dBuilder = new NotificationCompat.Builder(getApplicationContext()) + m509dBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID) .setSmallIcon(R.drawable.ic_stat_alert) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) .setContentText(getString(R.string.stat_509_alert_title)) .setContentText(getString(R.string.stat_509_alert_text)) .setAutoCancel(true) .setOngoing(false) - .setCategory(NotificationCompat.CATEGORY_ERROR); + .setCategory(NotificationCompat.CATEGORY_ERROR) + .setChannelId(CHANNEL_ID); m509Delay = new NotificationDelay(this, mNotifyManager, m509dBuilder, ID_509); } diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/A7ZipArchive.java b/app/src/main/java/com/hippo/ehviewer/gallery/A7ZipArchive.java new file mode 100644 index 000000000..72529c135 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/A7ZipArchive.java @@ -0,0 +1,156 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery; + +import com.hippo.a7zip.ArchiveException; +import com.hippo.a7zip.InArchive; +import com.hippo.a7zip.InStream; +import com.hippo.a7zip.PropID; +import com.hippo.a7zip.PropType; +import com.hippo.a7zip.SequentialOutStream; +import com.hippo.unifile.UniRandomAccessFile; +import java.io.Closeable; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +class A7ZipArchive implements Closeable { + + private InArchive archive; + + private A7ZipArchive(InArchive archive) { + this.archive = archive; + } + + @Override + public void close() { + archive.close(); + } + + private static boolean isSupportedFilename(String name) { + for (String extension : GalleryProvider2.SUPPORT_IMAGE_EXTENSIONS) { + if (name.endsWith(extension)) { + return true; + } + } + return false; + } + + List getArchiveEntries() { + List entries = new ArrayList<>(); + + for (int i = 0, n = archive.getNumberOfEntries(); i < n; i++) { + if (!archive.getEntryBooleanProperty(i, PropID.ENCRYPTED) + && !archive.getEntryBooleanProperty(i, PropID.IS_DIR) + && !archive.getEntryBooleanProperty(i, PropID.IS_VOLUME) + && !archive.getEntryBooleanProperty(i, PropID.SOLID)) { + String path = archive.getEntryPath(i); + if (isSupportedFilename(path.toLowerCase())) { + entries.add(new A7ZipArchiveEntry(archive, i, path)); + } + } + } + + return entries; + } + + static A7ZipArchive create(UniRandomAccessFile file) throws ArchiveException { + InStream store = new UniRandomAccessFileInStream(file); + InArchive archive = InArchive.open(store); + if ((archive.getArchivePropertyType(PropID.ENCRYPTED) == PropType.BOOL && archive.getArchiveBooleanProperty(PropID.ENCRYPTED)) + || (archive.getArchivePropertyType(PropID.SOLID) == PropType.BOOL && archive.getArchiveBooleanProperty(PropID.SOLID)) + || (archive.getArchivePropertyType(PropID.IS_VOLUME) == PropType.BOOL && archive.getArchiveBooleanProperty(PropID.IS_VOLUME))) { + throw new ArchiveException("Unsupported archive"); + } + return new A7ZipArchive(archive); + } + + static class A7ZipArchiveEntry { + + private InArchive archive; + private int index; + private String path; + + private A7ZipArchiveEntry(InArchive archive, int index, String path) { + this.archive = archive; + this.index = index; + this.path = path; + } + + String getPath() { + return path; + } + + void extract(OutputStream os) throws ArchiveException { + archive.extractEntry(index, new OutputStreamSequentialOutStream(os)); + } + } + + private static class UniRandomAccessFileInStream implements InStream { + + private UniRandomAccessFile file; + + public UniRandomAccessFileInStream(UniRandomAccessFile file) { + this.file = file; + } + + @Override + public void seek(long pos) throws IOException { + file.seek(pos); + } + + @Override + public long tell() throws IOException { + return file.getFilePointer(); + } + + @Override + public long size() throws IOException { + return file.length(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return file.read(b, off, len); + } + + @Override + public void close() throws IOException { + file.close(); + } + } + + private static class OutputStreamSequentialOutStream implements SequentialOutStream { + + private OutputStream stream; + + public OutputStreamSequentialOutStream(OutputStream stream) { + this.stream = stream; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + stream.write(b, off, len); + } + + @Override + public void close() throws IOException { + stream.close(); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java b/app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java new file mode 100644 index 000000000..55c4ff217 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java @@ -0,0 +1,281 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery; + +import android.content.Context; +import android.net.Uri; +import android.os.Process; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.hippo.a7zip.ArchiveException; +import com.hippo.ehviewer.GetText; +import com.hippo.ehviewer.R; +import com.hippo.glgallery.GalleryPageView; +import com.hippo.image.Image; +import com.hippo.unifile.UniFile; +import com.hippo.unifile.UniRandomAccessFile; +import com.hippo.util.NaturalComparator; +import com.hippo.yorozuya.thread.PriorityThread; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.concurrent.atomic.AtomicInteger; + +public class ArchiveGalleryProvider extends GalleryProvider2 { + + private static final AtomicInteger sIdGenerator = new AtomicInteger(); + + private final UniFile file; + + private Thread archiveThread; + private Thread decodeThread; + + private volatile int size = STATE_WAIT; + private String error; + + private final Stack requests = new Stack<>(); + private final AtomicInteger extractingIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX); + private final LinkedHashMap streams = new LinkedHashMap<>(); + private final AtomicInteger decodingIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX); + + public ArchiveGalleryProvider(Context context, Uri uri) { + file = UniFile.fromUri(context, uri); + } + + @Override + public void start() { + super.start(); + + int id = sIdGenerator.incrementAndGet(); + + archiveThread = new PriorityThread( + new ArchiveTask(), "ArchiveTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND); + archiveThread.start(); + + decodeThread = new PriorityThread( + new DecodeTask(), "DecodeTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND); + decodeThread.start(); + } + + @Override + public void stop() { + super.stop(); + + if (archiveThread != null) { + archiveThread.interrupt(); + archiveThread = null; + } + if (decodeThread != null) { + decodeThread.interrupt(); + decodeThread = null; + } + } + + @Override + public int size() { + return size; + } + + @Override + protected void onRequest(int index) { + boolean inDecodeTask; + synchronized (streams) { + inDecodeTask = streams.keySet().contains(index) || index == decodingIndex.get(); + } + + synchronized (requests) { + boolean inArchiveTask = requests.contains(index) || index == extractingIndex.get(); + if (!inArchiveTask && !inDecodeTask) { + requests.add(index); + requests.notify(); + } + } + notifyPageWait(index); + } + + @Override + protected void onForceRequest(int index) { + onRequest(index); + } + + @Override + protected void onCancelRequest(int index) { + synchronized (requests) { + requests.remove(Integer.valueOf(index)); + } + } + + @Override + public String getError() { + return error; + } + + @NonNull + @Override + public String getImageFilename(int index) { + // TODO + return Integer.toString(index); + } + + @Override + public boolean save(int index, @NonNull UniFile file) { + // TODO + return false; + } + + @Nullable + @Override + public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) { + // TODO + return null; + } + + private class ArchiveTask implements Runnable { + @Override + public void run() { + UniRandomAccessFile uraf = null; + if (file != null) { + try { + uraf = file.createRandomAccessFile("r"); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (uraf == null) { + size = STATE_ERROR; + error = GetText.getString(R.string.error_reading_failed); + notifyDataChanged(); + return; + } + + A7ZipArchive archive = null; + try { + archive = A7ZipArchive.create(uraf); + } catch (ArchiveException e) { + e.printStackTrace(); + } + if (archive == null) { + size = STATE_ERROR; + error = GetText.getString(R.string.error_invalid_archive); + notifyDataChanged(); + return; + } + + List entries = archive.getArchiveEntries(); + Collections.sort(entries, naturalComparator); + + // Update size and notify changed + size = entries.size(); + notifyDataChanged(); + + while (!Thread.currentThread().isInterrupted()) { + int index; + synchronized (requests) { + if (requests.isEmpty()) { + try { + requests.wait(); + } catch (InterruptedException e) { + // Interrupted + break; + } + continue; + } + index = requests.pop(); + extractingIndex.lazySet(index); + } + + // Check index valid + if (index < 0 || index >= entries.size()) { + extractingIndex.lazySet(GalleryPageView.INVALID_INDEX); + notifyPageFailed(index, GetText.getString(R.string.error_out_of_range)); + continue; + } + + Pipe pipe = new Pipe(4 * 1024); + + synchronized (streams) { + if (streams.get(index) != null) { + continue; + } + streams.put(index, pipe.getInputStream()); + streams.notify(); + } + + try { + entries.get(index).extract(pipe.getOutputStream()); + } catch (ArchiveException e) { + e.printStackTrace(); + } finally { + extractingIndex.lazySet(GalleryPageView.INVALID_INDEX); + } + } + } + } + + private class DecodeTask implements Runnable { + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + int index; + InputStream stream; + synchronized (streams) { + if (streams.isEmpty()) { + try { + streams.wait(); + } catch (InterruptedException e) { + // Interrupted + break; + } + continue; + } + + Iterator> iterator = streams.entrySet().iterator(); + Map.Entry entry = iterator.next(); + iterator.remove(); + index = entry.getKey(); + stream = entry.getValue(); + decodingIndex.lazySet(index); + } + + try { + Image image = Image.decode(stream, true); + if (image != null) { + notifyPageSucceed(index, image); + } else { + notifyPageFailed(index, GetText.getString(R.string.error_decoding_failed)); + } + } finally { + decodingIndex.lazySet(GalleryPageView.INVALID_INDEX); + } + } + } + } + + private static Comparator naturalComparator = new Comparator() { + private NaturalComparator comparator = new NaturalComparator(); + @Override + public int compare(A7ZipArchive.A7ZipArchiveEntry o1, A7ZipArchive.A7ZipArchiveEntry o2) { + return comparator.compare(o1.getPath(), o2.getPath()); + } + }; +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/DirGalleryProvider.java b/app/src/main/java/com/hippo/ehviewer/gallery/DirGalleryProvider.java index 7ef64fd90..529ae7859 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/DirGalleryProvider.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/DirGalleryProvider.java @@ -17,26 +17,25 @@ package com.hippo.ehviewer.gallery; import android.os.Process; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.util.Log; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.hippo.ehviewer.GetText; import com.hippo.ehviewer.R; import com.hippo.glgallery.GalleryPageView; import com.hippo.image.Image; +import com.hippo.unifile.FilenameFilter; import com.hippo.unifile.UniFile; +import com.hippo.util.NaturalComparator; import com.hippo.yorozuya.FileUtils; import com.hippo.yorozuya.IOUtils; import com.hippo.yorozuya.StringUtils; import com.hippo.yorozuya.thread.PriorityThread; - -import java.io.File; -import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; +import java.util.Comparator; import java.util.Stack; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -173,7 +172,7 @@ public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) { @Override public void run() { // It may take a long time, so run it in new thread - UniFile[] files = mDir.listFiles(); + UniFile[] files = mDir.listFiles(imageFilter); if (files == null) { mSize = STATE_ERROR; @@ -187,7 +186,7 @@ public void run() { } // Sort it - Arrays.sort(files); + Arrays.sort(files, naturalComparator); // Put file list mFileList.lazySet(files); @@ -244,11 +243,14 @@ public void run() { Log.i(TAG, "ImageDecoder end"); } - private static class ImageFilter implements FilenameFilter { + private static FilenameFilter imageFilter = + (dir, name) -> StringUtils.endsWith(name.toLowerCase(), SUPPORT_IMAGE_EXTENSIONS); + private static Comparator naturalComparator = new Comparator() { + private NaturalComparator comparator = new NaturalComparator(); @Override - public boolean accept(File dir, String filename) { - return StringUtils.endsWith(filename.toLowerCase(), SUPPORT_IMAGE_EXTENSIONS); + public int compare(UniFile o1, UniFile o2) { + return comparator.compare(o1.getName(), o2.getName()); } - } + }; } diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/EhGalleryProvider.java b/app/src/main/java/com/hippo/ehviewer/gallery/EhGalleryProvider.java index 19d2f1d43..aee72473d 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/EhGalleryProvider.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/EhGalleryProvider.java @@ -17,16 +17,14 @@ package com.hippo.ehviewer.gallery; import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.hippo.ehviewer.client.data.GalleryInfo; import com.hippo.ehviewer.spider.SpiderQueen; import com.hippo.glgallery.GalleryProvider; import com.hippo.image.Image; import com.hippo.unifile.UniFile; import com.hippo.yorozuya.SimpleHandler; - import java.util.Locale; public class EhGalleryProvider extends GalleryProvider2 implements SpiderQueen.OnSpiderListener { @@ -73,7 +71,7 @@ public int getStartPage() { @NonNull @Override public String getImageFilename(int index) { - return String.format(Locale.US, "%d-%08d", mGalleryInfo.gid, index + 1); + return String.format(Locale.US, "%d-%s-%08d", mGalleryInfo.gid, mGalleryInfo.token, index + 1); } @Override diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/GalleryProvider2.java b/app/src/main/java/com/hippo/ehviewer/gallery/GalleryProvider2.java index 434cda239..d9a2b9d9d 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/GalleryProvider2.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/GalleryProvider2.java @@ -16,9 +16,8 @@ package com.hippo.ehviewer.gallery; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.hippo.glgallery.GalleryProvider; import com.hippo.unifile.UniFile; diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/Pipe.java b/app/src/main/java/com/hippo/ehviewer/gallery/Pipe.java new file mode 100644 index 000000000..0cdb4a169 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/gallery/Pipe.java @@ -0,0 +1,162 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.gallery; + +import androidx.annotation.NonNull; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +class Pipe { + + private final int capacity; + private final byte[] buffer; + + private int head = 0; + private int tail = 0; + private boolean full = false; + + private boolean inClosed = false; + private boolean outClosed = false; + + private InputStream inputStream = new InputStream() { + @Override + public int read() throws IOException { + synchronized (Pipe.this) { + byte[] bytes = new byte[1]; + if (read(bytes, 0, 1) != -1) { + return bytes[0]; + } else { + return -1; + } + } + } + + @Override + public int read(@NonNull byte[] b, int off, int len) throws IOException { + synchronized (Pipe.this) { + for (;;) { + if (inClosed) { + throw new IOException("The InputStream is closed"); + } + if (len == 0) { + return 0; + } + + if (head == tail && !full) { + if (outClosed) { + // No bytes available and the OutputStream is closed. So it's the end. + return -1; + } else { + // Wait for OutputStream write bytes + try { + Pipe.this.wait(); + } catch (InterruptedException e) { + throw new IOException("The thread interrupted", e); + } + } + } else { + int read = Math.min(len, (head < tail ? tail : capacity) - head); + System.arraycopy(buffer, head, b, off, read); + head += read; + if (head == capacity) { + head = 0; + } + full = false; + Pipe.this.notifyAll(); + return read; + } + } + } + } + + @Override + public void close() { + synchronized (Pipe.this) { + inClosed = true; + Pipe.this.notifyAll(); + } + } + }; + + private OutputStream outputStream = new OutputStream() { + @Override + public void write(int b) throws IOException { + synchronized (Pipe.this) { + byte[] bytes = new byte[] { (byte) b}; + write(bytes, 0, 1); + } + } + + @Override + public void write(@NonNull byte[] b, int off, int len) throws IOException { + synchronized (Pipe.this) { + while (len != 0) { + if (outClosed) { + throw new IOException("The OutputStream is closed"); + } + if (inClosed) { + throw new IOException("The InputStream is closed"); + } + + if (head == tail && full) { + // The buffer is full, wait for InputStream read bytes + try { + Pipe.this.wait(); + } catch (InterruptedException e) { + throw new IOException("The thread interrupted", e); + } + } else { + int write = Math.min(len, (head <= tail ? capacity : head) - tail); + System.arraycopy(b, off, buffer, tail, write); + off += write; + len -= write; + tail += write; + if (tail == capacity) { + tail = 0; + } + if (head == tail) { + full = true; + } + Pipe.this.notifyAll(); + } + } + } + } + + @Override + public void close() { + synchronized (Pipe.this) { + outClosed = true; + Pipe.this.notifyAll(); + } + } + }; + + Pipe(int capacity) { + this.capacity = capacity; + this.buffer = new byte[capacity]; + } + + InputStream getInputStream() { + return inputStream; + } + + OutputStream getOutputStream() { + return outputStream; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ZipGalleryProvider.java b/app/src/main/java/com/hippo/ehviewer/gallery/ZipGalleryProvider.java deleted file mode 100644 index 92765e256..000000000 --- a/app/src/main/java/com/hippo/ehviewer/gallery/ZipGalleryProvider.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.ehviewer.gallery; - -import android.os.Process; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.Log; - -import com.hippo.ehviewer.GetText; -import com.hippo.ehviewer.R; -import com.hippo.glgallery.GalleryPageView; -import com.hippo.image.Image; -import com.hippo.unifile.UniFile; -import com.hippo.yorozuya.StringUtils; -import com.hippo.yorozuya.thread.PriorityThread; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; -import java.util.List; -import java.util.Stack; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.zip.ZipEntry; -import java.util.zip.ZipException; -import java.util.zip.ZipFile; - -public class ZipGalleryProvider extends GalleryProvider2 implements Runnable { - - private static final String TAG = ZipGalleryProvider.class.getSimpleName(); - private static final AtomicInteger sIdGenerator = new AtomicInteger(); - - private final File mFile; - private final Stack mRequests = new Stack<>(); - private final AtomicInteger mDecodingIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX); - @Nullable - private Thread mBgThread; - private volatile int mSize = STATE_WAIT; - private String mError; - - public ZipGalleryProvider(File file) { - mFile = file; - } - - @Override - public void start() { - super.start(); - - mBgThread = new PriorityThread(this, TAG + '-' + sIdGenerator.incrementAndGet(), - Process.THREAD_PRIORITY_BACKGROUND); - mBgThread.start(); - } - - @Override - public void stop() { - super.stop(); - - if (mBgThread != null) { - mBgThread.interrupt(); - mBgThread = null; - } - } - - @Override - public int size() { - return mSize; - } - - @Override - protected void onRequest(int index) { - synchronized (mRequests) { - if (!mRequests.contains(index) && index != mDecodingIndex.get()) { - mRequests.add(index); - mRequests.notify(); - } - } - notifyPageWait(index); - } - - @Override - protected void onForceRequest(int index) { - onRequest(index); - } - - @Override - protected void onCancelRequest(int index) { - synchronized (mRequests) { - mRequests.remove(Integer.valueOf(index)); - } - } - - @Override - public String getError() { - return mError; - } - - @NonNull - @Override - public String getImageFilename(int index) { - // TODO - return Integer.toString(index); - } - - @Override - public boolean save(int index, @NonNull UniFile file) { - // TODO - return false; - } - - @Nullable - @Override - public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) { - // TODO - return null; - } - - @Override - public void run() { - ZipFile zipFile = null; - try { - zipFile = new ZipFile(mFile); - } catch (ZipException e) { - mError = GetText.getString(R.string.error_invalid_zip_file); - } catch (FileNotFoundException e) { - mError = GetText.getString(R.string.error_not_found); - } catch (IOException e) { - mError = GetText.getString(R.string.error_reading_failed); - } - - // Check zip file null - if (zipFile == null) { - mSize = STATE_ERROR; - - // Notify to to show error - notifyDataChanged(); - - Log.i(TAG, "ImageDecoder end with error"); - return; - } - - // Get all image name - List filenames = new ArrayList<>(zipFile.size()); - Enumeration enumeration = zipFile.entries(); - while (enumeration.hasMoreElements()) { - ZipEntry zipEntry = enumeration.nextElement(); - String filename = zipEntry.getName(); - if (!zipEntry.isDirectory() && StringUtils.endsWith(filename, SUPPORT_IMAGE_EXTENSIONS)) { - filenames.add(filename); - } - } - Collections.sort(filenames); - - // Update size and notify changed - mSize = filenames.size(); - notifyDataChanged(); - - while (!Thread.currentThread().isInterrupted()) { - int index; - synchronized (mRequests) { - if (mRequests.isEmpty()) { - try { - mRequests.wait(); - } catch (InterruptedException e) { - // Interrupted - break; - } - continue; - } - index = mRequests.pop(); - mDecodingIndex.lazySet(index); - } - - // Check index valid - if (index < 0 || index >= filenames.size()) { - mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX); - notifyPageFailed(index, GetText.getString(R.string.error_out_of_range)); - continue; - } - - try { - ZipEntry zipEntry = zipFile.getEntry(filenames.get(index)); - if (zipEntry != null) { - InputStream is = zipFile.getInputStream(zipEntry); - Image image = Image.decode(is, true); - mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX); - if (image != null) { - notifyPageSucceed(index, image); - } else { - notifyPageFailed(index, GetText.getString(R.string.error_decoding_failed)); - } - } else { - mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX); - notifyPageFailed(index, GetText.getString(R.string.error_reading_failed)); - } - } catch (IOException e) { - mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX); - notifyPageFailed(index, GetText.getString(R.string.error_reading_failed)); - } - } - - // Clear - try { - zipFile.close(); - } catch (IOException e) { - // Ignore - } - - Log.i(TAG, "ImageDecoder end"); - } -} diff --git a/app/src/main/java/com/hippo/ehviewer/preference/CleanRedundancyPreference.java b/app/src/main/java/com/hippo/ehviewer/preference/CleanRedundancyPreference.java index 99665277f..1c0abae65 100644 --- a/app/src/main/java/com/hippo/ehviewer/preference/CleanRedundancyPreference.java +++ b/app/src/main/java/com/hippo/ehviewer/preference/CleanRedundancyPreference.java @@ -17,10 +17,9 @@ package com.hippo.ehviewer.preference; import android.content.Context; -import android.support.annotation.NonNull; import android.util.AttributeSet; import android.widget.Toast; - +import androidx.annotation.NonNull; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; @@ -62,6 +61,9 @@ public ClearTask(@NonNull Context context) { // True for cleared private boolean clearFile(UniFile file) { String name = file.getName(); + if (name == null) { + return false; + } int index = name.indexOf('-'); if (index >= 0) { name = name.substring(0, index); diff --git a/app/src/main/java/com/hippo/ehviewer/preference/ClearDownloadPathCachePreference.java b/app/src/main/java/com/hippo/ehviewer/preference/ClearDownloadPathCachePreference.java index d4f0e4e28..96a51fd14 100644 --- a/app/src/main/java/com/hippo/ehviewer/preference/ClearDownloadPathCachePreference.java +++ b/app/src/main/java/com/hippo/ehviewer/preference/ClearDownloadPathCachePreference.java @@ -17,9 +17,8 @@ package com.hippo.ehviewer.preference; import android.content.Context; -import android.support.v7.app.AlertDialog; import android.util.AttributeSet; - +import androidx.appcompat.app.AlertDialog; import com.hippo.ehviewer.EhDB; import com.hippo.ehviewer.R; import com.hippo.preference.MessagePreference; diff --git a/app/src/main/java/com/hippo/ehviewer/preference/DefaultCategoryPreference.java b/app/src/main/java/com/hippo/ehviewer/preference/DefaultCategoryPreference.java index cd3fb7203..ce3b57af1 100644 --- a/app/src/main/java/com/hippo/ehviewer/preference/DefaultCategoryPreference.java +++ b/app/src/main/java/com/hippo/ehviewer/preference/DefaultCategoryPreference.java @@ -17,10 +17,9 @@ package com.hippo.ehviewer.preference; import android.content.Context; -import android.support.annotation.Nullable; -import android.support.v7.app.AlertDialog; import android.util.AttributeSet; - +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.widget.CategoryTable; diff --git a/app/src/main/java/com/hippo/ehviewer/preference/ExcludedTagNamespacesPreference.java b/app/src/main/java/com/hippo/ehviewer/preference/ExcludedTagNamespacesPreference.java index 185cb12b1..f27ef6837 100644 --- a/app/src/main/java/com/hippo/ehviewer/preference/ExcludedTagNamespacesPreference.java +++ b/app/src/main/java/com/hippo/ehviewer/preference/ExcludedTagNamespacesPreference.java @@ -17,11 +17,10 @@ package com.hippo.ehviewer.preference; import android.content.Context; -import android.support.v7.app.AlertDialog; import android.util.AttributeSet; import android.view.View; import android.widget.CheckBox; - +import androidx.appcompat.app.AlertDialog; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; import com.hippo.preference.DialogPreference; diff --git a/app/src/main/java/com/hippo/ehviewer/preference/ExportDataPreference.java b/app/src/main/java/com/hippo/ehviewer/preference/ExportDataPreference.java new file mode 100644 index 000000000..6d647b66f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/preference/ExportDataPreference.java @@ -0,0 +1,78 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.preference; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Toast; +import androidx.annotation.NonNull; +import com.hippo.ehviewer.AppConfig; +import com.hippo.ehviewer.EhDB; +import com.hippo.ehviewer.GetText; +import com.hippo.ehviewer.R; +import com.hippo.util.ReadableTime; +import java.io.File; + +public class ExportDataPreference extends TaskPreference { + + public ExportDataPreference(Context context) { + super(context); + } + + public ExportDataPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ExportDataPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @NonNull + @Override + protected Task onCreateTask() { + return new ExportDataTask(getContext()); + } + + private static class ExportDataTask extends Task { + + public ExportDataTask(@NonNull Context context) { + super(context); + } + + @Override + protected Object doInBackground(Void... voids) { + File dir = AppConfig.getExternalDataDir(); + if (dir != null) { + File file = new File(dir, ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".db"); + if (EhDB.exportDB(getApplication(), file)) { + return file; + } + } + return null; + } + + @Override + protected void onPostExecute(Object o) { + Toast.makeText(getApplication(), + (o instanceof File) + ? GetText.getString(R.string.settings_advanced_export_data_to, ((File) o).getPath()) + : GetText.getString(R.string.settings_advanced_export_data_failed), + Toast.LENGTH_SHORT).show(); + super.onPostExecute(o); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/preference/IdentityCookiePreference.java b/app/src/main/java/com/hippo/ehviewer/preference/IdentityCookiePreference.java new file mode 100644 index 000000000..ee2964423 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/preference/IdentityCookiePreference.java @@ -0,0 +1,105 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.preference; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Toast; +import androidx.appcompat.app.AlertDialog; +import com.hippo.ehviewer.EhApplication; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.client.EhCookieStore; +import com.hippo.ehviewer.client.EhUrl; +import com.hippo.preference.MessagePreference; +import com.hippo.text.Html; +import java.util.LinkedList; +import java.util.List; +import okhttp3.Cookie; +import okhttp3.HttpUrl; + +public class IdentityCookiePreference extends MessagePreference { + + private String message; + + public IdentityCookiePreference(Context context) { + super(context); + init(); + } + + public IdentityCookiePreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public IdentityCookiePreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + EhCookieStore store = EhApplication.getEhCookieStore(getContext()); + List eCookies = store.getCookies(HttpUrl.get(EhUrl.HOST_E)); + List exCookies = store.getCookies(HttpUrl.get(EhUrl.HOST_EX)); + List cookies = new LinkedList<>(eCookies); + cookies.addAll(exCookies); + + String ipbMemberId = null; + String ipbPassHash = null; + String igneous = null; + + for (int i = 0, n = cookies.size(); i < n; i++) { + Cookie cookie = cookies.get(i); + switch (cookie.name()) { + case EhCookieStore.KEY_IPD_MEMBER_ID: + ipbMemberId = cookie.value(); + break; + case EhCookieStore.KEY_IPD_PASS_HASH: + ipbPassHash = cookie.value(); + break; + case EhCookieStore.KEY_IGNEOUS: + igneous = cookie.value(); + break; + } + } + + if (ipbMemberId != null || ipbPassHash != null || igneous != null) { + message = EhCookieStore.KEY_IPD_MEMBER_ID + ": " + ipbMemberId + "
" + + EhCookieStore.KEY_IPD_PASS_HASH + ": " + ipbPassHash + "
" + + EhCookieStore.KEY_IGNEOUS + ": " + igneous; + setDialogMessage(Html.fromHtml(getContext().getString(R.string.settings_eh_identity_cookies_signed, message))); + message = message.replace("
", "\n"); + } else { + setDialogMessage(getContext().getString(R.string.settings_eh_identity_cookies_tourist)); + } + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + super.onPrepareDialogBuilder(builder); + if (message != null) { + builder.setPositiveButton(R.string.settings_eh_identity_cookies_copy, (dialog, which) -> { + ClipboardManager cmb = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); + cmb.setPrimaryClip(ClipData.newPlainText(null, message)); + Toast.makeText(getContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); + + IdentityCookiePreference.this.onClick(dialog, which); + }); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/preference/HatHProxyPreference.java b/app/src/main/java/com/hippo/ehviewer/preference/ProxyPreference.java similarity index 57% rename from app/src/main/java/com/hippo/ehviewer/preference/HatHProxyPreference.java rename to app/src/main/java/com/hippo/ehviewer/preference/ProxyPreference.java index 02e1dd3f7..c5e14a095 100644 --- a/app/src/main/java/com/hippo/ehviewer/preference/HatHProxyPreference.java +++ b/app/src/main/java/com/hippo/ehviewer/preference/ProxyPreference.java @@ -20,64 +20,71 @@ import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; -import android.support.annotation.Nullable; -import android.support.design.widget.TextInputLayout; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.SwitchCompat; +import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.widget.EditText; - +import android.widget.Spinner; +import androidx.appcompat.app.AlertDialog; +import com.google.android.material.textfield.TextInputLayout; +import com.hippo.ehviewer.EhApplication; +import com.hippo.ehviewer.EhProxySelector; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; import com.hippo.network.InetValidator; import com.hippo.preference.DialogPreference; +import com.hippo.yorozuya.MathUtils; import com.hippo.yorozuya.ViewUtils; -public class HatHProxyPreference extends DialogPreference implements View.OnClickListener { +public class ProxyPreference extends DialogPreference implements View.OnClickListener { - @Nullable - private SwitchCompat mEnable; - @Nullable + private Spinner mType; private TextInputLayout mIpInputLayout; - @Nullable private EditText mIp; - @Nullable private TextInputLayout mPortInputLayout; - @Nullable private EditText mPort; - @Nullable - private TextInputLayout mPasskeyInputLayout; - @Nullable - private EditText mPasskey; - public HatHProxyPreference(Context context) { + public ProxyPreference(Context context) { super(context); init(); } - public HatHProxyPreference(Context context, AttributeSet attrs) { + public ProxyPreference(Context context, AttributeSet attrs) { super(context, attrs); init(); } - public HatHProxyPreference(Context context, AttributeSet attrs, int defStyleAttr) { + public ProxyPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { - setDialogLayoutResource(R.layout.preference_dialog_hath_proxy); - updateSummary(Settings.getHathProxy(), Settings.getHathIp(), Settings.getHathPort()); + setDialogLayoutResource(R.layout.preference_dialog_proxy); + updateSummary(Settings.getProxyType(), Settings.getProxyIp(), Settings.getProxyPort()); + } + + private String getProxyTypeText(Context context, int type) { + String[] array = context.getResources().getStringArray(R.array.proxy_types); + return array[MathUtils.clamp(type, 0, array.length - 1)]; } - private void updateSummary(boolean hathProxy, String hathIp, int hathPort) { - if (InetValidator.isValidInet4Address(hathIp) && InetValidator.isValidInetPort(hathPort)) { + private void updateSummary(int type, String ip, int port) { + if ((type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) + && (TextUtils.isEmpty(ip) || !InetValidator.isValidInetPort(port)) ) { + type = EhProxySelector.TYPE_SYSTEM; + } + + if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) { Context context = getContext(); - setSummary(context.getString(R.string.settings_eh_hath_proxy_summary_1, - context.getString(hathProxy ? R.string.enabled : R.string.disabled), hathIp, hathPort)); + setSummary(context.getString(R.string.settings_advanced_proxy_summary_1, + getProxyTypeText(context, type), + ip, + port)); } else { - setSummary(R.string.settings_eh_hath_proxy_summary_2); + Context context = getContext(); + setSummary(context.getString(R.string.settings_advanced_proxy_summary_2, + getProxyTypeText(context, type))); } } @@ -94,73 +101,63 @@ protected void onDialogCreated(AlertDialog dialog) { dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(this); - mEnable = (SwitchCompat) ViewUtils.$$(dialog, R.id.enable); + mType = (Spinner) ViewUtils.$$(dialog, R.id.type); mIpInputLayout = (TextInputLayout) ViewUtils.$$(dialog, R.id.ip_input_layout); mIp = (EditText) ViewUtils.$$(dialog, R.id.ip); mPortInputLayout = (TextInputLayout) ViewUtils.$$(dialog, R.id.port_input_layout); mPort = (EditText) ViewUtils.$$(dialog, R.id.port); - mPasskeyInputLayout = (TextInputLayout) ViewUtils.$$(dialog, R.id.passkey_input_layout); - mPasskey = (EditText) ViewUtils.$$(dialog, R.id.passkey); - mEnable.setChecked(Settings.getHathProxy()); - String ip = Settings.getHathIp(); - if (!InetValidator.isValidInet4Address(ip)) { - ip = null; - } + int type = Settings.getProxyType(); + String[] array = getContext().getResources().getStringArray(R.array.proxy_types); + mType.setSelection(MathUtils.clamp(type, 0, array.length)); + + mIp.setText(Settings.getProxyIp()); + String portString; - int port = Settings.getHathPort(); + int port = Settings.getProxyPort(); if (!InetValidator.isValidInetPort(port)) { portString = null; } else { - portString = Integer.toString(Settings.getHathPort()); + portString = Integer.toString(port); } - mIp.setText(ip); mPort.setText(portString); - mPasskey.setText(Settings.getHathPasskey()); } @Override protected void onDialogClosed(boolean positiveResult) { super.onDialogClosed(positiveResult); - mEnable = null; + mType = null; mIpInputLayout = null; mIp = null; mPortInputLayout = null; mPort = null; - mPasskeyInputLayout = null; - mPasskey = null; } @Override public void onClick(View v) { Dialog dialog = getDialog(); Context context = getContext(); - if (null == dialog || null == context || null == mEnable || + if (null == dialog || null == context || null == mType || null == mIpInputLayout || null == mIp || - null == mPortInputLayout || null == mPort || - null == mPasskeyInputLayout || null == mPasskey) { + null == mPortInputLayout || null == mPort) { return; } - boolean enable = mEnable.isChecked(); + int type = mType.getSelectedItemPosition(); String ip = mIp.getText().toString().trim(); if (ip.isEmpty()) { - if (enable) { + if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) { mIpInputLayout.setError(context.getString(R.string.text_is_empty)); return; } - } else if (!InetValidator.isValidInet4Address(ip)) { - mIpInputLayout.setError(context.getString(R.string.domain_not_supported)); - return; - } else { - mIpInputLayout.setError(null); } + mIpInputLayout.setError(null); int port; String portString = mPort.getText().toString().trim(); if (portString.isEmpty()) { - if (enable) { + if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) { mPortInputLayout.setError(context.getString(R.string.text_is_empty)); return; } else { @@ -173,19 +170,19 @@ public void onClick(View v) { port = -1; } if (!InetValidator.isValidInetPort(port)) { - mPortInputLayout.setError(context.getString(R.string.invalid_port)); + mPortInputLayout.setError(context.getString(R.string.proxy_invalid_port)); return; } } + mPortInputLayout.setError(null); - String passkey = mPasskey.getText().toString(); + Settings.putProxyType(type); + Settings.putProxyIp(ip); + Settings.putProxyPort(port); - Settings.putHathProxy(enable); - Settings.putHathIp(ip); - Settings.putHathPort(port); - Settings.putHathPasskey(passkey); + updateSummary(type, ip, port); - updateSummary(enable, ip, port); + EhApplication.getEhProxySelector(getContext()).updateProxy(); dialog.dismiss(); } diff --git a/app/src/main/java/com/hippo/ehviewer/preference/RestoreDownloadPreference.java b/app/src/main/java/com/hippo/ehviewer/preference/RestoreDownloadPreference.java index be5879127..61b322301 100644 --- a/app/src/main/java/com/hippo/ehviewer/preference/RestoreDownloadPreference.java +++ b/app/src/main/java/com/hippo/ehviewer/preference/RestoreDownloadPreference.java @@ -20,28 +20,27 @@ import android.content.Context; import android.os.Parcel; import android.preference.Preference; -import android.support.annotation.NonNull; import android.util.AttributeSet; import android.widget.Toast; - +import androidx.annotation.NonNull; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.EhDB; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.client.EhEngine; +import com.hippo.ehviewer.client.EhUrl; import com.hippo.ehviewer.client.data.GalleryInfo; import com.hippo.ehviewer.download.DownloadManager; import com.hippo.ehviewer.spider.SpiderInfo; import com.hippo.ehviewer.spider.SpiderQueen; import com.hippo.unifile.UniFile; +import com.hippo.util.ExceptionUtils; import com.hippo.yorozuya.IOUtils; - import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; - import okhttp3.OkHttpClient; public class RestoreDownloadPreference extends TaskPreference { @@ -120,6 +119,10 @@ protected Object doInBackground(Void... params) { List restoreItemList = new ArrayList<>(); UniFile[] files = dir.listFiles(); + if (files == null) { + return null; + } + for (UniFile file: files) { RestoreItem restoreItem = getRestoreItem(file); if (null != restoreItem) { @@ -132,8 +135,9 @@ protected Object doInBackground(Void... params) { } try { - return EhEngine.fillGalleryListByApi(null, mHttpClient, new ArrayList(restoreItemList)); - } catch (Exception e) { + return EhEngine.fillGalleryListByApi(null, mHttpClient, new ArrayList(restoreItemList), EhUrl.getReferer()); + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); e.printStackTrace(); return null; } diff --git a/app/src/main/java/com/hippo/ehviewer/preference/SignInRequiredActivityPreference.java b/app/src/main/java/com/hippo/ehviewer/preference/SignInRequiredActivityPreference.java new file mode 100644 index 000000000..0160b1bd6 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/preference/SignInRequiredActivityPreference.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.preference; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; +import com.google.android.material.snackbar.Snackbar; +import com.hippo.ehviewer.EhApplication; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.client.EhCookieStore; +import com.hippo.ehviewer.client.EhUrl; +import com.hippo.preference.ActivityPreference; +import okhttp3.HttpUrl; + +public class SignInRequiredActivityPreference extends ActivityPreference { + + private View view; + + public SignInRequiredActivityPreference(Context context) { + super(context); + } + + public SignInRequiredActivityPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SignInRequiredActivityPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected View onCreateView(ViewGroup parent) { + return view = super.onCreateView(parent); + } + + @Override + protected void onClick() { + EhCookieStore store = EhApplication.getEhCookieStore(getContext()); + HttpUrl e = HttpUrl.get(EhUrl.HOST_E); + HttpUrl ex = HttpUrl.get(EhUrl.HOST_EX); + + if (store.contains(e, EhCookieStore.KEY_IPD_MEMBER_ID) || + store.contains(e, EhCookieStore.KEY_IPD_PASS_HASH) || + store.contains(ex, EhCookieStore.KEY_IPD_MEMBER_ID) || + store.contains(ex, EhCookieStore.KEY_IPD_PASS_HASH)) { + super.onClick(); + } else { + if (view != null) { + Snackbar.make(view, R.string.error_please_login_first, 3000).show(); + } else { + Toast.makeText(getContext(), R.string.error_please_login_first, Toast.LENGTH_LONG).show(); + } + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/preference/SignOutPreference.java b/app/src/main/java/com/hippo/ehviewer/preference/SignOutPreference.java index e87f6da1a..e180f7656 100644 --- a/app/src/main/java/com/hippo/ehviewer/preference/SignOutPreference.java +++ b/app/src/main/java/com/hippo/ehviewer/preference/SignOutPreference.java @@ -17,10 +17,9 @@ package com.hippo.ehviewer.preference; import android.content.Context; -import android.support.v7.app.AlertDialog; import android.util.AttributeSet; import android.widget.Toast; - +import androidx.appcompat.app.AlertDialog; import com.hippo.ehviewer.R; import com.hippo.ehviewer.client.EhUtils; import com.hippo.preference.MessagePreference; diff --git a/app/src/main/java/com/hippo/ehviewer/preference/TaskPreference.java b/app/src/main/java/com/hippo/ehviewer/preference/TaskPreference.java index 7891394aa..5aa2f525b 100644 --- a/app/src/main/java/com/hippo/ehviewer/preference/TaskPreference.java +++ b/app/src/main/java/com/hippo/ehviewer/preference/TaskPreference.java @@ -21,15 +21,16 @@ import android.os.AsyncTask; import android.os.Parcel; import android.os.Parcelable; -import android.support.annotation.CallSuper; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.app.AlertDialog; import android.util.AttributeSet; - +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.customview.view.AbsSavedState; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.R; import com.hippo.preference.DialogPreference; +import com.hippo.util.IoThreadPoolExecutor; import com.hippo.yorozuya.IntIdGenerator; public abstract class TaskPreference extends DialogPreference { @@ -62,7 +63,7 @@ protected void onDialogCreated(AlertDialog dialog) { mTask = onCreateTask(); mTask.setPreference(this); mTaskId = ((EhApplication) getContext().getApplicationContext()).putGlobalStuff(mTask); - mTask.execute(); + mTask.executeOnExecutor(IoThreadPoolExecutor.getInstance()); } } @@ -123,11 +124,11 @@ protected void onRestoreInstanceState(Parcelable state) { super.onRestoreInstanceState(myState.getSuperState()); } - private static class SavedState extends BaseSavedState { + private static class SavedState extends AbsSavedState { int asyncTaskId; public SavedState(Parcel source) { - super(source); + super(source, SavedState.class.getClassLoader()); asyncTaskId = source.readInt(); } @@ -165,6 +166,10 @@ public Task(@NonNull Context context) { mApplication = (EhApplication) context.getApplicationContext(); } + public EhApplication getApplication() { + return mApplication; + } + @Nullable public TaskPreference getPreference() { return mPreference; diff --git a/app/src/main/java/com/hippo/ehviewer/shortcuts/ShortcutsActivity.java b/app/src/main/java/com/hippo/ehviewer/shortcuts/ShortcutsActivity.java new file mode 100644 index 000000000..2987bde1e --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/shortcuts/ShortcutsActivity.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.shortcuts; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +import com.hippo.ehviewer.download.DownloadService; + +/** + * Created by onlymash on 3/25/18. + */ + +public class ShortcutsActivity extends Activity{ + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String action = null; + Intent intent = getIntent(); + if (intent != null){ + action = intent.getAction(); + if (action != null && (action.equals(DownloadService.ACTION_START_ALL) || + action.equals(DownloadService.ACTION_STOP_ALL))){ + startService(new Intent(this, DownloadService.class).setAction(action)); + } + } + finish(); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.java b/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.java index fce40aa0e..321b30e41 100644 --- a/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.java +++ b/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.java @@ -18,9 +18,8 @@ import android.content.Context; import android.graphics.BitmapFactory; -import android.support.annotation.Nullable; import android.webkit.MimeTypeMap; - +import androidx.annotation.Nullable; import com.hippo.beerbelly.SimpleDiskCache; import com.hippo.ehviewer.EhDB; import com.hippo.ehviewer.Settings; @@ -38,7 +37,6 @@ import com.hippo.yorozuya.IOUtils; import com.hippo.yorozuya.MathUtils; import com.hippo.yorozuya.Utilities; - import java.io.File; import java.io.IOException; import java.io.OutputStream; @@ -323,7 +321,12 @@ private OutputStreamPipe openDownloadOutputStreamPipe(int index, @Nullable Strin @Nullable public OutputStreamPipe openOutputStreamPipe(int index, @Nullable String extension) { if (mMode == SpiderQueen.MODE_READ) { - return openCacheOutputStreamPipe(index); + // Return the download pipe is the gallery has been downloaded + OutputStreamPipe pipe = openDownloadOutputStreamPipe(index, extension); + if (pipe == null) { + pipe = openCacheOutputStreamPipe(index); + } + return pipe; } else if (mMode == SpiderQueen.MODE_DOWNLOAD) { return openDownloadOutputStreamPipe(index, extension); } else { diff --git a/app/src/main/java/com/hippo/ehviewer/spider/SpiderInfo.java b/app/src/main/java/com/hippo/ehviewer/spider/SpiderInfo.java index 98747bb8c..11ef800ed 100644 --- a/app/src/main/java/com/hippo/ehviewer/spider/SpiderInfo.java +++ b/app/src/main/java/com/hippo/ehviewer/spider/SpiderInfo.java @@ -16,16 +16,14 @@ package com.hippo.ehviewer.spider; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.hippo.unifile.UniFile; import com.hippo.yorozuya.IOUtils; import com.hippo.yorozuya.NumberUtils; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; diff --git a/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java b/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java index 91ece4302..e53e79917 100644 --- a/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java +++ b/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java @@ -20,21 +20,19 @@ import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.os.Process; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.UiThread; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import android.webkit.MimeTypeMap; - +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; import com.hippo.beerbelly.SimpleDiskCache; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.GetText; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; -import com.hippo.ehviewer.client.EhConfig; import com.hippo.ehviewer.client.EhEngine; import com.hippo.ehviewer.client.EhRequestBuilder; import com.hippo.ehviewer.client.EhUrl; @@ -43,6 +41,7 @@ import com.hippo.ehviewer.client.exception.Image509Exception; import com.hippo.ehviewer.client.exception.ParseException; import com.hippo.ehviewer.client.parser.GalleryDetailParser; +import com.hippo.ehviewer.client.parser.GalleryPageApiParser; import com.hippo.ehviewer.client.parser.GalleryPageParser; import com.hippo.ehviewer.client.parser.GalleryPageUrlParser; import com.hippo.ehviewer.gallery.GalleryProvider2; @@ -53,6 +52,7 @@ import com.hippo.streampipe.OutputStreamPipe; import com.hippo.unifile.UniFile; import com.hippo.util.ExceptionUtils; +import com.hippo.util.IoThreadPoolExecutor; import com.hippo.yorozuya.IOUtils; import com.hippo.yorozuya.MathUtils; import com.hippo.yorozuya.OSUtils; @@ -61,7 +61,7 @@ import com.hippo.yorozuya.collect.SparseJLArray; import com.hippo.yorozuya.thread.PriorityThread; import com.hippo.yorozuya.thread.PriorityThreadFactory; - +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -78,8 +78,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; - import okhttp3.Call; +import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; @@ -163,6 +163,9 @@ public final class SpiderQueen implements Runnable { private final AtomicInteger mDownloadedPages = new AtomicInteger(0); private final AtomicInteger mFinishedPages = new AtomicInteger(0); + private AtomicReference showKey = new AtomicReference<>(); + private final Object showKeyLock = new Object(); + // Store page error private final ConcurrentHashMap mPageErrorMap = new ConcurrentHashMap<>(); // Store page download percent @@ -430,11 +433,11 @@ public String getError() { } public Object forceRequest(int index) { - return request(index, true, false); + return request(index, true, true, false); } public Object request(int index) { - return request(index, false, true); + return request(index, true, false, true); } private int getPageState(int index) { @@ -483,7 +486,7 @@ public void cancelRequest(int index) { * Float for download percent
* null for wait */ - private Object request(int index, boolean force, boolean addNeighbor) { + private Object request(int index, boolean ignoreError, boolean force, boolean addNeighbor) { if (mQueenThread == null) { return null; } @@ -492,7 +495,8 @@ private Object request(int index, boolean force, boolean addNeighbor) { int state = getPageState(index); // Fix state for force - if (force && (state == STATE_FINISHED || state == STATE_FAILED)) { + if ((force && (state == STATE_FINISHED || state == STATE_FAILED)) || + (ignoreError && state == STATE_FAILED)) { // Update state to none at once updatePageState(index, STATE_NONE); state = STATE_NONE; @@ -518,7 +522,7 @@ private Object request(int index, boolean force, boolean addNeighbor) { } else { size = Integer.MAX_VALUE; } - for (int i = index + 1, n = index + i + mPreloadNumber; i < n && i < size; i++) { + for (int i = index + 1, n = index + 1 + mPreloadNumber; i < n && i < size; i++) { if (STATE_NONE == getPageState(i)) { mRequestPageQueue2.add(i); } @@ -667,7 +671,7 @@ protected Void doInBackground(Void... params) { writeSpiderInfoToLocal(spiderInfo); return null; } - }.execute(); + }.executeOnExecutor(IoThreadPoolExecutor.getInstance()); } } @@ -713,10 +717,13 @@ private void readPreviews(String body, int index, SpiderInfo spiderInfo) throws spiderInfo.pages = GalleryDetailParser.parsePages(body); spiderInfo.previewPages = GalleryDetailParser.parsePreviewPages(body); PreviewSet previewSet = GalleryDetailParser.parsePreviewSet(body); - if ((index >= 0 && index < spiderInfo.pages - 1) || (index == 0 && spiderInfo.pages == 1)) { - spiderInfo.previewPerPage = previewSet.size(); - } else { - spiderInfo.previewPerPage = Math.max(spiderInfo.previewPerPage, previewSet.size()); + + if (previewSet.size() > 0) { + if (index == 0) { + spiderInfo.previewPerPage = previewSet.size(); + } else { + spiderInfo.previewPerPage = previewSet.getPosition(0) / index; + } } for (int i = 0, n = previewSet.size(); i < n; i++) { @@ -729,14 +736,14 @@ private void readPreviews(String body, int index, SpiderInfo spiderInfo) throws } } - private SpiderInfo readSpiderInfoFromInternet(EhConfig config) { + private SpiderInfo readSpiderInfoFromInternet() { try { SpiderInfo spiderInfo = new SpiderInfo(); spiderInfo.gid = mGalleryInfo.gid; spiderInfo.token = mGalleryInfo.token; Request request = new EhRequestBuilder(EhUrl.getGalleryDetailUrl( - mGalleryInfo.gid, mGalleryInfo.token, 0, false), config).build(); + mGalleryInfo.gid, mGalleryInfo.token, 0, false), EhUrl.getReferer()).build(); Response response = mHttpClient.newCall(request).execute(); String body = response.body().string(); @@ -744,12 +751,13 @@ private SpiderInfo readSpiderInfoFromInternet(EhConfig config) { spiderInfo.pTokenMap = new SparseArray<>(spiderInfo.pages); readPreviews(body, 0, spiderInfo); return spiderInfo; - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); return null; } } - private String getPTokenFromInternet(int index, EhConfig config) { + private String getPTokenFromInternet(int index) { SpiderInfo spiderInfo = mSpiderInfo.get(); if (spiderInfo == null) { return null; @@ -762,15 +770,19 @@ private String getPTokenFromInternet(int index, EhConfig config) { } else { previewIndex = 0; } + if (spiderInfo.previewPages > 0) { + previewIndex = Math.min(previewIndex, spiderInfo.previewPages - 1); + } try { String url = EhUrl.getGalleryDetailUrl( mGalleryInfo.gid, mGalleryInfo.token, previewIndex, false); + String referer = EhUrl.getReferer(); if (DEBUG_PTOKEN) { Log.d(TAG, "index " + index + ", previewIndex " + previewIndex + ", previewPerPage " + spiderInfo.previewPerPage+ ", url " + url); } - Request request = new EhRequestBuilder(url, config).build(); + Request request = new EhRequestBuilder(url, referer).build(); Response response = mHttpClient.newCall(request).execute(); String body = response.body().string(); readPreviews(body, previewIndex, spiderInfo); @@ -783,7 +795,8 @@ private String getPTokenFromInternet(int index, EhConfig config) { pToken = spiderInfo.pTokenMap.get(index); } return pToken; - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); return null; } } @@ -795,7 +808,8 @@ private synchronized void writeSpiderInfoToLocal(@NonNull SpiderInfo spiderInfo) UniFile file = downloadDir.createFile(SPIDER_INFO_FILENAME); try { spiderInfo.write(file.openOutputStream()); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); // Ignore } } @@ -814,11 +828,6 @@ private synchronized void writeSpiderInfoToLocal(@NonNull SpiderInfo spiderInfo) } private void runInternal() { - // Get EhConfig - EhConfig config = Settings.getEhConfig().clone(); - config.previewSize = EhConfig.PREVIEW_SIZE_NORMAL; - config.setDirty(); - // Read spider info SpiderInfo spiderInfo = readSpiderInfoFromLocal(); @@ -829,7 +838,7 @@ private void runInternal() { // Spider info from internet if (spiderInfo == null) { - spiderInfo = readSpiderInfoFromInternet(config); + spiderInfo = readSpiderInfoFromInternet(); } // Error! Can't get spiderInfo @@ -900,10 +909,10 @@ private void runInternal() { } // Get pToken from internet - pToken = getPTokenFromInternet(index, config); + pToken = getPTokenFromInternet(index); if (null == pToken) { // Preview size may changed, so try to get pToken twice - pToken = getPTokenFromInternet(index, config); + pToken = getPTokenFromInternet(index); } if (null == pToken) { @@ -1026,8 +1035,20 @@ private String getPageUrl(long gid, int index, String pToken, return pageUrl; } - private GalleryPageParser.Result getImageUrl(int index, String pageUrl) throws Exception { - GalleryPageParser.Result result = EhEngine.getGalleryPage(null, mHttpClient, pageUrl); + private GalleryPageParser.Result fetchPageResultFromHtml(int index, String pageUrl) throws Throwable { + GalleryPageParser.Result result = EhEngine.getGalleryPage(null, mHttpClient, pageUrl, mGalleryInfo.gid, mGalleryInfo.token); + if (StringUtils.endsWith(result.imageUrl, URL_509_SUFFIX_ARRAY)) { + // Get 509 + // Notify listeners + notifyGet509(index); + throw new Image509Exception(); + } + + return result; + } + + private GalleryPageApiParser.Result fetchPageResultFromApi(long gid, int index, String pToken, String showKey, String previousPToken) throws Throwable { + GalleryPageApiParser.Result result = EhEngine.getGalleryPageApi(null, mHttpClient, gid, index, pToken, showKey, previousPToken); if (StringUtils.endsWith(result.imageUrl, URL_509_SUFFIX_ARRAY)) { // Get 509 // Notify listeners @@ -1039,137 +1060,241 @@ private GalleryPageParser.Result getImageUrl(int index, String pageUrl) throws E } // false for stop - private boolean downloadImage(long gid, int index, String pToken, boolean force) { - List skipHathKeys = new ArrayList<>(5); + private boolean downloadImage(long gid, int index, String pToken, String previousPToken, boolean force) { String skipHathKey = null; - String imageUrl; - String error = null; + List skipHathKeys = new ArrayList<>(5); + String originImageUrl = null; String pageUrl = null; + String error = null; + boolean forceHtml = false; boolean interrupt = false; boolean leakSkipHathKey = false; - // Try twice for (int i = 0; i < 5; i++) { - if (leakSkipHathKey) { - break; - } + String imageUrl = null; + String localShowKey; + + // Check show key + synchronized (showKeyLock) { + localShowKey = showKey.get(); + if (localShowKey == null || forceHtml) { + if (leakSkipHathKey) { + break; + } - pageUrl = getPageUrl(gid, index, pToken, pageUrl, skipHathKey); + // Try to get show key + pageUrl = getPageUrl(gid, index, pToken, pageUrl, skipHathKey); + try { + GalleryPageParser.Result result = fetchPageResultFromHtml(index, pageUrl); + imageUrl = result.imageUrl; + skipHathKey = result.skipHathKey; + originImageUrl = result.originImageUrl; + localShowKey = result.showKey; + + if (!TextUtils.isEmpty(skipHathKey)) { + if (skipHathKeys.contains(skipHathKey)) { + // Duplicate skip hath key + leakSkipHathKey = true; + } else { + skipHathKeys.add(skipHathKey); + } + } else { + leakSkipHathKey = true; + } - GalleryPageParser.Result result = null; - try { - result = getImageUrl(index, pageUrl); - } catch (Image509Exception e) { - error = GetText.getString(R.string.error_509); - } catch (Exception e) { - error = ExceptionUtils.getReadableString(e); - } - if (result == null) { - // Get image url failed - break; + showKey.lazySet(result.showKey); + } catch (Image509Exception e) { + error = GetText.getString(R.string.error_509); + break; + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); + error = ExceptionUtils.getReadableString(e); + break; + } + + // Check interrupted + if (Thread.currentThread().isInterrupted()) { + error = "Interrupted"; + interrupt = true; + break; + } + } } - // Check interrupted - if (Thread.currentThread().isInterrupted()) { - error = "Interrupted"; - interrupt = true; - break; + + if (imageUrl == null) { + if (localShowKey == null) { + error = "ShowKey error"; + break; + } + + try { + GalleryPageApiParser.Result result = fetchPageResultFromApi(gid, index, pToken, localShowKey, previousPToken); + imageUrl = result.imageUrl; + skipHathKey = result.skipHathKey; + originImageUrl = result.originImageUrl; + } catch (Image509Exception e) { + error = GetText.getString(R.string.error_509); + break; + } catch (Throwable e) { + if (e instanceof ParseException && "Key mismatch".equals(e.getMessage())) { + // Show key is wrong, enter a new loop to get the new show key + showKey.compareAndSet(localShowKey, null); + continue; + } else { + ExceptionUtils.throwIfFatal(e); + error = ExceptionUtils.getReadableString(e); + break; + } + } + + // Check interrupted + if (Thread.currentThread().isInterrupted()) { + error = "Interrupted"; + interrupt = true; + break; + } } - if (Settings.getDownloadOriginImage() && !TextUtils.isEmpty(result.originImageUrl)) { - imageUrl = result.originImageUrl; + String targetImageUrl; + String referer; + if (Settings.getDownloadOriginImage() && !TextUtils.isEmpty(originImageUrl)) { + targetImageUrl = originImageUrl; + referer = EhUrl.getPageUrl(gid, index, pToken); } else { - imageUrl = result.imageUrl; + targetImageUrl = imageUrl; + referer = null; } - skipHathKey = result.skipHathKey; - if (!TextUtils.isEmpty(skipHathKey)) { - if (skipHathKeys.contains(skipHathKey)) { - // Duplicate skip hath key, don't run next turn - leakSkipHathKey = true; - } else { - skipHathKeys.add(skipHathKey); - } - } else { - // No skip hath key, don't run next turn - leakSkipHathKey = true; + if (targetImageUrl == null) { + error = "TargetImageUrl error"; + break; } - - // If it is force request, skip first image - //if (force && i == 0) { - // continue; - //} - if (DEBUG_LOG) { - Log.d(TAG, imageUrl); + Log.d(TAG, targetImageUrl); } // Download image - OutputStreamPipe pipe = null; InputStream is = null; try { if (DEBUG_LOG) { Log.d(TAG, "Start download image " + index); } - Call call = mHttpClient.newCall(new EhRequestBuilder(imageUrl).build()); + Call call = mHttpClient.newCall(new EhRequestBuilder(targetImageUrl, referer).build()); Response response = call.execute(); + ResponseBody responseBody = response.body(); + if (response.code() >= 400) { // Maybe 404 - response.body().close(); + response.close(); error = "Bad code: " + response.code(); + forceHtml = true; + continue; + } + + if (responseBody == null) { + error = "Empty response body"; + forceHtml = true; continue; } - ResponseBody responseBody = response.body(); // Get extension - String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType( - responseBody.contentType().toString()); + String extension = null; + MediaType mediaType = responseBody.contentType(); + if (mediaType != null) { + extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mediaType.toString()); + } // Ensure extension if (!Utilities.contain(GalleryProvider2.SUPPORT_IMAGE_EXTENSIONS, extension)) { extension = GalleryProvider2.SUPPORT_IMAGE_EXTENSIONS[0]; } - // Get out put pipe - pipe = mSpiderDen.openOutputStreamPipe(index, extension); - if (null == pipe) { - // Can't get pipe - error = GetText.getString(R.string.error_write_failed); - response.body().close(); - break; - } + OutputStreamPipe osPipe = null; + try { + // Get out put pipe + osPipe = mSpiderDen.openOutputStreamPipe(index, extension); + if (osPipe == null) { + // Can't get pipe + error = GetText.getString(R.string.error_write_failed); + response.close(); + break; + } - long contentLength = responseBody.contentLength(); - is = responseBody.byteStream(); - pipe.obtain(); - OutputStream os = pipe.open(); + long contentLength = responseBody.contentLength(); + is = responseBody.byteStream(); + osPipe.obtain(); + OutputStream os = osPipe.open(); - final byte data[] = new byte[1024 * 4]; - long receivedSize = 0; + final byte[] data = new byte[1024 * 4]; + long receivedSize = 0; - while (!Thread.currentThread().isInterrupted()) { - int bytesRead = is.read(data); - if (bytesRead == -1) { - response.body().close(); - break; + while (!Thread.currentThread().isInterrupted()) { + int bytesRead = is.read(data); + if (bytesRead == -1) { + response.close(); + break; + } + os.write(data, 0, bytesRead); + receivedSize += bytesRead; + // Update page percent + if (contentLength > 0) { + mPagePercentMap.put(index, (float) receivedSize / contentLength); + } + // Notify listener + notifyPageDownload(index, contentLength, receivedSize, bytesRead); } - os.write(data, 0, bytesRead); - receivedSize += bytesRead; - // Update page percent - if (contentLength > 0) { - mPagePercentMap.put(index, (float) receivedSize / contentLength); + os.flush(); + + // check download size + if (contentLength >= 0) { + if (receivedSize < contentLength) { + Log.e(TAG, "Can't download all of image data"); + error = "Incomplete"; + forceHtml = true; + continue; + } else if (receivedSize > contentLength) { + Log.w(TAG, "Received data is more than contentLength"); + } + } + } finally { + if (osPipe != null) { + osPipe.close(); + osPipe.release(); } - // Notify listener - notifyPageDownload(index, contentLength, receivedSize, bytesRead); } - os.flush(); - // check download size - if (contentLength >= 0) { - if (receivedSize < contentLength) { - Log.e(TAG, "Can't download all of image data"); - error = "Incomplete"; + InputStreamPipe isPipe = null; + try { + // Get InputStreamPipe + isPipe = mSpiderDen.openInputStreamPipe(index); + if (isPipe == null) { + // Can't get pipe + error = GetText.getString(R.string.error_reading_failed); + break; + } + + // Check plain txt + isPipe.obtain(); + InputStream inputStream = new BufferedInputStream(isPipe.open()); + boolean isPlainTxt = true; + for (;;) { + int b = inputStream.read(); + if (b == -1) { + break; + } + if (b > 126) { + isPlainTxt = false; + break; + } + } + if (isPlainTxt) { + error = GetText.getString(R.string.error_reading_failed); + forceHtml = true; continue; - } else if (receivedSize > contentLength) { - Log.w(TAG, "Received data is more than contentLength"); + } + } finally { + if (isPipe != null) { + isPipe.close(); + isPipe.release(); } } @@ -1188,13 +1313,11 @@ private boolean downloadImage(long gid, int index, String pToken, boolean force) updatePageState(index, STATE_FINISHED); return true; } catch (IOException e) { + e.printStackTrace(); error = GetText.getString(R.string.error_socket); + forceHtml = true; } finally { IOUtils.closeQuietly(is); - if (null != pipe) { - pipe.close(); - pipe.release(); - } if (DEBUG_LOG) { Log.d(TAG, "End download image " + index); @@ -1311,6 +1434,36 @@ private boolean runInternal() { return false; } + String previousPToken = null; + int previousIndex = index - 1; + // Get token + while (previousIndex >= 0 && !Thread.currentThread().isInterrupted()) { + synchronized (mPTokenLock) { + previousPToken = spiderInfo.pTokenMap.get(previousIndex); + } + if (previousPToken == null) { + mRequestPTokenQueue.add(previousIndex); + // Notify Queen + synchronized (mQueenLock) { + mQueenLock.notify(); + } + // Wait + synchronized (mWorkerLock) { + try { + mWorkerLock.wait(); + } catch (InterruptedException e) { + // Interrupted + if (DEBUG_LOG) { + Log.d(TAG, Thread.currentThread().getName() + " Interrupted"); + } + break; + } + } + } else { + break; + } + } + if (SpiderInfo.TOKEN_FAILED.equals(pToken)) { // Get token failed updatePageState(index, STATE_FAILED, GetText.getString(R.string.error_get_ptoken_error)); @@ -1318,7 +1471,7 @@ private boolean runInternal() { } // Get image url - return downloadImage(mGid, index, pToken, force); + return downloadImage(mGid, index, pToken, previousPToken, force); } @Override @@ -1400,7 +1553,7 @@ public void run() { // Can't find the file, it might be removed from cache, // Reset it state and request it updatePageState(index, STATE_NONE, null); - request(index, false, false); + request(index, false, false, false); continue; } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/CommonOperations.java b/app/src/main/java/com/hippo/ehviewer/ui/CommonOperations.java index 217515f92..8ae36e915 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/CommonOperations.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/CommonOperations.java @@ -22,9 +22,8 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.AsyncTask; -import android.support.v7.app.AlertDialog; import android.util.Log; - +import androidx.appcompat.app.AlertDialog; import com.hippo.app.ListCheckBoxDialogBuilder; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.EhDB; @@ -40,19 +39,21 @@ import com.hippo.ehviewer.ui.scene.BaseScene; import com.hippo.text.Html; import com.hippo.unifile.UniFile; +import com.hippo.util.ExceptionUtils; +import com.hippo.util.IoThreadPoolExecutor; import com.hippo.yorozuya.FileUtils; import com.hippo.yorozuya.IOUtils; - -import org.json.JSONException; -import org.json.JSONObject; - +import com.hippo.yorozuya.collect.LongList; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; - import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; +import org.json.JSONException; +import org.json.JSONObject; public final class CommonOperations { @@ -63,7 +64,7 @@ public final class CommonOperations { public static void checkUpdate(Activity activity, boolean feedback) { if (!UPDATING) { UPDATING = true; - new UpdateTask(activity, feedback).execute(); + new UpdateTask(activity, feedback).executeOnExecutor(IoThreadPoolExecutor.getInstance()); } } @@ -79,38 +80,50 @@ public UpdateTask(Activity activity, boolean feedback) { mFeedback = feedback; } + private JSONObject fetchUpdateInfo(String url) throws IOException, JSONException { + Log.d(TAG, url); + Request request = new Request.Builder().url(url).build(); + Response response = mHttpClient.newCall(request).execute(); + return new JSONObject(response.body().string()); + } + @Override protected JSONObject doInBackground(Void... params) { + String url; + if (Settings.getBetaUpdateChannel()) { + url = "http://www.ehviewer.com/update_beta.json"; + } else { + url = "http://www.ehviewer.com/update.json"; + } + try { - String url; + return fetchUpdateInfo(url); + } catch (Throwable e1) { + ExceptionUtils.throwIfFatal(e1); + if (Settings.getBetaUpdateChannel()) { - url = "http://www.ehviewer.com/update_beta.json"; + url = "https://raw.githubusercontent.com/seven332/EhViewer/api/update_beta.json"; } else { - url = "http://www.ehviewer.com/update.json"; + url = "https://raw.githubusercontent.com/seven332/EhViewer/api/update.json"; + } + + try { + return fetchUpdateInfo(url); + } catch (Throwable e2) { + ExceptionUtils.throwIfFatal(e2); + return null; } - Log.d(TAG, url); - Request request = new Request.Builder().url(url).build(); - Response response = mHttpClient.newCall(request).execute(); - return new JSONObject(response.body().string()); - } catch (IOException e) { - return null; - } catch (JSONException e) { - return null; } } private void showUpToDateDialog() { - if (!mFeedback) { - return; - } - new AlertDialog.Builder(mActivity) .setMessage(R.string.update_to_date) .setPositiveButton(android.R.string.ok, null) .show(); } - private void showUpdateDialog(String versionName, String size, CharSequence info, final String url) { + private void showUpdateDialog(String versionName, int versionCode, String size, CharSequence info, final String url) { new AlertDialog.Builder(mActivity) .setTitle(R.string.update) .setMessage(mActivity.getString(R.string.update_plain, versionName, size, info)) @@ -119,16 +132,22 @@ private void showUpdateDialog(String versionName, String size, CharSequence info public void onClick(DialogInterface dialog, int which) { UrlOpener.openUrl(mActivity, url, false); } + }) + .setNegativeButton(R.string.update_ignore, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Settings.putSkipUpdateVersion(versionCode); + } }).show(); } - private void handleResult(JSONObject jo) { if (null == jo || mActivity.isFinishing()) { return; } String versionName; + int versionCode; String size; CharSequence info; String url; @@ -137,10 +156,12 @@ private void handleResult(JSONObject jo) { PackageManager pm = mActivity.getPackageManager(); PackageInfo pi = pm.getPackageInfo(mActivity.getPackageName(), PackageManager.GET_ACTIVITIES); int currentVersionCode = pi.versionCode; - int newVersionCode = jo.getInt("version_code"); - if (currentVersionCode >= newVersionCode) { + versionCode = jo.getInt("version_code"); + if (currentVersionCode >= versionCode) { // Update to date - showUpToDateDialog(); + if (mFeedback) { + showUpToDateDialog(); + } return; } @@ -148,11 +169,14 @@ private void handleResult(JSONObject jo) { size = FileUtils.humanReadableByteCount(jo.getLong("size"), false); info = Html.fromHtml(jo.getString("info")); url = jo.getString("url"); - } catch (PackageManager.NameNotFoundException | JSONException e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); return; } - showUpdateDialog(versionName, size, info, url); + if (mFeedback || versionCode != Settings.getSkipUpdateVersion()) { + showUpdateDialog(versionName, versionCode, size, info, url); + } } @Override @@ -185,46 +209,107 @@ private static void doAddToFavorites(Activity activity, GalleryInfo galleryInfo, public static void addToFavorites(final Activity activity, final GalleryInfo galleryInfo, final EhClient.Callback listener) { int slot = Settings.getDefaultFavSlot(); + String[] items = new String[11]; + items[0] = activity.getString(R.string.local_favorites); + String[] favCat = Settings.getFavCat(); + System.arraycopy(favCat, 0, items, 1, 10); if (slot >= -1 && slot <= 9) { - doAddToFavorites(activity, galleryInfo, slot, listener); + String newFavoriteName = slot >= 0 ? items[slot + 1] : null; + doAddToFavorites(activity, galleryInfo, slot, new DelegateFavoriteCallback(listener, galleryInfo, newFavoriteName, slot)); } else { - String[] items = new String[11]; - items[0] = activity.getString(R.string.local_favorites); - String[] favCat = Settings.getFavCat(); - System.arraycopy(favCat, 0, items, 1, 10); new ListCheckBoxDialogBuilder(activity, items, - new ListCheckBoxDialogBuilder.OnItemClickListener() { - @Override - public void onItemClick(ListCheckBoxDialogBuilder builder, AlertDialog dialog, int position) { - int slot = position - 1; - doAddToFavorites(activity, galleryInfo, slot, listener); - if (builder.isChecked()) { - Settings.putDefaultFavSlot(slot); - } else { - Settings.putDefaultFavSlot(Settings.INVALID_DEFAULT_FAV_SLOT); - } + (builder, dialog, position) -> { + int slot1 = position - 1; + String newFavoriteName = (slot1 >= 0 && slot1 <= 9) ? items[slot1+1] : null; + doAddToFavorites(activity, galleryInfo, slot1, new DelegateFavoriteCallback(listener, galleryInfo, newFavoriteName, slot1)); + if (builder.isChecked()) { + Settings.putDefaultFavSlot(slot1); + } else { + Settings.putDefaultFavSlot(Settings.INVALID_DEFAULT_FAV_SLOT); } }, activity.getString(R.string.remember_favorite_collection), false) .setTitle(R.string.add_favorites_dialog_title) + .setOnCancelListener(dialog -> listener.onCancel()) .show(); } } public static void removeFromFavorites(Activity activity, GalleryInfo galleryInfo, final EhClient.Callback listener) { + EhDB.removeLocalFavorites(galleryInfo.gid); EhClient client = EhApplication.getEhClient(activity); EhRequest request = new EhRequest(); request.setMethod(EhClient.METHOD_ADD_FAVORITES); request.setArgs(galleryInfo.gid, galleryInfo.token, -1, ""); - request.setCallback(listener); + request.setCallback(new DelegateFavoriteCallback(listener, galleryInfo, null, -2)); client.execute(request); } - // TODO Add context if activity and context are different style + private static class DelegateFavoriteCallback implements EhClient.Callback { + + private final EhClient.Callback delegate; + private final GalleryInfo info; + private final String newFavoriteName; + private final int slot; + + DelegateFavoriteCallback(EhClient.Callback delegate, GalleryInfo info, + String newFavoriteName, int slot) { + this.delegate = delegate; + this.info = info; + this.newFavoriteName = newFavoriteName; + this.slot = slot; + } + + @Override + public void onSuccess(Void result) { + info.favoriteName = newFavoriteName; + info.favoriteSlot = slot; + delegate.onSuccess(result); + EhApplication.getFavouriteStatusRouter().modifyFavourites(info.gid, slot); + } + + @Override + public void onFailure(Exception e) { + delegate.onFailure(e); + } + + @Override + public void onCancel() { + delegate.onCancel(); + } + } + public static void startDownload(final MainActivity activity, final GalleryInfo galleryInfo, boolean forceDefault) { + startDownload(activity, Collections.singletonList(galleryInfo), forceDefault); + } + + // TODO Add context if activity and context are different style + public static void startDownload(final MainActivity activity, final List galleryInfos, boolean forceDefault) { final DownloadManager dm = EhApplication.getDownloadManager(activity); - boolean justStart = forceDefault || dm.containDownloadInfo(galleryInfo.gid); + LongList toStart = new LongList(); + List toAdd = new ArrayList<>(); + for (GalleryInfo gi : galleryInfos) { + if (dm.containDownloadInfo(gi.gid)) { + toStart.add(gi.gid); + } else { + toAdd.add(gi); + } + } + + if (!toStart.isEmpty()) { + Intent intent = new Intent(activity, DownloadService.class); + intent.setAction(DownloadService.ACTION_START_RANGE); + intent.putExtra(DownloadService.KEY_GID_LIST, toStart); + activity.startService(intent); + } + + if (toAdd.isEmpty()) { + activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT); + return; + } + + boolean justStart = forceDefault; String label = null; // Get default download label if (!justStart && Settings.getHasDefaultDownloadLabel()) { @@ -238,12 +323,14 @@ public static void startDownload(final MainActivity activity, final GalleryInfo } if (justStart) { - // Already in download list or get default label - Intent intent = new Intent(activity, DownloadService.class); - intent.setAction(DownloadService.ACTION_START); - intent.putExtra(DownloadService.KEY_LABEL, label); - intent.putExtra(DownloadService.KEY_GALLERY_INFO, galleryInfo); - activity.startService(intent); + // Got default label + for (GalleryInfo gi : toAdd) { + Intent intent = new Intent(activity, DownloadService.class); + intent.setAction(DownloadService.ACTION_START); + intent.putExtra(DownloadService.KEY_LABEL, label); + intent.putExtra(DownloadService.KEY_GALLERY_INFO, gi); + activity.startService(intent); + } // Notify activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT); } else { @@ -269,11 +356,13 @@ public void onItemClick(ListCheckBoxDialogBuilder builder, AlertDialog dialog, i } } // Start download - Intent intent = new Intent(activity, DownloadService.class); - intent.setAction(DownloadService.ACTION_START); - intent.putExtra(DownloadService.KEY_LABEL, label); - intent.putExtra(DownloadService.KEY_GALLERY_INFO, galleryInfo); - activity.startService(intent); + for (GalleryInfo gi : toAdd) { + Intent intent = new Intent(activity, DownloadService.class); + intent.setAction(DownloadService.ACTION_START); + intent.putExtra(DownloadService.KEY_LABEL, label); + intent.putExtra(DownloadService.KEY_GALLERY_INFO, gi); + activity.startService(intent); + } // Save settings if (builder.isChecked()) { Settings.putHasDefaultDownloadLabel(true); diff --git a/app/src/main/java/com/hippo/ehviewer/ui/DirPickerActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/DirPickerActivity.java index 59dac07ad..971f3f472 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/DirPickerActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/DirPickerActivity.java @@ -19,18 +19,20 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.view.MenuItem; import android.view.View; import android.widget.TextView; import android.widget.Toast; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import com.hippo.android.resource.AttrResources; +import com.hippo.ehviewer.AppConfig; import com.hippo.ehviewer.R; import com.hippo.ripple.Ripple; import com.hippo.widget.DirExplorer; -import com.hippo.yorozuya.ViewUtils; - +import com.hippo.yorozuya.FileUtils; import java.io.File; public class DirPickerActivity extends ToolbarActivity @@ -47,6 +49,7 @@ public class DirPickerActivity extends ToolbarActivity private TextView mPath; @Nullable private DirExplorer mDirExplorer; + private View mDefault; @Nullable private View mOk; @@ -56,9 +59,10 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_dir_picker); setNavigationIcon(R.drawable.v_arrow_left_dark_x24); - mPath = (TextView) ViewUtils.$$(this, R.id.path); - mDirExplorer = (DirExplorer) ViewUtils.$$(this, R.id.dir_explorer); - mOk = ViewUtils.$$(this, R.id.ok); + mPath = findViewById(R.id.path); + mDirExplorer = findViewById(R.id.dir_explorer); + mDefault = findViewById(R.id.preset); + mOk = findViewById(R.id.ok); File file; if (null == savedInstanceState) { @@ -70,8 +74,9 @@ protected void onCreate(Bundle savedInstanceState) { mDirExplorer.setCurrentFile(file); mDirExplorer.setOnChangeDirListener(this); - Ripple.addRipple(mOk, false); + Ripple.addRipple(mOk, !AttrResources.getAttrBoolean(this, R.attr.isLightTheme)); + mDefault.setOnClickListener(this); mOk.setOnClickListener(this); mPath.setText(mDirExplorer.getCurrentFile().getPath()); @@ -127,7 +132,31 @@ public boolean onOptionsItemSelected(MenuItem item) { @Override public void onClick(@NonNull View v) { - if (mOk == v) { + if (mDefault == v) { + File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(this, null); + File[] dirs = new File[externalFilesDirs.length + 1]; + dirs[0] = AppConfig.getDefaultDownloadDir(); + for (int i = 0; i < externalFilesDirs.length; i++) { + dirs[i + 1] = new File(externalFilesDirs[i], "download"); + } + + CharSequence[] items = new CharSequence[dirs.length]; + items[0] = getString(R.string.default_directory); + for (int i = 1; i < items.length; i++) { + items[i] = getString(R.string.application_file_directory, i); + } + + new AlertDialog.Builder(this).setItems(items, (dialog, which) -> { + File dir = dirs[which]; + if (!FileUtils.ensureDirectory(dir)) { + Toast.makeText(DirPickerActivity.this, R.string.directory_not_writable, Toast.LENGTH_SHORT).show(); + return; + } + if (mDirExplorer != null) { + mDirExplorer.setCurrentFile(dir); + } + }).show(); + } else if (mOk == v) { if (null == mDirExplorer) { return; } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/EhActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/EhActivity.java index 474702f8e..f18e114c8 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/EhActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/EhActivity.java @@ -16,23 +16,42 @@ package com.hippo.ehviewer.ui; +import android.content.Context; +import android.os.Build; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v7.app.AppCompatActivity; - -import com.google.analytics.tracking.android.EasyTracker; +import android.view.WindowManager; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.appcompat.app.AppCompatActivity; +import com.google.firebase.analytics.FirebaseAnalytics; +import com.hippo.android.resource.AttrResources; +import com.hippo.content.ContextLocalWrapper; +import com.hippo.ehviewer.Analytics; import com.hippo.ehviewer.EhApplication; +import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; +import java.util.Locale; public abstract class EhActivity extends AppCompatActivity { - private boolean mTrackStarted; + @StyleRes + protected abstract int getThemeResId(int theme); @Override protected void onCreate(@Nullable Bundle savedInstanceState) { + setTheme(getThemeResId(Settings.getTheme())); + super.onCreate(savedInstanceState); ((EhApplication) getApplication()).registerActivity(this); + + if (Analytics.isEnabled()) { + FirebaseAnalytics.getInstance(this); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Settings.getApplyNavBarThemeColor()) { + getWindow().setNavigationBarColor(AttrResources.getAttrColor(this, R.attr.colorPrimaryDark)); + } } @Override @@ -43,22 +62,35 @@ protected void onDestroy() { } @Override - public void onStart() { - super.onStart(); - - if (Settings.getEnableAnalytics()) { - EasyTracker.getInstance(this).activityStart(this); - mTrackStarted = true; + protected void onResume() { + super.onResume(); + if(Settings.getEnabledSecurity()){ + getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE); + }else{ + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE); } } @Override - public void onStop() { - super.onStop(); + protected void attachBaseContext(Context newBase) { + Locale locale = null; + String language = Settings.getAppLanguage(); + if (language != null && !language.equals("system")) { + String[] split = language.split("-"); + if (split.length == 1) { + locale = new Locale(split[0]); + } else if (split.length == 2) { + locale = new Locale(split[0], split[1]); + } else if (split.length == 3) { + locale = new Locale(split[0], split[1], split[2]); + } + } - if (mTrackStarted) { - EasyTracker.getInstance(this).activityStop(this); - mTrackStarted = false; + if (locale != null) { + newBase = ContextLocalWrapper.wrap(newBase, locale); } + + super.attachBaseContext(newBase); } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/EhPreferenceActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/EhPreferenceActivity.java index 6efdabb30..10f3706ab 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/EhPreferenceActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/EhPreferenceActivity.java @@ -16,23 +16,43 @@ package com.hippo.ehviewer.ui; +import android.content.Context; +import android.content.res.Resources; +import android.os.Build; import android.os.Bundle; -import android.support.annotation.Nullable; - -import com.google.analytics.tracking.android.EasyTracker; -import com.hippo.app.AppCompatPreferenceActivity; +import android.view.WindowManager; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import com.google.firebase.analytics.FirebaseAnalytics; +import com.hippo.android.resource.AttrResources; +import com.hippo.app.PrettyPreferenceActivity; +import com.hippo.content.ContextLocalWrapper; +import com.hippo.ehviewer.Analytics; import com.hippo.ehviewer.EhApplication; +import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; +import java.util.Locale; -public abstract class EhPreferenceActivity extends AppCompatPreferenceActivity { +public abstract class EhPreferenceActivity extends PrettyPreferenceActivity { - private boolean mTrackStarted; + @StyleRes + protected abstract int getThemeResId(int theme); @Override protected void onCreate(@Nullable Bundle savedInstanceState) { + setTheme(getThemeResId(Settings.getTheme())); + super.onCreate(savedInstanceState); ((EhApplication) getApplication()).registerActivity(this); + + if (Analytics.isEnabled()) { + FirebaseAnalytics.getInstance(this); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Settings.getApplyNavBarThemeColor()) { + getWindow().setNavigationBarColor(AttrResources.getAttrColor(this, R.attr.colorPrimaryDark)); + } } @Override @@ -43,22 +63,36 @@ protected void onDestroy() { } @Override - public void onStart() { - super.onStart(); - - if (Settings.getEnableAnalytics()) { - EasyTracker.getInstance(this).activityStart(this); - mTrackStarted = true; + protected void onResume() { + super.onResume(); + if(Settings.getEnabledSecurity()){ + getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE); + }else{ + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE); } } @Override - public void onStop() { - super.onStop(); + protected void attachBaseContext(Context newBase) { + Locale locale = null; + String language = Settings.getAppLanguage(); + if (language != null && !language.equals("system")) { + String[] split = language.split("-"); + if (split.length == 1) { + locale = new Locale(split[0]); + } else if (split.length == 2) { + locale = new Locale(split[0], split[1]); + } else if (split.length == 3) { + locale = new Locale(split[0], split[1], split[2]); + } + } - if (mTrackStarted) { - EasyTracker.getInstance(this).activityStop(this); - mTrackStarted = false; + if (locale == null) { + locale = Resources.getSystem().getConfiguration().locale; } + + newBase = ContextLocalWrapper.wrap(newBase, locale); + super.attachBaseContext(newBase); } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/ExcludedLanguagesActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/ExcludedLanguagesActivity.java index bc637baeb..93ad8d6d1 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/ExcludedLanguagesActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/ExcludedLanguagesActivity.java @@ -17,15 +17,15 @@ package com.hippo.ehviewer.ui; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.hippo.android.resource.AttrResources; import com.hippo.easyrecyclerview.EasyRecyclerView; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; @@ -164,11 +164,12 @@ public void onCreate(@Nullable Bundle savedInstanceState) { mDeselectAll.setOnClickListener(this); mInvertSelection.setOnClickListener(this); - Ripple.addRipple(mCancel, false); - Ripple.addRipple(mOk, false); - Ripple.addRipple(mSelectAll, false); - Ripple.addRipple(mDeselectAll, false); - Ripple.addRipple(mInvertSelection, false); + boolean isDarkTheme = !AttrResources.getAttrBoolean(this, R.attr.isLightTheme); + Ripple.addRipple(mCancel, isDarkTheme); + Ripple.addRipple(mOk, isDarkTheme); + Ripple.addRipple(mSelectAll, isDarkTheme); + Ripple.addRipple(mDeselectAll, isDarkTheme); + Ripple.addRipple(mInvertSelection, isDarkTheme); } private boolean isDecimal(String str) { diff --git a/app/src/main/java/com/hippo/ehviewer/ui/FilterActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/FilterActivity.java index 458ddfd17..068641afb 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/FilterActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/FilterActivity.java @@ -17,13 +17,9 @@ package com.hippo.ehviewer.ui; import android.content.DialogInterface; +import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.design.widget.TextInputLayout; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; @@ -33,7 +29,11 @@ import android.widget.ImageView; import android.widget.Spinner; import android.widget.TextView; - +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.textfield.TextInputLayout; import com.hippo.easyrecyclerview.EasyRecyclerView; import com.hippo.ehviewer.R; import com.hippo.ehviewer.client.EhFilter; @@ -41,7 +41,6 @@ import com.hippo.util.DrawableManager; import com.hippo.view.ViewTransition; import com.hippo.yorozuya.ViewUtils; - import java.util.List; public class FilterActivity extends ToolbarActivity { @@ -67,7 +66,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { TextView tip = (TextView) ViewUtils.$$(this, R.id.tip); mViewTransition = new ViewTransition(mRecyclerView, tip); - Drawable drawable = DrawableManager.getDrawable(this, R.drawable.big_filter); + Drawable drawable = DrawableManager.getVectorDrawable(this, R.drawable.big_filter); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); tip.setCompoundDrawables(null, drawable, null, null); @@ -77,6 +76,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { mRecyclerView.setClipChildren(false); mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); mRecyclerView.hasFixedSize(); + mRecyclerView.setItemAnimator(null); updateView(false); } @@ -145,36 +145,18 @@ private void showAddFilterDialog() { } private void showDeleteFilterDialog(final Filter filter) { - int messageId; - switch (filter.mode) { - case EhFilter.MODE_TITLE: - messageId = R.string.delete_title_filter; - break; - case EhFilter.MODE_UPLOADER: - messageId = R.string.delete_uploader_filter; - break; - case EhFilter.MODE_TAG: - messageId = R.string.delete_tag_filter; - break; - default: - messageId = R.string.delete_filter; - } - String message = getString(messageId, filter.text); - + String message = getString(R.string.delete_filter, filter.text); new AlertDialog.Builder(this) .setMessage(message) - .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (DialogInterface.BUTTON_POSITIVE != which || null == mFilterList) { - return; - } - mFilterList.delete(filter); - if (null != mAdapter) { - mAdapter.notifyDataSetChanged(); - } - updateView(true); + .setPositiveButton(R.string.delete, (dialog, which) -> { + if (DialogInterface.BUTTON_POSITIVE != which || null == mFilterList) { + return; + } + mFilterList.delete(filter); + if (null != mAdapter) { + mAdapter.notifyDataSetChanged(); } + updateView(true); }).show(); } @@ -242,11 +224,13 @@ private class FilterHolder extends RecyclerView.ViewHolder implements View.OnCli public FilterHolder(View itemView) { super(itemView); text = (TextView) ViewUtils.$$(itemView, R.id.text); - icon = (ImageView) itemView.findViewById(R.id.icon); + icon = itemView.findViewById(R.id.icon); if (null != icon) { icon.setOnClickListener(this); } + // click on the filter text to enable/disable it + text.setOnClickListener(this); } @Override @@ -257,7 +241,15 @@ public void onClick(View v) { } Filter filter = mFilterList.get(position); if (FilterList.MODE_HEADER != filter.mode) { - showDeleteFilterDialog(filter); + if (v instanceof ImageView) { + showDeleteFilterDialog(filter); + } else if (v instanceof TextView) { + mFilterList.trigger(filter); + + //for updating delete line on filter text + mAdapter.notifyItemChanged(getAdapterPosition()); + } + } } } @@ -297,7 +289,7 @@ public FilterHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (R.layout.item_filter == layoutId) { holder.icon.setImageDrawable( - DrawableManager.getDrawable(FilterActivity.this, R.drawable.v_delete_x24)); + DrawableManager.getVectorDrawable(FilterActivity.this, R.drawable.v_delete_x24)); } return holder; @@ -314,12 +306,18 @@ public void onBindViewHolder(FilterHolder holder, int position) { holder.text.setText(filter.text); } else { holder.text.setText(filter.text); + // add a delete line if the filter is disabled + if (!filter.enable) { + holder.text.setPaintFlags(holder.text.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } else { + holder.text.setPaintFlags(holder.text.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG)); + } } } @Override public int getItemCount() { - return null != mFilterList ? mFilterList.size(): 0; + return null != mFilterList ? mFilterList.size() : 0; } } @@ -450,5 +448,9 @@ public void add(Filter filter) { public void delete(Filter filter) { mEhFilter.deleteFilter(filter); } + + public void trigger(Filter filter) { + mEhFilter.triggerFilter(filter); + } } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java index 609597a24..587555f06 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.java @@ -20,6 +20,7 @@ import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; +import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; @@ -30,10 +31,6 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.SwitchCompat; import android.text.TextUtils; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -47,16 +44,22 @@ import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.SwitchCompat; +import com.hippo.android.resource.AttrResources; import com.hippo.ehviewer.AppConfig; +import com.hippo.ehviewer.BuildConfig; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.client.data.GalleryInfo; +import com.hippo.ehviewer.gallery.ArchiveGalleryProvider; import com.hippo.ehviewer.gallery.DirGalleryProvider; import com.hippo.ehviewer.gallery.EhGalleryProvider; import com.hippo.ehviewer.gallery.GalleryProvider2; -import com.hippo.ehviewer.gallery.ZipGalleryProvider; import com.hippo.ehviewer.widget.GalleryGuideView; +import com.hippo.ehviewer.widget.GalleryHeader; import com.hippo.ehviewer.widget.ReversibleSeekBar; import com.hippo.glgallery.GalleryPageView; import com.hippo.glgallery.GalleryProvider; @@ -64,23 +67,27 @@ import com.hippo.glgallery.SimpleAdapter; import com.hippo.glview.view.GLRootView; import com.hippo.unifile.UniFile; +import com.hippo.util.ExceptionUtils; import com.hippo.util.SystemUiHelper; import com.hippo.widget.ColorView; import com.hippo.yorozuya.AnimationUtils; import com.hippo.yorozuya.ConcurrentPool; +import com.hippo.yorozuya.IOUtils; import com.hippo.yorozuya.MathUtils; import com.hippo.yorozuya.ResourcesUtils; import com.hippo.yorozuya.SimpleAnimatorListener; import com.hippo.yorozuya.SimpleHandler; import com.hippo.yorozuya.ViewUtils; - import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChangeListener, GalleryView.Listener { public static final String ACTION_DIR = "dir"; - public static final String ACTION_ZIP = "zip"; public static final String ACTION_EH = "eh"; public static final String KEY_ACTION = "action"; @@ -93,11 +100,14 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan private static final long SLIDER_ANIMATION_DURING = 150; private static final long HIDE_SLIDER_DELAY = 3000; + private static final int WRITE_REQUEST_CODE = 43; + private String mAction; private String mFilename; private Uri mUri; private GalleryInfo mGalleryInfo; private int mPage; + private String mCacheFileName; @Nullable private GLRootView mGLRootView; @@ -172,6 +182,19 @@ public void run() { } }; + @Override + protected int getThemeResId(int theme) { + switch (theme) { + case Settings.THEME_LIGHT: + default: + return R.style.AppTheme_Gallery; + case Settings.THEME_DARK: + return R.style.AppTheme_Gallery_Dark; + case Settings.THEME_BLACK: + return R.style.AppTheme_Gallery_Black; + } + } + private void buildProvider() { if (mGalleryProvider != null) { return; @@ -181,10 +204,6 @@ private void buildProvider() { if (mFilename != null) { mGalleryProvider = new DirGalleryProvider(UniFile.fromFile(new File(mFilename))); } - } else if (ACTION_ZIP.equals(mAction)) { - if (mFilename != null) { - mGalleryProvider = new ZipGalleryProvider(new File(mFilename)); - } } else if (ACTION_EH.equals(mAction)) { if (mGalleryInfo != null) { mGalleryProvider = new EhGalleryProvider(this, mGalleryInfo); @@ -192,7 +211,7 @@ private void buildProvider() { } else if (Intent.ACTION_VIEW.equals(mAction)) { if (mUri != null) { // Only support zip now - mGalleryProvider = new ZipGalleryProvider(new File(mUri.getPath())); + mGalleryProvider = new ArchiveGalleryProvider(this, mUri); } } } @@ -271,22 +290,21 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { mGLRootView = (GLRootView) ViewUtils.$$(this, R.id.gl_root_view); mGalleryAdapter = new GalleryAdapter(mGLRootView, mGalleryProvider); Resources resources = getResources(); - int primaryColor = ResourcesUtils.getAttrColor(this, R.attr.colorPrimary); mGalleryView = new GalleryView.Builder(this, mGalleryAdapter) .setListener(this) .setLayoutMode(Settings.getReadingDirection()) .setScaleMode(Settings.getPageScaling()) .setStartPosition(Settings.getStartPosition()) .setStartPage(startPage) - .setBackgroundColor(resources.getColor(R.color.gallery_background)) - .setEdgeColor(primaryColor & 0xffffff | 0x33000000) - .setPagerInterval(resources.getDimensionPixelOffset(R.dimen.gallery_pager_interval)) - .setScrollInterval(resources.getDimensionPixelOffset(R.dimen.gallery_scroll_interval)) + .setBackgroundColor(AttrResources.getAttrColor(this, android.R.attr.colorBackground)) + .setEdgeColor(AttrResources.getAttrColor(this, R.attr.colorEdgeEffect) & 0xffffff | 0x33000000) + .setPagerInterval(Settings.getShowPageInterval() ? resources.getDimensionPixelOffset(R.dimen.gallery_pager_interval) : 0) + .setScrollInterval(Settings.getShowPageInterval() ? resources.getDimensionPixelOffset(R.dimen.gallery_scroll_interval) : 0) .setPageMinHeight(resources.getDimensionPixelOffset(R.dimen.gallery_page_min_height)) .setPageInfoInterval(resources.getDimensionPixelOffset(R.dimen.gallery_page_info_interval)) - .setProgressColor(primaryColor) + .setProgressColor(ResourcesUtils.getAttrColor(this, R.attr.colorPrimary)) .setProgressSize(resources.getDimensionPixelOffset(R.dimen.gallery_progress_size)) - .setPageTextColor(resources.getColor(R.color.secondary_text_default_dark)) + .setPageTextColor(AttrResources.getAttrColor(this, android.R.attr.textColorSecondary)) .setPageTextSize(resources.getDimensionPixelOffset(R.dimen.gallery_page_text_size)) .setPageTextTypeface(Typeface.DEFAULT) .setErrorTextColor(resources.getColor(R.color.red_500)) @@ -352,12 +370,26 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { case 2: orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; break; + case 3: + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR; + break; } setRequestedOrientation(orientation); // Screen lightness setScreenLightness(Settings.getCustomScreenLightness(), Settings.getScreenLightness()); + // Cutout + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + + GalleryHeader galleryHeader = findViewById(R.id.gallery_header); + galleryHeader.setOnApplyWindowInsetsListener((v, insets) -> { + galleryHeader.setDisplayCutout(insets.getDisplayCutout()); + return insets; + }); + } + if (Settings.getGuideGallery()) { FrameLayout mainLayout = (FrameLayout) ViewUtils.$$(this, R.id.main); mainLayout.addView(new GalleryGuideView(this)); @@ -413,13 +445,15 @@ protected void onResume() { public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); - if (hasFocus && mSystemUiHelper != null) { - if (mShowSystemUi) { - mSystemUiHelper.show(); - } else { - mSystemUiHelper.hide(); + SimpleHandler.getInstance().postDelayed(() -> { + if (hasFocus && mSystemUiHelper != null) { + if (mShowSystemUi) { + mSystemUiHelper.show(); + } else { + mSystemUiHelper.hide(); + } } - } + }, 300); } @Override @@ -612,6 +646,16 @@ public void onTapMenuArea() { SimpleHandler.getInstance().post(task); } + @Override + public void onTapErrorText(int index) { + NotifyTask task = mNotifyTaskPool.pop(); + if (task == null) { + task = new NotifyTask(); + } + task.setData(NotifyTask.KEY_TAP_ERROR_TEXT, index); + SimpleHandler.getInstance().post(task); + } + @Override public void onLongPressPage(int index) { NotifyTask task = mNotifyTaskPool.pop(); @@ -702,6 +746,7 @@ private class GalleryMenuHelper implements DialogInterface.OnClickListener { private final SwitchCompat mShowClock; private final SwitchCompat mShowProgress; private final SwitchCompat mShowBattery; + private final SwitchCompat mShowPageInterval; private final SwitchCompat mVolumePage; private final SwitchCompat mReadingFullscreen; private final SwitchCompat mCustomScreenLightness; @@ -718,6 +763,7 @@ public GalleryMenuHelper(Context context) { mShowClock = (SwitchCompat) mView.findViewById(R.id.show_clock); mShowProgress = (SwitchCompat) mView.findViewById(R.id.show_progress); mShowBattery = (SwitchCompat) mView.findViewById(R.id.show_battery); + mShowPageInterval = (SwitchCompat) mView.findViewById(R.id.show_page_interval); mVolumePage = (SwitchCompat) mView.findViewById(R.id.volume_page); mReadingFullscreen = (SwitchCompat) mView.findViewById(R.id.reading_fullscreen); mCustomScreenLightness = (SwitchCompat) mView.findViewById(R.id.custom_screen_lightness); @@ -731,6 +777,7 @@ public GalleryMenuHelper(Context context) { mShowClock.setChecked(Settings.getShowClock()); mShowProgress.setChecked(Settings.getShowProgress()); mShowBattery.setChecked(Settings.getShowBattery()); + mShowPageInterval.setChecked(Settings.getShowPageInterval()); mVolumePage.setChecked(Settings.getVolumePage()); mReadingFullscreen.setChecked(Settings.getReadingFullscreen()); mCustomScreenLightness.setChecked(Settings.getCustomScreenLightness()); @@ -751,6 +798,10 @@ public View getView() { @Override public void onClick(DialogInterface dialog, int which) { + if (mGalleryView == null) { + return; + } + int screenRotation = mScreenRotation.getSelectedItemPosition(); int layoutMode = GalleryView.sanitizeLayoutMode(mReadingDirection.getSelectedItemPosition()); int scaleMode = GalleryView.sanitizeScaleMode(mScaleMode.getSelectedItemPosition()); @@ -759,6 +810,7 @@ public void onClick(DialogInterface dialog, int which) { boolean showClock = mShowClock.isChecked(); boolean showProgress = mShowProgress.isChecked(); boolean showBattery = mShowBattery.isChecked(); + boolean showPageInterval = mShowPageInterval.isChecked(); boolean volumePage = mVolumePage.isChecked(); boolean readingFullscreen = mReadingFullscreen.isChecked(); boolean customScreenLightness = mCustomScreenLightness.isChecked(); @@ -774,6 +826,7 @@ public void onClick(DialogInterface dialog, int which) { Settings.putShowClock(showClock); Settings.putShowProgress(showProgress); Settings.putShowBattery(showBattery); + Settings.putShowPageInterval(showPageInterval); Settings.putVolumePage(volumePage); Settings.putReadingFullscreen(readingFullscreen); Settings.putCustomScreenLightness(customScreenLightness); @@ -791,13 +844,14 @@ public void onClick(DialogInterface dialog, int which) { case 2: orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; break; + case 3: + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR; + break; } setRequestedOrientation(orientation); - if (mGalleryView != null) { - mGalleryView.setLayoutMode(layoutMode); - mGalleryView.setScaleMode(scaleMode); - mGalleryView.setStartPosition(startPosition); - } + mGalleryView.setLayoutMode(layoutMode); + mGalleryView.setScaleMode(scaleMode); + mGalleryView.setStartPosition(startPosition); if (keepScreenOn) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { @@ -812,6 +866,8 @@ public void onClick(DialogInterface dialog, int which) { if (mBattery != null) { mBattery.setVisibility(showBattery ? View.VISIBLE : View.GONE); } + mGalleryView.setPagerInterval(showPageInterval ? getResources().getDimensionPixelOffset(R.dimen.gallery_pager_interval) : 0); + mGalleryView.setScrollInterval(showPageInterval ? getResources().getDimensionPixelOffset(R.dimen.gallery_scroll_interval) : 0); setScreenLightness(customScreenLightness, screenLightness); // Update slider @@ -829,7 +885,7 @@ private void shareImage(int page) { return; } - File dir = AppConfig.getExternalImageDir(); + File dir = AppConfig.getExternalTempDir(); if (null == dir) { Toast.makeText(this, R.string.error_cant_create_temp_file, Toast.LENGTH_SHORT).show(); return; @@ -853,18 +909,22 @@ private void shareImage(int page) { Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) - .authority("com.hippo.ehviewer.fileprovider") - .appendPath("image") + .authority(BuildConfig.FILE_PROVIDER_AUTHORITY) + .appendPath("temp") .appendPath(filename) .build(); + Intent intent = new Intent(); intent.setAction(Intent.ACTION_SEND); intent.putExtra(Intent.EXTRA_STREAM, uri); intent.setType(mimeType); - startActivity(Intent.createChooser(intent, getString(R.string.share_image))); - // Sync media store - sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, file.getUri())); + try { + startActivity(Intent.createChooser(intent, getString(R.string.share_image))); + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); + Toast.makeText(this, R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show(); + } } private void saveImage(int page) { @@ -889,32 +949,113 @@ private void saveImage(int page) { sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, file.getUri())); } + private void saveImageTo(int page) { + if (null == mGalleryProvider) { + return; + } + File dir = getCacheDir(); + UniFile file; + if (null == (file = mGalleryProvider.save(page, UniFile.fromFile(dir), mGalleryProvider.getImageFilename(page)))) { + Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); + return; + } + String filename = file.getName(); + if (filename == null) { + Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); + return; + } + mCacheFileName = filename; + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("image/*"); + intent.putExtra(Intent.EXTRA_TITLE, filename); + try { + startActivityForResult(intent, WRITE_REQUEST_CODE); + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); + Toast.makeText(this, R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent resultData) { + if (requestCode == WRITE_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + if (resultData != null){ + Uri uri = resultData.getData(); + String filepath = getCacheDir() + "/" + mCacheFileName; + File cachefile = new File(filepath); + + InputStream is = null; + OutputStream os = null; + ContentResolver resolver = getContentResolver(); + + try { + is = new FileInputStream(cachefile); + os = resolver.openOutputStream(uri); + IOUtils.copy(is, os); + } catch (IOException e) { + e.printStackTrace(); + } finally { + IOUtils.closeQuietly(is); + IOUtils.closeQuietly(os); + } + + cachefile.delete(); + + Toast.makeText(this, getString(R.string.image_saved, uri.getPath()), Toast.LENGTH_SHORT).show(); + // Sync media store + sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri)); + } + } + } + private void showPageDialog(final int page) { Resources resources = GalleryActivity.this.getResources(); - new AlertDialog.Builder(GalleryActivity.this) - .setTitle(resources.getString(R.string.page_menu_title, page + 1)) - .setItems(R.array.page_menu_entries, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (mGalleryProvider == null) { - return; - } - - switch (which) { - case 0: // Refresh - mGalleryProvider.forceRequest(page); - break; - case 1: // Share - shareImage(page); - break; - case 2: // Save - saveImage(page); - break; - case 3: // Add a bookmark - break; - } - } - }).show(); + AlertDialog.Builder builder = new AlertDialog.Builder(GalleryActivity.this); + builder.setTitle(resources.getString(R.string.page_menu_title, page + 1)); + + final CharSequence[] items; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){ + items = new CharSequence[]{ + getString(R.string.page_menu_refresh), + getString(R.string.page_menu_share), + getString(R.string.page_menu_save), + getString(R.string.page_menu_save_to)}; + }else { + items = new CharSequence[]{ + getString(R.string.page_menu_refresh), + getString(R.string.page_menu_share), + getString(R.string.page_menu_save)}; + } + pageDialogListener(builder, items, page); + builder.show(); + } + + private void pageDialogListener(AlertDialog.Builder builder, CharSequence[] items, int page){ + builder.setItems(items, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (mGalleryProvider == null) { + return; + } + + switch (which) { + case 0: // Refresh + mGalleryProvider.removeCache(page); + mGalleryProvider.forceRequest(page); + break; + case 1: // Share + shareImage(page); + break; + case 2: // Save + saveImage(page); + break; + case 3: // Save to + saveImageTo(page); + break; + } + } + }); } private class NotifyTask implements Runnable { @@ -924,7 +1065,8 @@ private class NotifyTask implements Runnable { public static final int KEY_CURRENT_INDEX = 2; public static final int KEY_TAP_SLIDER_AREA = 3; public static final int KEY_TAP_MENU_AREA = 4; - public static final int KEY_LONG_PRESS_PAGE = 5; + public static final int KEY_TAP_ERROR_TEXT = 5; + public static final int KEY_LONG_PRESS_PAGE = 6; private int mKey; private int mValue; @@ -957,6 +1099,12 @@ private void onTapSliderArea() { } } + private void onTapErrorText(int index) { + if (mGalleryProvider != null) { + mGalleryProvider.forceRequest(index); + } + } + private void onLongPressPage(final int index) { showPageDialog(index); } @@ -984,6 +1132,9 @@ public void run() { case KEY_TAP_SLIDER_AREA: onTapSliderArea(); break; + case KEY_TAP_ERROR_TEXT: + onTapErrorText(mValue); + break; case KEY_LONG_PRESS_PAGE: onLongPressPage(mValue); break; diff --git a/app/src/main/java/com/hippo/ehviewer/ui/HostsActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/HostsActivity.java new file mode 100644 index 000000000..f1790c740 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/HostsActivity.java @@ -0,0 +1,275 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.ui; + +/* + * Created by Hippo on 2018/3/23. + */ + +import android.app.Dialog; +import android.content.DialogInterface; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.textfield.TextInputLayout; +import com.hippo.android.resource.AttrResources; +import com.hippo.easyrecyclerview.EasyRecyclerView; +import com.hippo.easyrecyclerview.LinearDividerItemDecoration; +import com.hippo.ehviewer.EhApplication; +import com.hippo.ehviewer.Hosts; +import com.hippo.ehviewer.R; +import com.hippo.ripple.Ripple; +import com.hippo.yorozuya.LayoutUtils; +import java.util.List; +import java.util.Locale; + +public class HostsActivity extends ToolbarActivity + implements EasyRecyclerView.OnItemClickListener, View.OnClickListener { + + private static final String DIALOG_TAG_ADD_HOST = AddHostDialogFragment.class.getName(); + private static final String DIALOG_TAG_EDIT_HOST = EditHostDialogFragment.class.getName(); + + private static final String KEY_HOST = "com.hippo.ehviewer.ui.HostsActivity.HOST"; + private static final String KEY_IP = "com.hippo.ehviewer.ui.HostsActivity.IP"; + + private Hosts hosts; + private List> data; + + private EasyRecyclerView recyclerView; + private View tip; + private HostsAdapter adapter; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + hosts = EhApplication.getHosts(this); + data = hosts.getAll(); + + setContentView(R.layout.activity_hosts); + setNavigationIcon(R.drawable.v_arrow_left_dark_x24); + recyclerView = findViewById(R.id.recycler_view); + tip = findViewById(R.id.tip); + FloatingActionButton fab = findViewById(R.id.fab); + + adapter = new HostsAdapter(); + recyclerView.setAdapter(adapter); + recyclerView.setLayoutManager(new LinearLayoutManager(this, RecyclerView.VERTICAL, false)); + LinearDividerItemDecoration decoration = new LinearDividerItemDecoration( + LinearDividerItemDecoration.VERTICAL, + AttrResources.getAttrColor(this, R.attr.dividerColor), + LayoutUtils.dp2pix(this, 1)); + decoration.setShowLastDivider(true); + recyclerView.addItemDecoration(decoration); + recyclerView.setSelector(Ripple.generateRippleDrawable(this, !AttrResources.getAttrBoolean(this, R.attr.isLightTheme), new ColorDrawable(Color.TRANSPARENT))); + recyclerView.setHasFixedSize(true); + recyclerView.setOnItemClickListener(this); + recyclerView.setPadding( + recyclerView.getPaddingLeft(), + recyclerView.getPaddingTop(), + recyclerView.getPaddingRight(), + recyclerView.getPaddingBottom() + getResources().getDimensionPixelOffset(R.dimen.gallery_padding_bottom_fab)); + + fab.setOnClickListener(this); + + recyclerView.setVisibility(data.isEmpty() ? View.GONE : View.VISIBLE); + tip.setVisibility(data.isEmpty() ? View.VISIBLE : View.GONE); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public boolean onItemClick(EasyRecyclerView easyRecyclerView, View view, int position, long id) { + Pair pair = data.get(position); + Bundle args = new Bundle(); + args.putString(KEY_HOST, pair.first); + args.putString(KEY_IP, pair.second); + + DialogFragment fragment = new EditHostDialogFragment(); + fragment.setArguments(args); + fragment.show(getSupportFragmentManager(), DIALOG_TAG_EDIT_HOST); + + return true; + } + + @Override + public void onClick(View v) { + new AddHostDialogFragment().show(getSupportFragmentManager(), DIALOG_TAG_ADD_HOST); + } + + private void notifyHostsChanges() { + data = hosts.getAll(); + recyclerView.setVisibility(data.isEmpty() ? View.GONE : View.VISIBLE); + tip.setVisibility(data.isEmpty() ? View.VISIBLE : View.GONE); + adapter.notifyDataSetChanged(); + } + + private class HostsHolder extends RecyclerView.ViewHolder { + + public final TextView host; + public final TextView ip; + + public HostsHolder(View itemView) { + super(itemView); + host = itemView.findViewById(R.id.host); + ip = itemView.findViewById(R.id.ip); + } + } + + private class HostsAdapter extends RecyclerView.Adapter { + + private final LayoutInflater inflater = getLayoutInflater(); + + @NonNull + @Override + public HostsHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new HostsHolder(inflater.inflate(R.layout.item_hosts, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull HostsHolder holder, int position) { + Pair pair = data.get(position); + holder.host.setText(pair.first); + holder.ip.setText(pair.second); + } + + @Override + public int getItemCount() { + return data.size(); + } + } + + public abstract static class HostDialogFragment extends DialogFragment { + + private TextView host; + private TextView ip; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + View view = getActivity().getLayoutInflater().inflate(R.layout.dialog_hosts, null, false); + host = view.findViewById(R.id.host); + ip = view.findViewById(R.id.ip); + + Bundle arguments = getArguments(); + if (savedInstanceState == null && arguments != null) { + host.setText(arguments.getString(KEY_HOST)); + ip.setText(arguments.getString(KEY_IP)); + } + + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()).setView(view); + onCreateDialogBuilder(builder); + AlertDialog dialog = builder.create(); + dialog.setOnShowListener(d -> onCreateDialog((AlertDialog) d)); + + return dialog; + } + + protected abstract void onCreateDialogBuilder(AlertDialog.Builder builder); + + protected abstract void onCreateDialog(AlertDialog dialog); + + protected void put(AlertDialog dialog) { + TextView host = dialog.findViewById(R.id.host); + TextView ip = dialog.findViewById(R.id.ip); + String hostString = host.getText().toString().trim().toLowerCase(Locale.US); + String ipString = ip.getText().toString().trim(); + + if (!Hosts.isValidHost(hostString)) { + TextInputLayout hostInputLayout = dialog.findViewById(R.id.host_input_layout); + hostInputLayout.setError(getContext().getString(R.string.invalid_host)); + return; + } + + if (!Hosts.isValidIp(ipString)) { + TextInputLayout ipInputLayout = dialog.findViewById(R.id.ip_input_layout); + ipInputLayout.setError(getContext().getString(R.string.invalid_ip)); + return; + } + + HostsActivity activity = (HostsActivity) dialog.getOwnerActivity(); + activity.hosts.put(hostString, ipString); + activity.notifyHostsChanges(); + + dialog.dismiss(); + } + + protected void delete(AlertDialog dialog) { + TextView host = dialog.findViewById(R.id.host); + String hostString = host.getText().toString().trim().toLowerCase(Locale.US); + + HostsActivity activity = (HostsActivity) dialog.getOwnerActivity(); + activity.hosts.delete(hostString); + activity.notifyHostsChanges(); + + dialog.dismiss(); + } + } + + public static class AddHostDialogFragment extends HostDialogFragment { + + @Override + protected void onCreateDialogBuilder(AlertDialog.Builder builder) { + builder.setTitle(R.string.add_host); + builder.setPositiveButton(R.string.add_host_add, null); + } + + @Override + protected void onCreateDialog(AlertDialog dialog) { + dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> put(dialog)); + } + } + + public static class EditHostDialogFragment extends HostDialogFragment { + + @Override + protected void onCreateDialogBuilder(AlertDialog.Builder builder) { + builder.setTitle(R.string.edit_host); + builder.setPositiveButton(R.string.edit_host_confirm, null); + builder.setNegativeButton(R.string.edit_host_delete, null); + } + + @Override + protected void onCreateDialog(AlertDialog dialog) { + dialog.findViewById(R.id.host_input_layout).setEnabled(false); + dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> put(dialog)); + dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(v -> delete(dialog)); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/LicenseActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/LicenseActivity.java index 17a1f3c85..0f335e591 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/LicenseActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/LicenseActivity.java @@ -17,10 +17,9 @@ package com.hippo.ehviewer.ui; import android.os.Bundle; -import android.support.annotation.Nullable; import android.view.MenuItem; import android.webkit.WebView; - +import androidx.annotation.Nullable; import com.hippo.ehviewer.R; public class LicenseActivity extends ToolbarActivity { diff --git a/app/src/main/java/com/hippo/ehviewer/ui/MainActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/MainActivity.java index f0032a5c9..541984c35 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/MainActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/MainActivity.java @@ -17,6 +17,9 @@ package com.hippo.ehviewer.ui; import android.Manifest; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Bitmap; @@ -25,33 +28,35 @@ import android.os.Build; import android.os.Bundle; import android.os.PersistableBundle; -import android.support.annotation.IdRes; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.StringRes; -import android.support.design.widget.NavigationView; -import android.support.design.widget.Snackbar; -import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.view.Gravity; import android.view.MenuItem; import android.view.View; +import android.widget.Button; import android.widget.FrameLayout; import android.widget.TextView; import android.widget.Toast; - +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; +import com.google.android.material.navigation.NavigationView; +import com.google.android.material.snackbar.Snackbar; import com.hippo.drawerlayout.DrawerLayout; import com.hippo.ehviewer.AppConfig; -import com.hippo.ehviewer.Crash; +import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; +import com.hippo.ehviewer.client.EhTagDatabase; import com.hippo.ehviewer.client.EhUrlOpener; import com.hippo.ehviewer.client.EhUtils; import com.hippo.ehviewer.client.data.ListUrlBuilder; +import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser; +import com.hippo.ehviewer.client.parser.GalleryPageUrlParser; import com.hippo.ehviewer.ui.scene.AnalyticsScene; import com.hippo.ehviewer.ui.scene.BaseScene; import com.hippo.ehviewer.ui.scene.CookieSignInScene; -import com.hippo.ehviewer.ui.scene.CrashScene; import com.hippo.ehviewer.ui.scene.DownloadLabelsScene; import com.hippo.ehviewer.ui.scene.DownloadsScene; import com.hippo.ehviewer.ui.scene.FavoritesScene; @@ -81,8 +86,8 @@ import com.hippo.widget.LoadImageView; import com.hippo.yorozuya.IOUtils; import com.hippo.yorozuya.ResourcesUtils; +import com.hippo.yorozuya.SimpleHandler; import com.hippo.yorozuya.ViewUtils; - import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -96,6 +101,7 @@ public final class MainActivity extends StageActivity private static final int REQUEST_CODE_SETTINGS = 0; private static final String KEY_NAV_CHECKED_ITEM = "nav_checked_item"; + private static final String KEY_CLIP_TEXT_HASH_CODE = "clip_text_hash_code"; /*--------------- Whole life cycle @@ -110,6 +116,8 @@ public final class MainActivity extends StageActivity private LoadImageView mAvatar; @Nullable private TextView mDisplayName; + @Nullable + private Button mChangeTheme; private int mNavCheckedItem = 0; @@ -117,7 +125,6 @@ public final class MainActivity extends StageActivity registerLaunchMode(SecurityScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK); registerLaunchMode(WarningScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK); registerLaunchMode(AnalyticsScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK); - registerLaunchMode(CrashScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK); registerLaunchMode(SignInScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK); registerLaunchMode(WebViewSignInScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK); registerLaunchMode(CookieSignInScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK); @@ -135,6 +142,19 @@ public final class MainActivity extends StageActivity registerLaunchMode(ProgressScene.class, SceneFragment.LAUNCH_MODE_STANDARD); } + @Override + protected int getThemeResId(int theme) { + switch (theme) { + case Settings.THEME_LIGHT: + default: + return R.style.AppTheme_Main; + case Settings.THEME_DARK: + return R.style.AppTheme_Main_Dark; + case Settings.THEME_BLACK: + return R.style.AppTheme_Main_Black; + } + } + @Override public int getContainerViewId() { return R.id.fragment_container; @@ -149,15 +169,13 @@ protected Announcer getLaunchAnnouncer() { return new Announcer(WarningScene.class); } else if (Settings.getAskAnalytics()) { return new Announcer(AnalyticsScene.class); - } else if (Crash.hasCrashFile()) { - return new Announcer(CrashScene.class); } else if (EhUtils.needSignedIn(this)) { return new Announcer(SignInScene.class); } else if (Settings.getSelectSite()) { return new Announcer(SelectSiteScene.class); } else { Bundle args = new Bundle(); - args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_HOMEPAGE); + args.putString(GalleryListScene.KEY_ACTION, Settings.getLaunchPageGalleryListSceneAction()); return new Announcer(GalleryListScene.class).setArgs(args); } } @@ -180,11 +198,6 @@ private Announcer processAnnouncer(Announcer announcer) { newArgs.putString(AnalyticsScene.KEY_TARGET_SCENE, announcer.getClazz().getName()); newArgs.putBundle(AnalyticsScene.KEY_TARGET_ARGS, announcer.getArgs()); return new Announcer(AnalyticsScene.class).setArgs(newArgs); - } else if (Crash.hasCrashFile()) { - Bundle newArgs = new Bundle(); - newArgs.putString(CrashScene.KEY_TARGET_SCENE, announcer.getClazz().getName()); - newArgs.putBundle(CrashScene.KEY_TARGET_ARGS, announcer.getArgs()); - return new Announcer(CrashScene.class).setArgs(newArgs); } else if (EhUtils.needSignedIn(this)) { Bundle newArgs = new Bundle(); newArgs.putString(SignInScene.KEY_TARGET_SCENE, announcer.getClazz().getName()); @@ -239,7 +252,11 @@ private boolean handleIntent(Intent intent) { String action = intent.getAction(); if (Intent.ACTION_VIEW.equals(action)) { - Announcer announcer = EhUrlOpener.parseUrl(intent.getData().toString()); + Uri uri = intent.getData(); + if (uri == null) { + return false; + } + Announcer announcer = EhUrlOpener.parseUrl(uri.toString()); if (announcer != null) { startScene(processAnnouncer(announcer)); return true; @@ -292,7 +309,7 @@ protected void onUnrecognizedIntent(@Nullable Intent intent) { finish(); } else { Bundle args = new Bundle(); - args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_HOMEPAGE); + args.putString(GalleryListScene.KEY_ACTION, Settings.getLaunchPageGalleryListSceneAction()); startScene(processAnnouncer(new Announcer(GalleryListScene.class).setArgs(args))); } } @@ -315,6 +332,7 @@ protected void onCreate2(@Nullable Bundle savedInstanceState) { View headerLayout = mNavView.getHeaderView(0); mAvatar = (LoadImageView) ViewUtils.$$(headerLayout, R.id.avatar); mDisplayName = (TextView) ViewUtils.$$(headerLayout, R.id.display_name); + mChangeTheme = (Button) ViewUtils.$$(this, R.id.change_theme); mDrawerLayout.setStatusBarColor(ResourcesUtils.getAttrColor(this, R.attr.colorPrimaryDark)); // Pre-L need shadow drawable @@ -329,6 +347,12 @@ protected void onCreate2(@Nullable Bundle savedInstanceState) { mNavView.setNavigationItemSelectedListener(this); } + mChangeTheme.setText(getThemeText()); + mChangeTheme.setOnClickListener(v -> { + Settings.putTheme(getNextTheme()); + ((EhApplication) getApplication()).recreate(); + }); + if (savedInstanceState == null) { onInit(); CommonOperations.checkUpdate(this, false); @@ -339,6 +363,37 @@ protected void onCreate2(@Nullable Bundle savedInstanceState) { } else { onRestore(savedInstanceState); } + + EhTagDatabase.update(this); + } + + private String getThemeText() { + int resId; + switch (Settings.getTheme()) { + default: + case Settings.THEME_LIGHT: + resId = R.string.theme_light; + break; + case Settings.THEME_DARK: + resId = R.string.theme_dark; + break; + case Settings.THEME_BLACK: + resId = R.string.theme_black; + break; + } + return getString(resId); + } + + private int getNextTheme() { + switch (Settings.getTheme()) { + default: + case Settings.THEME_LIGHT: + return Settings.THEME_DARK; + case Settings.THEME_DARK: + return Settings.THEME_BLACK; + case Settings.THEME_BLACK: + return Settings.THEME_LIGHT; + } } private void checkDownloadLocation() { @@ -392,6 +447,79 @@ protected void onResume() { super.onResume(); setNavCheckedItem(mNavCheckedItem); + + checkClipboardUrl(); + } + + @Override + protected void onTransactScene() { + super.onTransactScene(); + + checkClipboardUrl(); + } + + private void checkClipboardUrl() { + SimpleHandler.getInstance().postDelayed(() -> { + if (!isSolid()) { + checkClipboardUrlInternal(); + } + }, 300); + } + + private boolean isSolid() { + Class topClass = getTopSceneClass(); + return topClass == null || SolidScene.class.isAssignableFrom(topClass); + } + + private String getTextFromClipboard() { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboard != null) { + ClipData clip = clipboard.getPrimaryClip(); + if (clip != null && clip.getItemCount() > 0 && clip.getItemAt(0).getText() != null) { + return clip.getItemAt(0).getText().toString(); + } + } + return null; + } + + @Nullable + private Announcer createAnnouncerFromClipboardUrl(String url) { + GalleryDetailUrlParser.Result result1 = GalleryDetailUrlParser.parse(url, false); + if (result1 != null) { + Bundle args = new Bundle(); + args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN); + args.putLong(GalleryDetailScene.KEY_GID, result1.gid); + args.putString(GalleryDetailScene.KEY_TOKEN, result1.token); + return new Announcer(GalleryDetailScene.class).setArgs(args); + } + + GalleryPageUrlParser.Result result2 = GalleryPageUrlParser.parse(url, false); + if (result2 != null) { + Bundle args = new Bundle(); + args.putString(ProgressScene.KEY_ACTION, ProgressScene.ACTION_GALLERY_TOKEN); + args.putLong(ProgressScene.KEY_GID, result2.gid); + args.putString(ProgressScene.KEY_PTOKEN, result2.pToken); + args.putInt(ProgressScene.KEY_PAGE, result2.page); + return new Announcer(ProgressScene.class).setArgs(args); + } + + return null; + } + + private void checkClipboardUrlInternal() { + String text = getTextFromClipboard(); + int hashCode = text != null ? text.hashCode() : 0; + + if (text != null && hashCode != 0 && Settings.getClipboardTextHashCode() != hashCode) { + Announcer announcer = createAnnouncerFromClipboardUrl(text); + if (announcer != null && mDrawerLayout != null) { + Snackbar snackbar = Snackbar.make(mDrawerLayout, R.string.clipboard_gallery_url_snack_message, Snackbar.LENGTH_INDEFINITE); + snackbar.setAction(R.string.clipboard_gallery_url_snack_action, v -> startScene(announcer)); + snackbar.show(); + } + } + + Settings.putClipboardTextHashCode(hashCode); } @Override @@ -413,7 +541,7 @@ public void onSceneViewCreated(SceneFragment scene, Bundle savedInstanceState) { if (scene instanceof BaseScene && mRightDrawer != null && mDrawerLayout != null) { BaseScene baseScene = (BaseScene) scene; mRightDrawer.removeAllViews(); - View drawerView = baseScene.onCreateDrawerView( + View drawerView = baseScene.createDrawerView( baseScene.getLayoutInflater2(), mRightDrawer, savedInstanceState); if (drawerView != null) { mRightDrawer.addView(drawerView); @@ -430,7 +558,7 @@ public void onSceneViewDestroyed(SceneFragment scene) { if (scene instanceof BaseScene) { BaseScene baseScene = (BaseScene) scene; - baseScene.onDestroyDrawerView(); + baseScene.destroyDrawerView(); } } @@ -501,6 +629,20 @@ public void toggleDrawer(int drawerGravity) { } } + public void setDrawerGestureBlocker(DrawerLayout.GestureBlocker gestureBlocker) { + if (mDrawerLayout != null) { + mDrawerLayout.setGestureBlocker(gestureBlocker); + } + } + + public boolean isDrawersVisible() { + if (mDrawerLayout != null) { + return mDrawerLayout.isDrawersVisible(); + } else { + return false; + } + } + public void setNavCheckedItem(@IdRes int resId) { mNavCheckedItem = resId; if (mNavView != null) { @@ -522,7 +664,7 @@ public void showTip(@StringRes int id, int length) { public void showTip(CharSequence message, int length) { if (null != mDrawerLayout) { Snackbar.make(mDrawerLayout, message, - length == BaseScene.LENGTH_LONG ? Snackbar.LENGTH_LONG : Snackbar.LENGTH_SHORT).show(); + length == BaseScene.LENGTH_LONG ? 5000 : 3000).show(); } else { Toast.makeText(this, message, length == BaseScene.LENGTH_LONG ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show(); @@ -553,6 +695,11 @@ public boolean onNavigationItemSelected(MenuItem item) { args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_HOMEPAGE); startSceneFirstly(new Announcer(GalleryListScene.class) .setArgs(args)); + } else if (id == R.id.nav_subscription) { + Bundle args = new Bundle(); + args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_SUBSCRIPTION); + startSceneFirstly(new Announcer(GalleryListScene.class) + .setArgs(args)); } else if (id == R.id.nav_whats_hot) { Bundle args = new Bundle(); args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_WHATS_HOT); diff --git a/app/src/main/java/com/hippo/ehviewer/ui/MyTagsActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/MyTagsActivity.java new file mode 100644 index 000000000..7a1d07e13 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/MyTagsActivity.java @@ -0,0 +1,109 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.ui; + +import android.annotation.SuppressLint; +import android.graphics.Bitmap; +import android.os.Build; +import android.os.Bundle; +import android.view.MenuItem; +import android.view.View; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.annotation.Nullable; +import com.hippo.ehviewer.EhApplication; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.client.EhCookieStore; +import com.hippo.ehviewer.client.EhUrl; +import com.hippo.ehviewer.widget.DialogWebChromeClient; +import com.hippo.widget.ProgressView; +import okhttp3.Cookie; +import okhttp3.HttpUrl; + +public class MyTagsActivity extends ToolbarActivity { + + private WebView webView; + private ProgressView progress; + private String url; + + @SuppressLint("SetJavaScriptEnabled") + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // http://stackoverflow.com/questions/32284642/how-to-handle-an-uncatched-exception + CookieManager cookieManager = CookieManager.getInstance(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + cookieManager.flush(); + cookieManager.removeAllCookies(null); + cookieManager.removeSessionCookies(null); + } else { + CookieSyncManager cookieSyncManager = CookieSyncManager.createInstance(this); + cookieSyncManager.startSync(); + cookieManager.removeAllCookie(); + cookieManager.removeSessionCookie(); + cookieSyncManager.stopSync(); + } + + // Copy cookies from okhttp cookie store to CookieManager + url = EhUrl.getMyTagsUrl(); + EhCookieStore store = EhApplication.getEhCookieStore(this); + for (Cookie cookie : store.getCookies(HttpUrl.parse(url))) { + cookieManager.setCookie(url, cookie.toString()); + } + + setContentView(R.layout.activity_my_tags); + setNavigationIcon(R.drawable.v_arrow_left_dark_x24); + webView = findViewById(R.id.webview); + webView.getSettings().setJavaScriptEnabled(true); + webView.setWebViewClient(new MyTagsWebViewClient()); + webView.setWebChromeClient(new DialogWebChromeClient(this)); + webView.loadUrl(url); + progress = findViewById(R.id.progress); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private class MyTagsWebViewClient extends WebViewClient { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + // Never load other urls + return !url.equals(MyTagsActivity.this.url); + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + progress.setVisibility(View.VISIBLE); + } + + @Override + public void onPageFinished(WebView view, String url) { + progress.setVisibility(View.GONE); + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/SetSecurityActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/SetSecurityActivity.java index e17ca11a0..30041fab0 100755 --- a/app/src/main/java/com/hippo/ehviewer/ui/SetSecurityActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/SetSecurityActivity.java @@ -17,13 +17,14 @@ package com.hippo.ehviewer.ui; import android.hardware.fingerprint.FingerprintManager; +import android.os.Build; import android.os.Bundle; -import android.support.annotation.Nullable; import android.text.TextUtils; import android.view.MenuItem; import android.view.View; import android.widget.CheckBox; - +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; import com.hippo.widget.lockpattern.LockPatternUtils; @@ -62,7 +63,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { FingerprintManager fingerprintManager = getSystemService(FingerprintManager.class); // The line below prevents the false positive inspection from Android Studio // noinspection ResourceType - if (fingerprintManager.hasEnrolledFingerprints()) { + if (fingerprintManager != null && hasEnrolledFingerprints(fingerprintManager)) { mFingerprint.setVisibility(View.VISIBLE); mFingerprint.setChecked(Settings.getEnableFingerprint()); } @@ -72,6 +73,16 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { mSet.setOnClickListener(this); } + @RequiresApi(api = Build.VERSION_CODES.M) + public static boolean hasEnrolledFingerprints(FingerprintManager fingerprintManager) { + try { + return fingerprintManager.isHardwareDetected() + && fingerprintManager.hasEnrolledFingerprints(); + } catch (Throwable e) { + return false; + } + } + @Override protected void onDestroy() { super.onDestroy(); diff --git a/app/src/main/java/com/hippo/ehviewer/ui/SettingsActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/SettingsActivity.java index 4c0d61dba..54b2b53eb 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/SettingsActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/SettingsActivity.java @@ -17,30 +17,22 @@ package com.hippo.ehviewer.ui; import android.app.Fragment; -import android.content.Context; import android.content.Intent; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.support.annotation.StringRes; -import android.support.v7.app.ActionBar; -import android.support.v7.app.ActionBarDrawerToggle; -import android.view.LayoutInflater; import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ListAdapter; - +import androidx.annotation.StringRes; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.ActionBarDrawerToggle; import com.hippo.ehviewer.R; +import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.ui.fragment.AboutFragment; import com.hippo.ehviewer.ui.fragment.AdvancedFragment; import com.hippo.ehviewer.ui.fragment.DownloadFragment; import com.hippo.ehviewer.ui.fragment.EhFragment; +import com.hippo.ehviewer.ui.fragment.PrivacyFragment; import com.hippo.ehviewer.ui.fragment.ReadFragment; import com.hippo.util.DrawableManager; - -import java.lang.reflect.Field; import java.util.List; public final class SettingsActivity extends EhPreferenceActivity { @@ -53,77 +45,20 @@ public final class SettingsActivity extends EhPreferenceActivity { DownloadFragment.class.getName(), AdvancedFragment.class.getName(), AboutFragment.class.getName(), + PrivacyFragment.class.getName(), }; - private class FakeLayoutInflater extends LayoutInflater { - - private final LayoutInflater mInflater; - - protected FakeLayoutInflater(LayoutInflater inflater) { - super(null); - mInflater = inflater; - } - - @Override - public LayoutInflater cloneInContext(Context newContext) { - return null; - } - - @Override - public View inflate(int resource, ViewGroup root, boolean attachToRoot) { - return mInflater.inflate(R.layout.item_preference_header, root, attachToRoot); - } - } - - @SuppressWarnings("TryWithIdenticalCatches") - private void replaceHeaderLayoutResId() { - try { - ListAdapter adapter = getListAdapter(); - Class headerAdapterClazz = Class.forName("android.preference.PreferenceActivity$HeaderAdapter"); - if (!headerAdapterClazz.isInstance(adapter)) { - return; - } - - boolean ok = false; - - // For lollipop and above this work - try { - Field field = headerAdapterClazz.getDeclaredField("mLayoutResId"); - field.setAccessible(true); - field.setInt(adapter, R.layout.item_preference_header); - - ok = true; - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (NoSuchFieldException e) { - e.printStackTrace(); - } - - // For pre-lollipop this work - if (!ok) { - try { - Field field = headerAdapterClazz.getDeclaredField("mInflater"); - field.setAccessible(true); - field.set(adapter, new FakeLayoutInflater((LayoutInflater) field.get(adapter))); - - ok = true; - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (NoSuchFieldException e) { - e.printStackTrace(); - } catch (ClassCastException e) { - e.printStackTrace(); - } - } - - if (ok) { - getListView().setDivider(new ColorDrawable(Color.TRANSPARENT)); - getListView().setDividerHeight(0); - } - - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } + @Override + protected int getThemeResId(int theme) { + switch (theme) { + case Settings.THEME_LIGHT: + default: + return R.style.AppTheme_Settings; + case Settings.THEME_DARK: + return R.style.AppTheme_Settings_Dark; + case Settings.THEME_BLACK: + return R.style.AppTheme_Settings_Black; + } } private void setActionBarUpIndicator(Drawable drawable) { @@ -142,16 +77,14 @@ private void setActionBarUpIndicator(Drawable drawable) { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setActionBarUpIndicator(DrawableManager.getDrawable(this, R.drawable.v_arrow_left_dark_x24)); - - replaceHeaderLayoutResId(); + setActionBarUpIndicator(DrawableManager.getVectorDrawable(this, R.drawable.v_arrow_left_dark_x24)); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: - finish(); + onBackPressed(); return true; default: return super.onOptionsItemSelected(item); diff --git a/app/src/main/java/com/hippo/ehviewer/ui/ToolbarActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/ToolbarActivity.java index 000c5acd3..ed569bc4a 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/ToolbarActivity.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/ToolbarActivity.java @@ -17,17 +17,30 @@ package com.hippo.ehviewer.ui; import android.graphics.drawable.Drawable; -import android.support.annotation.DrawableRes; -import android.support.annotation.LayoutRes; -import android.support.annotation.Nullable; -import android.support.v7.widget.Toolbar; import android.view.View; import android.view.ViewGroup; - +import androidx.annotation.DrawableRes; +import androidx.annotation.LayoutRes; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.Toolbar; import com.hippo.ehviewer.R; +import com.hippo.ehviewer.Settings; public abstract class ToolbarActivity extends EhActivity { + @Override + protected int getThemeResId(int theme) { + switch (theme) { + case Settings.THEME_LIGHT: + default: + return R.style.AppTheme_Toolbar; + case Settings.THEME_DARK: + return R.style.AppTheme_Toolbar_Dark; + case Settings.THEME_BLACK: + return R.style.AppTheme_Toolbar_Black; + } + } + @Override public void setContentView(@LayoutRes int layoutResID) { super.setContentView(R.layout.activity_toolbar); diff --git a/app/src/main/java/com/hippo/ehviewer/ui/UConfigActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/UConfigActivity.java new file mode 100644 index 000000000..6a51aefed --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/UConfigActivity.java @@ -0,0 +1,179 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.ui; + +/* + * Created by Hippo on 2018/2/9. + */ + +import android.annotation.SuppressLint; +import android.graphics.Bitmap; +import android.os.Build; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.annotation.Nullable; +import com.google.android.material.snackbar.Snackbar; +import com.hippo.ehviewer.EhApplication; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.client.EhCookieStore; +import com.hippo.ehviewer.client.EhUrl; +import com.hippo.ehviewer.widget.DialogWebChromeClient; +import com.hippo.widget.ProgressView; +import okhttp3.Cookie; +import okhttp3.HttpUrl; + +public class UConfigActivity extends ToolbarActivity { + + private WebView webView; + private ProgressView progress; + private String url; + private boolean loaded; + + @SuppressLint("SetJavaScriptEnabled") + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // http://stackoverflow.com/questions/32284642/how-to-handle-an-uncatched-exception + CookieManager cookieManager = CookieManager.getInstance(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + cookieManager.flush(); + cookieManager.removeAllCookies(null); + cookieManager.removeSessionCookies(null); + } else { + CookieSyncManager cookieSyncManager = CookieSyncManager.createInstance(this); + cookieSyncManager.startSync(); + cookieManager.removeAllCookie(); + cookieManager.removeSessionCookie(); + cookieSyncManager.stopSync(); + } + + // Copy cookies from okhttp cookie store to CookieManager + url = EhUrl.getUConfigUrl(); + EhCookieStore store = EhApplication.getEhCookieStore(this); + for (Cookie cookie : store.getCookies(HttpUrl.parse(url))) { + cookieManager.setCookie(url, cookie.toString()); + } + + setContentView(R.layout.activity_u_config); + setNavigationIcon(R.drawable.v_arrow_left_dark_x24); + webView = (WebView) findViewById(R.id.webview); + webView.getSettings().setJavaScriptEnabled(true); + webView.setWebViewClient(new UConfigWebViewClient()); + webView.setWebChromeClient(new DialogWebChromeClient(this)); + webView.loadUrl(url); + progress = (ProgressView) findViewById(R.id.progress); + + Snackbar.make(webView, R.string.apply_tip, Snackbar.LENGTH_LONG).show(); + } + + private void apply() { + webView.loadUrl("javascript:" + + "(function() {\n" + + " var apply = document.getElementById(\"apply\").children[0];\n" + + " apply.click();\n" + + "})();"); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.activity_u_config, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + case R.id.action_apply: + if (loaded) { + apply(); + } + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private Cookie longLive(Cookie cookie) { + return new Cookie.Builder() + .name(cookie.name()) + .value(cookie.value()) + .domain(cookie.domain()) + .path(cookie.path()) + .expiresAt(Long.MAX_VALUE) + .build(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + webView.destroy(); + + // Put cookies back to okhttp cookie store + CookieManager cookieManager = CookieManager.getInstance(); + String cookiesString = cookieManager.getCookie(url); + + if (cookiesString != null && !cookiesString.isEmpty()) { + EhCookieStore store = EhApplication.getEhCookieStore(this); + HttpUrl eUrl = HttpUrl.parse(EhUrl.HOST_E); + HttpUrl exUrl = HttpUrl.parse(EhUrl.HOST_EX); + + // The cookies saved in the uconfig page should be shared between e and ex + for (String header : cookiesString.split(";")) { + Cookie eCookie = Cookie.parse(eUrl, header); + if (eCookie != null) { + store.addCookie(longLive(eCookie)); + } + + Cookie exCookie = Cookie.parse(exUrl, header); + if (exCookie != null) { + store.addCookie(longLive(exCookie)); + } + } + } + } + + private class UConfigWebViewClient extends WebViewClient { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + // Never load other urls + return true; + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + progress.setVisibility(View.VISIBLE); + loaded = false; + } + + @Override + public void onPageFinished(WebView view, String url) { + progress.setVisibility(View.GONE); + loaded = true; + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/dialog/SelectItemWithIconAdapter.java b/app/src/main/java/com/hippo/ehviewer/ui/dialog/SelectItemWithIconAdapter.java new file mode 100644 index 000000000..124380cb2 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/dialog/SelectItemWithIconAdapter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.ui.dialog; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; +import androidx.appcompat.content.res.AppCompatResources; +import com.hippo.ehviewer.R; + +public class SelectItemWithIconAdapter extends BaseAdapter { + + private Context context; + private LayoutInflater inflater; + + private CharSequence[] texts; + private int[] icons; + + public SelectItemWithIconAdapter(Context context, CharSequence[] texts, int[] icons) { + int count = texts.length; + if (count != icons.length) { + throw new IllegalArgumentException("Length conflict"); + } + this.context = context; + this.inflater = LayoutInflater.from(context); + this.texts = texts; + this.icons = icons; + } + + @Override + public int getCount() { + return texts.length; + } + + @Override + public Object getItem(int position) { + return texts[position]; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = inflater.inflate(R.layout.dialog_item_select_with_icon, parent, false); + } + TextView view = (TextView) convertView; + + view.setText(texts[position]); + + Drawable icon = AppCompatResources.getDrawable(context, icons[position]); + icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); + view.setCompoundDrawables(icon, null, null, null); + + return view; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/fragment/AboutFragment.java b/app/src/main/java/com/hippo/ehviewer/ui/fragment/AboutFragment.java index 04226c2ca..39ded05d1 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/fragment/AboutFragment.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/fragment/AboutFragment.java @@ -19,22 +19,23 @@ import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceFragment; -import android.support.v7.app.AlertDialog; +import android.util.Base64; +import android.widget.TextView; import android.widget.Toast; - -import com.hippo.ehviewer.Analytics; +import androidx.appcompat.app.AlertDialog; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.R; -import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.ui.CommonOperations; import com.hippo.util.AppHelper; +import java.io.UnsupportedEncodingException; public class AboutFragment extends PreferenceFragment - implements Preference.OnPreferenceChangeListener, - Preference.OnPreferenceClickListener { + implements Preference.OnPreferenceClickListener { private static final String KEY_AUTHOR = "author"; private static final String KEY_DONATE = "donate"; @@ -46,30 +47,14 @@ public void onCreate(Bundle savedInstanceState) { addPreferencesFromResource(R.xml.about_settings); Preference author = findPreference(KEY_AUTHOR); - Preference enableAnalytics = findPreference(Settings.KEY_ENABLE_ANALYTICS); Preference donate = findPreference(KEY_DONATE); Preference checkForUpdate = findPreference(KEY_CHECK_FOR_UPDATES); author.setSummary(getString(R.string.settings_about_author_summary).replace('$', '@')); - donate.setSummary(getString(R.string.settings_about_donate_summary).replace('$', '@')); author.setOnPreferenceClickListener(this); donate.setOnPreferenceClickListener(this); checkForUpdate.setOnPreferenceClickListener(this); - - enableAnalytics.setOnPreferenceChangeListener(this); - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - String key = preference.getKey(); - if (Settings.KEY_ENABLE_ANALYTICS.equals(key)) { - if (newValue instanceof Boolean && (Boolean) newValue) { - Analytics.start(getActivity()); - } - return true; - } - return true; } @Override @@ -79,17 +64,51 @@ public boolean onPreferenceClick(Preference preference) { AppHelper.sendEmail(getActivity(), EhApplication.getDeveloperEmail(), "About EhViewer", null); } else if (KEY_DONATE.equals(key)) { - ClipboardManager cmb = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); - cmb.setPrimaryClip(ClipData.newPlainText(null, "seven332$163.com".replace('$', '@'))); - Toast.makeText(getActivity(), R.string.settings_about_donate_toast, Toast.LENGTH_SHORT).show(); - - new AlertDialog.Builder(getActivity()) - .setTitle(R.string.settings_about_donate) - .setMessage(getString(R.string.settings_about_donate_message).replace('$', '@')) - .show(); + showDonationDialog(); } else if (KEY_CHECK_FOR_UPDATES.equals(key)) { CommonOperations.checkUpdate(getActivity(), true); } return true; } + + private void showDonationDialog() { + AlertDialog dialog = new AlertDialog.Builder(getActivity()) + .setView(R.layout.dialog_donate) + .show(); + + String alipayStr = base64Decode("c2V2ZW4zMzJAMTYzLmNvbQ=="); + TextView alipayText = dialog.findViewById(R.id.alipay_text); + alipayText.setText(alipayStr); + dialog.findViewById(R.id.alipay_copy).setOnClickListener(v -> copyToClipboard(alipayStr)); + + String paypalStr = base64Decode("aHR0cHM6Ly9wYXlwYWwubWUvc2V2ZW4zMzI="); + TextView paypalText = dialog.findViewById(R.id.paypal_text); + paypalText.setText(paypalStr); + dialog.findViewById(R.id.paypal_open).setOnClickListener(v -> openUrl(paypalStr)); + dialog.findViewById(R.id.paypal_copy).setOnClickListener(v -> copyToClipboard(paypalStr)); + } + + private static String base64Decode(String encoded) { + byte[] bytes = Base64.decode(encoded, Base64.DEFAULT); + try { + return new String(bytes, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + + private void copyToClipboard(String text) { + ClipboardManager cmb = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); + if (cmb != null) { + cmb.setPrimaryClip(ClipData.newPlainText(null, text)); + Toast.makeText(getActivity(), R.string.settings_about_donate_copied, Toast.LENGTH_SHORT).show(); + } + } + + private void openUrl(String url) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + Intent chooser = Intent.createChooser(intent, ""); + startActivity(chooser); + } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/fragment/AdvancedFragment.java b/app/src/main/java/com/hippo/ehviewer/ui/fragment/AdvancedFragment.java index ebd490f26..323ef76ff 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/fragment/AdvancedFragment.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/fragment/AdvancedFragment.java @@ -17,33 +17,29 @@ package com.hippo.ehviewer.ui.fragment; import android.app.Activity; -import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.res.Resources; import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceFragment; -import android.text.TextUtils; import android.widget.Toast; - +import androidx.appcompat.app.AlertDialog; import com.hippo.ehviewer.AppConfig; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.EhDB; import com.hippo.ehviewer.R; -import com.hippo.ehviewer.Settings; import com.hippo.util.LogCat; import com.hippo.util.ReadableTime; - import java.io.File; import java.util.Arrays; -public class AdvancedFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener { +public class AdvancedFragment extends PreferenceFragment + implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener { private static final String KEY_DUMP_LOGCAT = "dump_logcat"; private static final String KEY_CLEAR_MEMORY_CACHE = "clear_memory_cache"; - private static final String KEY_PATTERN_PROTECTION = "pattern_protection"; - private static final String KEY_EXPORT_DATA = "export_data"; + private static final String KEY_APP_LANGUAGE = "app_language"; private static final String KEY_IMPORT_DATA = "import_data"; @Override @@ -53,23 +49,19 @@ public void onCreate(Bundle savedInstanceState) { Preference dumpLogcat = findPreference(KEY_DUMP_LOGCAT); Preference clearMemoryCache = findPreference(KEY_CLEAR_MEMORY_CACHE); - Preference exportData = findPreference(KEY_EXPORT_DATA); + Preference appLanguage = findPreference(KEY_APP_LANGUAGE); Preference importData = findPreference(KEY_IMPORT_DATA); dumpLogcat.setOnPreferenceClickListener(this); clearMemoryCache.setOnPreferenceClickListener(this); - exportData.setOnPreferenceClickListener(this); importData.setOnPreferenceClickListener(this); + + appLanguage.setOnPreferenceChangeListener(this); } @Override public void onResume() { super.onResume(); - - Preference patternProtection = findPreference(KEY_PATTERN_PROTECTION); - patternProtection.setSummary(TextUtils.isEmpty(Settings.getSecurity()) ? - R.string.settings_advanced_pattern_protection_not_set : - R.string.settings_advanced_pattern_protection_set); } @Override @@ -93,18 +85,6 @@ public boolean onPreferenceClick(Preference preference) { } else if (KEY_CLEAR_MEMORY_CACHE.equals(key)) { ((EhApplication) getActivity().getApplication()).clearMemoryCache(); Runtime.getRuntime().gc(); - } else if (KEY_EXPORT_DATA.equals(key)) { - File dir = AppConfig.getExternalDataDir(); - if (dir != null) { - File file = new File(dir, ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".db"); - if (EhDB.exportDB(getActivity(), file)) { - Toast.makeText(getActivity(), - getString(R.string.settings_advanced_export_data_to, file.getPath()), Toast.LENGTH_SHORT).show(); - return true; - } - } - Toast.makeText(getActivity(),R.string.settings_advanced_export_data_failed, Toast.LENGTH_SHORT).show(); - return true; } else if (KEY_IMPORT_DATA.equals(key)) { importData(getActivity()); getActivity().setResult(Activity.RESULT_OK); @@ -137,4 +117,14 @@ public void onClick(DialogInterface dialog, int which) { } }).show(); } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + String key = preference.getKey(); + if (KEY_APP_LANGUAGE.equals(key)) { + ((EhApplication) getActivity().getApplication()).recreate(); + return true; + } + return false; + } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/fragment/DownloadFragment.java b/app/src/main/java/com/hippo/ehviewer/ui/fragment/DownloadFragment.java index 244febabe..c2e511886 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/fragment/DownloadFragment.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/fragment/DownloadFragment.java @@ -17,7 +17,6 @@ package com.hippo.ehviewer.ui.fragment; import android.app.Activity; -import android.content.ActivityNotFoundException; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; @@ -25,15 +24,15 @@ import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceFragment; -import android.support.annotation.Nullable; -import android.support.v7.app.AlertDialog; import android.widget.Toast; - +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.ui.CommonOperations; import com.hippo.ehviewer.ui.DirPickerActivity; import com.hippo.unifile.UniFile; +import com.hippo.util.ExceptionUtils; public class DownloadFragment extends PreferenceFragment implements Preference.OnPreferenceChangeListener, @@ -145,7 +144,8 @@ private void openDirPickerL() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); try { startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE_DIR_L); - } catch (ActivityNotFoundException e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); Toast.makeText(getActivity(), R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show(); } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/fragment/EhFragment.java b/app/src/main/java/com/hippo/ehviewer/ui/fragment/EhFragment.java index 9beae252f..9f2831d61 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/fragment/EhFragment.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/fragment/EhFragment.java @@ -17,12 +17,14 @@ package com.hippo.ehviewer.ui.fragment; import android.app.Activity; +import android.os.Build; import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceFragment; - +import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; +import com.hippo.ehviewer.client.EhTagDatabase; public class EhFragment extends PreferenceFragment implements Preference.OnPreferenceChangeListener { @@ -32,21 +34,43 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.eh_settings); + Preference theme = findPreference(Settings.KEY_THEME); + Preference applyNavBarThemeColor = findPreference(Settings.KEY_APPLY_NAV_BAR_THEME_COLOR); Preference gallerySite = findPreference(Settings.KEY_GALLERY_SITE); Preference listMode = findPreference(Settings.KEY_LIST_MODE); Preference detailSize = findPreference(Settings.KEY_DETAIL_SIZE); Preference thumbSize = findPreference(Settings.KEY_THUMB_SIZE); + Preference showTagTranslations = findPreference(Settings.KEY_SHOW_TAG_TRANSLATIONS); + Preference tagTranslationsSource = findPreference("tag_translations_source"); + theme.setOnPreferenceChangeListener(this); + applyNavBarThemeColor.setOnPreferenceChangeListener(this); gallerySite.setOnPreferenceChangeListener(this); listMode.setOnPreferenceChangeListener(this); detailSize.setOnPreferenceChangeListener(this); thumbSize.setOnPreferenceChangeListener(this); + showTagTranslations.setOnPreferenceChangeListener(this); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + getPreferenceScreen().removePreference(applyNavBarThemeColor); + } + + if (!EhTagDatabase.isPossible(getActivity())) { + getPreferenceScreen().removePreference(showTagTranslations); + getPreferenceScreen().removePreference(tagTranslationsSource); + } } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { String key = preference.getKey(); - if (Settings.KEY_GALLERY_SITE.equals(key)) { + if (Settings.KEY_THEME.equals(key)) { + ((EhApplication) getActivity().getApplication()).recreate(); + return true; + } else if (Settings.KEY_APPLY_NAV_BAR_THEME_COLOR.equals(key)) { + ((EhApplication) getActivity().getApplication()).recreate(); + return true; + } else if (Settings.KEY_GALLERY_SITE.equals(key)) { getActivity().setResult(Activity.RESULT_OK); return true; } else if (Settings.KEY_LIST_MODE.equals(key)) { @@ -56,6 +80,10 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { getActivity().setResult(Activity.RESULT_OK); } else if (Settings.KEY_THUMB_SIZE.equals(key)) { getActivity().setResult(Activity.RESULT_OK); + } else if (Settings.KEY_SHOW_TAG_TRANSLATIONS.equals(key)) { + if (Boolean.TRUE.equals(newValue)) { + EhTagDatabase.update(getActivity()); + } } return true; } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/fragment/PrivacyFragment.java b/app/src/main/java/com/hippo/ehviewer/ui/fragment/PrivacyFragment.java new file mode 100644 index 000000000..6757946ad --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/fragment/PrivacyFragment.java @@ -0,0 +1,47 @@ +package com.hippo.ehviewer.ui.fragment; + +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.text.TextUtils; + +import com.hippo.ehviewer.Analytics; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.Settings; + +/** + * Created by Mo10 on 2018/2/10. + */ + +public class PrivacyFragment extends PreferenceFragment + implements Preference.OnPreferenceChangeListener { + private static final String KEY_PATTERN_PROTECTION = "pattern_protection"; + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.privacy_settings); + Preference enableAnalytics = findPreference(Settings.KEY_ENABLE_ANALYTICS); + + enableAnalytics.setOnPreferenceChangeListener(this); + } + @Override + public void onResume() { + super.onResume(); + Preference patternProtection = findPreference(KEY_PATTERN_PROTECTION); + patternProtection.setSummary(TextUtils.isEmpty(Settings.getSecurity()) ? + R.string.settings_privacy_pattern_protection_not_set : + R.string.settings_privacy_pattern_protection_set); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + String key = preference.getKey(); + if (Settings.KEY_ENABLE_ANALYTICS.equals(key)) { + if (newValue instanceof Boolean && (Boolean) newValue) { + Analytics.start(getActivity()); + } + return true; + } + return true; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/AnalyticsScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/AnalyticsScene.java index 183f32924..424add9eb 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/AnalyticsScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/AnalyticsScene.java @@ -18,12 +18,11 @@ import android.content.Context; import android.os.Bundle; -import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - +import androidx.annotation.Nullable; import com.hippo.ehviewer.Analytics; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/BaseScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/BaseScene.java index 4734f80ba..a9a4db218 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/BaseScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/BaseScene.java @@ -19,18 +19,21 @@ import android.content.Context; import android.content.res.Resources; import android.os.Bundle; -import android.support.annotation.IdRes; -import android.support.annotation.Nullable; -import android.support.annotation.StringRes; -import android.support.annotation.StyleRes; -import android.support.v4.app.FragmentActivity; -import android.support.v4.widget.DrawerLayout; +import android.os.Parcelable; +import android.util.SparseArray; import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.annotation.StyleRes; +import androidx.fragment.app.FragmentActivity; +import com.hippo.drawerlayout.DrawerLayout; +import com.hippo.ehviewer.Analytics; import com.hippo.ehviewer.ui.MainActivity; import com.hippo.scene.SceneFragment; import com.hippo.util.AppHelper; @@ -40,8 +43,16 @@ public abstract class BaseScene extends SceneFragment { public static final int LENGTH_SHORT = 0; public static final int LENGTH_LONG = 1; + public static final String KEY_DRAWER_VIEW_STATE = + "com.hippo.ehviewer.ui.scene.BaseScene:DRAWER_VIEW_STATE"; + private Context mThemeContext; + @Nullable + private View drawerView; + @Nullable + private SparseArray drawerViewState; + public void updateAvatar() { FragmentActivity activity = getActivity(); if (activity instanceof MainActivity) { @@ -100,6 +111,22 @@ public void toggleDrawer(int drawerGravity) { } } + public void setDrawerGestureBlocker(DrawerLayout.GestureBlocker gestureBlocker) { + FragmentActivity activity = getActivity(); + if (activity instanceof MainActivity) { + ((MainActivity) activity).setDrawerGestureBlocker(gestureBlocker); + } + } + + public boolean isDrawersVisible() { + FragmentActivity activity = getActivity(); + if (activity instanceof MainActivity) { + return ((MainActivity) activity).isDrawersVisible(); + } else { + return false; + } + } + /** * @param resId 0 for clear */ @@ -132,11 +159,39 @@ public int getNavCheckedItem() { return 0; } + public final View createDrawerView(LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + drawerView = onCreateDrawerView(inflater, container, savedInstanceState); + + if (drawerView != null) { + SparseArray saved = drawerViewState; + if (saved == null && savedInstanceState != null) { + saved = savedInstanceState.getSparseParcelableArray(KEY_DRAWER_VIEW_STATE); + } + if (saved != null) { + drawerView.restoreHierarchyState(saved); + } + } + + return drawerView; + } + public View onCreateDrawerView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return null; } + public final void destroyDrawerView() { + if (drawerView != null) { + drawerViewState = new SparseArray<>(); + drawerView.saveHierarchyState(drawerViewState); + } + + onDestroyDrawerView(); + + drawerView = null; + } + public void onDestroyDrawerView() { } @@ -222,4 +277,21 @@ public void showSoftInput(@Nullable View view) { AppHelper.showSoftInput(activity, view); } } + + @Override + public void onResume() { + super.onResume(); + Analytics.onSceneView(this); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + + if (drawerView != null) { + drawerViewState = new SparseArray<>(); + drawerView.saveHierarchyState(drawerViewState); + outState.putSparseParcelableArray(KEY_DRAWER_VIEW_STATE, drawerViewState); + } + } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/CookieSignInScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/CookieSignInScene.java index ab3f72d5c..cb627c0e9 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/CookieSignInScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/CookieSignInScene.java @@ -20,9 +20,6 @@ import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.design.widget.TextInputLayout; -import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -30,13 +27,16 @@ import android.view.ViewGroup; import android.widget.EditText; import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import com.google.android.material.textfield.TextInputLayout; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.R; import com.hippo.ehviewer.client.EhCookieStore; import com.hippo.ehviewer.client.EhUrl; import com.hippo.ehviewer.client.EhUtils; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.ViewUtils; -import junit.framework.Assert; import okhttp3.Cookie; public class CookieSignInScene extends SolidScene implements EditText.OnEditorActionListener, @@ -72,13 +72,13 @@ public View onCreateView2(LayoutInflater inflater, View view = inflater.inflate(R.layout.scene_cookie_sign_in, container, false); mIpbMemberIdLayout = (TextInputLayout) ViewUtils.$$(view, R.id.ipb_member_id_layout); mIpbMemberId = mIpbMemberIdLayout.getEditText(); - Assert.assertNotNull(mIpbMemberId); + AssertUtils.assertNotNull(mIpbMemberId); mIpbPassHashLayout = (TextInputLayout) ViewUtils.$$(view, R.id.ipb_pass_hash_layout); mIpbPassHash = mIpbPassHashLayout.getEditText(); - Assert.assertNotNull(mIpbPassHash); + AssertUtils.assertNotNull(mIpbPassHash); mIgneousLayout = (TextInputLayout) ViewUtils.$$(view, R.id.igneous_layout); mIgneous = mIgneousLayout.getEditText(); - Assert.assertNotNull(mIgneous); + AssertUtils.assertNotNull(mIgneous); mOk = ViewUtils.$$(view, R.id.ok); mIpbPassHash.setOnEditorActionListener(this); @@ -87,7 +87,7 @@ public View onCreateView2(LayoutInflater inflater, // Try to get old version cookie info Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); SharedPreferences sharedPreferences = context.getSharedPreferences("eh_info", 0); String ipbMemberId = sharedPreferences.getString("ipb_member_id", null); String ipbPassHash = sharedPreferences.getString("ipb_pass_hash", null); @@ -196,12 +196,6 @@ public void enter() { } else { mIpbPassHashLayout.setError(null); } - if (TextUtils.isEmpty(igneous)) { - mIgneousLayout.setError(getString(R.string.text_is_empty)); - return; - } else { - mIgneousLayout.setError(null); - } hideSoftInput(); @@ -242,7 +236,9 @@ private void storeCookie(String id, String hash, String igneous) { store.addCookie(newCookie(EhCookieStore.KEY_IPD_MEMBER_ID, id, EhUrl.DOMAIN_EX)); store.addCookie(newCookie(EhCookieStore.KEY_IPD_PASS_HASH, hash, EhUrl.DOMAIN_E)); store.addCookie(newCookie(EhCookieStore.KEY_IPD_PASS_HASH, hash, EhUrl.DOMAIN_EX)); - store.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_E)); - store.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_EX)); + if (!igneous.isEmpty()) { + store.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_E)); + store.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_EX)); + } } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/CrashScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/CrashScene.java deleted file mode 100644 index ddaae1bde..000000000 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/CrashScene.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.ehviewer.ui.scene; - -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.hippo.ehviewer.Crash; -import com.hippo.ehviewer.EhApplication; -import com.hippo.ehviewer.R; -import com.hippo.ehviewer.ui.MainActivity; -import com.hippo.ripple.Ripple; -import com.hippo.util.AppHelper; -import com.hippo.yorozuya.ViewUtils; - -public class CrashScene extends SolidScene implements View.OnClickListener { - - /*--------------- - Whole life cycle - ---------------*/ - @Nullable - private String mCrash; - - /*--------------- - View life cycle - ---------------*/ - @Nullable - private View mCancel; - @Nullable - private View mSend; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mCrash = Crash.getCrashContent(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - mCrash = null; - } - - @Nullable - @Override - public View onCreateView2(LayoutInflater inflater, - @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.scene_crash, container, false); - - mCancel = ViewUtils.$$(view, R.id.cancel); - mSend = ViewUtils.$$(view, R.id.send); - - mCancel.setOnClickListener(this); - mSend.setOnClickListener(this); - - Ripple.addRipple(mCancel, true); - Ripple.addRipple(mSend, true); - - return view; - } - - @Override - public void onClick(View v) { - MainActivity activity = getActivity2(); - - if (mSend == v && null != mCrash && null != activity) { - AppHelper.sendEmail(activity, EhApplication.getDeveloperEmail(), - "I found a bug in EhViewer !", mCrash); - } - Crash.resetCrashFile(); - - // Start new scene and finish it self - if (null != activity) { - startSceneForCheckStep(CHECK_STEP_CRASH, getArguments()); - } - finish(); - } -} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/DownloadLabelsScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/DownloadLabelsScene.java index d2fd32abe..436f0f5fc 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/DownloadLabelsScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/DownloadLabelsScene.java @@ -21,10 +21,6 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.NinePatchDrawable; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.MenuItem; @@ -32,20 +28,16 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; - +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator; import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator; import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemAdapter; import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange; import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultAction; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionDefault; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionMoveToSwipedDirection; -import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager; -import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractDraggableSwipeableItemViewHolder; +import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractDraggableItemViewHolder; import com.hippo.app.EditTextDialogBuilder; import com.hippo.easyrecyclerview.EasyRecyclerView; import com.hippo.ehviewer.EhApplication; @@ -53,10 +45,8 @@ import com.hippo.ehviewer.dao.DownloadLabel; import com.hippo.util.DrawableManager; import com.hippo.view.ViewTransition; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.ViewUtils; - -import junit.framework.Assert; - import java.util.List; public class DownloadLabelsScene extends ToolbarScene { @@ -101,35 +91,27 @@ public View onCreateView3(LayoutInflater inflater, mViewTransition = new ViewTransition(mRecyclerView, tip); Context context = getContext2(); - Assert.assertNotNull(context); - Drawable drawable = DrawableManager.getDrawable(context, R.drawable.big_label); + AssertUtils.assertNotNull(context); + Drawable drawable = DrawableManager.getVectorDrawable(context, R.drawable.big_label); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); tip.setCompoundDrawables(null, drawable, null, null); tip.setText(R.string.no_download_label); - // touch guard manager (this class is required to suppress scrolling while swipe-dismiss animation is running) - RecyclerViewTouchActionGuardManager guardManager = new RecyclerViewTouchActionGuardManager(); - guardManager.setInterceptVerticalScrollingWhileAnimationRunning(true); - guardManager.setEnabled(true); // drag & drop manager RecyclerViewDragDropManager dragDropManager = new RecyclerViewDragDropManager(); dragDropManager.setDraggingItemShadowDrawable( (NinePatchDrawable) context.getResources().getDrawable(R.drawable.shadow_8dp)); - // swipe manager - RecyclerViewSwipeManager swipeManager = new RecyclerViewSwipeManager(); + RecyclerView.Adapter adapter = new LabelAdapter(); adapter.setHasStableIds(true); adapter = dragDropManager.createWrappedAdapter(adapter); // wrap for dragging - adapter = swipeManager.createWrappedAdapter(adapter); // wrap for swiping mAdapter = adapter; final GeneralItemAnimator animator = new SwipeDismissItemAnimator(); - animator.setSupportsChangeAnimations(false); - mRecyclerView.hasFixedSize(); + mRecyclerView.setLayoutManager(new LinearLayoutManager(context)); mRecyclerView.setAdapter(adapter); mRecyclerView.setItemAnimator(animator); - guardManager.attachRecyclerView(mRecyclerView); - swipeManager.attachRecyclerView(mRecyclerView); + dragDropManager.attachRecyclerView(mRecyclerView); updateView(); @@ -287,66 +269,77 @@ public void onClick(View v) { } } - private class LabelHolder extends AbstractDraggableSwipeableItemViewHolder + private class LabelHolder extends AbstractDraggableItemViewHolder implements View.OnClickListener { - public final View swipeHandler; public final TextView label; public final View dragHandler; + public final View delete; public LabelHolder(View itemView) { super(itemView); - swipeHandler = ViewUtils.$$(itemView, R.id.swipe_handler); label = (TextView) ViewUtils.$$(itemView, R.id.label); dragHandler = ViewUtils.$$(itemView, R.id.drag_handler); + delete = ViewUtils.$$(itemView, R.id.delete); label.setOnClickListener(this); + delete.setOnClickListener(this); } @Override public void onClick(View v) { + int position = getAdapterPosition(); Context context = getContext2(); if (null == context || null == mList || null == mRecyclerView) { return; } - int index = mRecyclerView.getChildAdapterPosition(itemView); - if (index < 0 || index >= mList.size()) { - return; - } - if (label == v) { - DownloadLabel raw = mList.get(index); + DownloadLabel raw = mList.get(position); EditTextDialogBuilder builder = new EditTextDialogBuilder( context, raw.getLabel(), getString(R.string.download_labels)); builder.setTitle(R.string.rename_label_title); builder.setPositiveButton(android.R.string.ok, null); AlertDialog dialog = builder.show(); - new RenameLabelDialogHelper(builder, dialog, raw.getLabel(), index); + new RenameLabelDialogHelper(builder, dialog, raw.getLabel(), position); + } else if (delete == v) { + final DownloadLabel label = mList.get(position); + new AlertDialog.Builder(context) + .setTitle(R.string.delete_label_title) + .setMessage(getString(R.string.delete_label_message, label.getLabel())) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + EhApplication.getDownloadManager(context).deleteLabel(label.getLabel()); + } + }) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + if (null != mAdapter) { + mAdapter.notifyDataSetChanged(); + } + updateView(); + } + }).show(); } } - - @Override - public View getSwipeableContainerView() { - return swipeHandler; - } } private class LabelAdapter extends RecyclerView.Adapter - implements DraggableItemAdapter, - SwipeableItemAdapter { + implements DraggableItemAdapter { private final LayoutInflater mInflater; public LabelAdapter() { mInflater = getLayoutInflater2(); - Assert.assertNotNull(mInflater); + AssertUtils.assertNotNull(mInflater); } @Override public LabelHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new LabelHolder(mInflater.inflate(R.layout.item_label_list, parent, false)); + return new LabelHolder(mInflater.inflate(R.layout.item_download_label, parent, false)); } @Override @@ -354,7 +347,6 @@ public void onBindViewHolder(LabelHolder holder, int position) { if (mList != null) { holder.label.setText(mList.get(position).getLabel()); } - holder.setSwipeItemHorizontalSlideAmount(0); } @Override @@ -385,9 +377,6 @@ public void onMoveItem(int fromPosition, int toPosition) { } EhApplication.getDownloadManager(context).moveLabel(fromPosition, toPosition); - if (mAdapter != null && mList != null) { - notifyItemMoved(fromPosition, toPosition); - } } @Override @@ -396,69 +385,9 @@ public boolean onCheckCanDrop(int draggingPosition, int dropPosition) { } @Override - public int onGetSwipeReactionType(LabelHolder holder, int position, int x, int y) { - if (ViewUtils.isViewUnder(holder.getSwipeableContainerView(), x, y, 0)) { - return SwipeableItemConstants.REACTION_CAN_SWIPE_LEFT; - } else { - return SwipeableItemConstants.REACTION_CAN_NOT_SWIPE_BOTH_H; - } - } - - @Override - public void onSetSwipeBackground(LabelHolder holder, int position, int type) {} + public void onItemDragStarted(int position) { } @Override - public SwipeResultAction onSwipeItem(LabelHolder holder, int position, int result) { - switch (result) { - // swipe left --- pin - case SwipeableItemConstants.RESULT_SWIPED_LEFT: - return new SwipeLeftResultAction(position); - // other --- do nothing - case SwipeableItemConstants.RESULT_SWIPED_RIGHT: - case SwipeableItemConstants.RESULT_CANCELED: - default: - return new SwipeResultActionDefault(); - } - } - } - - private class SwipeLeftResultAction extends SwipeResultActionMoveToSwipedDirection { - - private final int mPosition; - - public SwipeLeftResultAction(int position) { - mPosition = position; - } - - @Override - protected void onPerformAction() { - super.onPerformAction(); - - final Context context = getContext2(); - final List list = mList; - if (null == context || null == list || mPosition < 0 || mPosition >= list.size()) { - return; - } - final DownloadLabel label = list.get(mPosition); - - new AlertDialog.Builder(context) - .setTitle(R.string.delete_label_title) - .setMessage(getString(R.string.delete_label_message, label.getLabel())) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - EhApplication.getDownloadManager(context).deleteLabel(label.getLabel()); - } - }) - .setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialog) { - if (null != mAdapter) { - mAdapter.notifyDataSetChanged(); - } - updateView(); - } - }).show(); - } + public void onItemDragFinished(int fromPosition, int toPosition, boolean result) { } } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/DownloadsScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/DownloadsScene.java index 4b070e4c1..35ab58070 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/DownloadsScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/DownloadsScene.java @@ -22,20 +22,13 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.res.Resources; +import android.graphics.Color; import android.graphics.Point; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.FloatingActionButton; -import android.support.v4.widget.DrawerLayout; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.SimpleItemAnimator; -import android.support.v7.widget.StaggeredGridLayoutManager; -import android.support.v7.widget.Toolbar; import android.util.Log; import android.util.SparseBooleanArray; import android.view.Display; @@ -50,14 +43,24 @@ import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.Toolbar; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SimpleItemAnimator; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; import com.github.amlcurran.showcaseview.ShowcaseView; import com.github.amlcurran.showcaseview.SimpleShowcaseEventListener; import com.github.amlcurran.showcaseview.targets.PointTarget; import com.github.amlcurran.showcaseview.targets.ViewTarget; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.hippo.android.resource.AttrResources; import com.hippo.app.CheckBoxDialogBuilder; import com.hippo.conaco.DataContainer; import com.hippo.conaco.ProgressNotifier; +import com.hippo.drawerlayout.DrawerLayout; import com.hippo.easyrecyclerview.EasyRecyclerView; import com.hippo.easyrecyclerview.FastScroller; import com.hippo.easyrecyclerview.HandlerDrawable; @@ -82,21 +85,18 @@ import com.hippo.scene.Announcer; import com.hippo.streampipe.InputStreamPipe; import com.hippo.unifile.UniFile; -import com.hippo.util.ApiHelper; import com.hippo.util.DrawableManager; +import com.hippo.util.IoThreadPoolExecutor; import com.hippo.view.ViewTransition; import com.hippo.widget.FabLayout; import com.hippo.widget.LoadImageView; import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.FileUtils; import com.hippo.yorozuya.IOUtils; import com.hippo.yorozuya.ObjectUtils; -import com.hippo.yorozuya.ResourcesUtils; import com.hippo.yorozuya.ViewUtils; import com.hippo.yorozuya.collect.LongList; - -import junit.framework.Assert; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -194,7 +194,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); mDownloadManager = EhApplication.getDownloadManager(context); mDownloadManager.addDownloadInfoListener(this); @@ -287,10 +287,10 @@ public View onCreateView3(LayoutInflater inflater, mViewTransition = new ViewTransition(content, tip); Context context = getContext2(); - Assert.assertNotNull(content); + AssertUtils.assertNotNull(content); Resources resources = context.getResources(); - Drawable drawable = DrawableManager.getDrawable(context, R.drawable.big_download); + Drawable drawable = DrawableManager.getVectorDrawable(context, R.drawable.big_download); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); tip.setCompoundDrawables(null, drawable, null, null); @@ -301,9 +301,8 @@ public View onCreateView3(LayoutInflater inflater, mLayoutManager.setColumnSize(resources.getDimensionPixelOffset(Settings.getDetailSizeResId())); mLayoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_MIN_SIZE); mRecyclerView.setLayoutManager(mLayoutManager); - mRecyclerView.setSelector(Ripple.generateRippleDrawable(context, false)); + mRecyclerView.setSelector(Ripple.generateRippleDrawable(context, !AttrResources.getAttrBoolean(context, R.attr.isLightTheme), new ColorDrawable(Color.TRANSPARENT))); mRecyclerView.setDrawSelectorOnTop(true); - mRecyclerView.hasFixedSize(); mRecyclerView.setClipToPadding(false); mRecyclerView.setOnItemClickListener(this); mRecyclerView.setOnItemLongClickListener(this); @@ -327,7 +326,7 @@ public View onCreateView3(LayoutInflater inflater, fastScroller.attachToRecyclerView(mRecyclerView); HandlerDrawable handlerDrawable = new HandlerDrawable(); - handlerDrawable.setColor(ResourcesUtils.getAttrColor(context, R.attr.colorAccent)); + handlerDrawable.setColor(AttrResources.getAttrColor(context, R.attr.widgetColorThemeAccent)); fastScroller.setHandlerDrawable(handlerDrawable); fastScroller.setOnDragHandlerListener(this); @@ -489,6 +488,17 @@ public boolean onMenuItemClick(MenuItem item) { } return true; } + case R.id.action_reset_reading_progress: { + new AlertDialog.Builder(getContext()) + .setMessage(R.string.reset_reading_progress_message) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + if (mDownloadManager != null) { + mDownloadManager.resetAllReadingProgress(); + } + }).show(); + return true; + } } return false; } @@ -509,7 +519,7 @@ public View onCreateDrawerView(LayoutInflater inflater, View view = inflater.inflate(R.layout.drawer_list, container, false); final Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); Toolbar toolbar = (Toolbar) view.findViewById(R.id.toolbar); toolbar.setTitle(R.string.download_labels); @@ -635,7 +645,7 @@ public boolean onItemClick(EasyRecyclerView parent, View view, int position, lon if (list == null) { return false; } - if (position < 0 && position >= list.size()) { + if (position < 0 || position >= list.size()) { return false; } @@ -921,11 +931,13 @@ private static void deleteFileAsync(UniFile... files) { @Override protected Void doInBackground(UniFile... params) { for (UniFile file: params) { - file.delete(); + if (file != null) { + file.delete(); + } } return null; } - }.execute(files); + }.executeOnExecutor(IoThreadPoolExecutor.getInstance(), files); } private class DeleteDialogHelper implements DialogInterface.OnClickListener { @@ -1074,8 +1086,10 @@ public DownloadHolder(View itemView) { thumb.setOnClickListener(this); start.setOnClickListener(this); stop.setOnClickListener(this); - Ripple.addRipple(start, false); - Ripple.addRipple(stop, false); + + boolean isDarkTheme = !AttrResources.getAttrBoolean(getContext2(), R.attr.isLightTheme); + Ripple.addRipple(start, isDarkTheme); + Ripple.addRipple(stop, isDarkTheme); } @Override @@ -1092,7 +1106,7 @@ public void onClick(View v) { } int size = list.size(); int index = recyclerView.getChildAdapterPosition(itemView); - if (index < 0 && index >= size) { + if (index < 0 || index >= size) { return; } @@ -1101,9 +1115,7 @@ public void onClick(View v) { args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO); args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, list.get(index)); Announcer announcer = new Announcer(GalleryDetailScene.class).setArgs(args); - if (ApiHelper.SUPPORT_TRANSITION) { - announcer.setTranHelper(new EnterGalleryDetailTransaction(thumb)); - } + announcer.setTranHelper(new EnterGalleryDetailTransaction(thumb)); startScene(announcer); } else if (start == v) { Intent intent = new Intent(activity, DownloadService.class); @@ -1121,10 +1133,17 @@ public void onClick(View v) { private class DownloadAdapter extends RecyclerView.Adapter { private final LayoutInflater mInflater; + private final int mListThumbWidth; + private final int mListThumbHeight; public DownloadAdapter() { mInflater = getLayoutInflater2(); - Assert.assertNotNull(mInflater); + AssertUtils.assertNotNull(mInflater); + + View calculator = mInflater.inflate(R.layout.item_gallery_list_thumb_height, null); + ViewUtils.measureView(calculator, 1024, ViewGroup.LayoutParams.WRAP_CONTENT); + mListThumbHeight = calculator.getMeasuredHeight(); + mListThumbWidth = mListThumbHeight * 2 / 3; } @Override @@ -1137,7 +1156,14 @@ public long getItemId(int position) { @Override public DownloadHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new DownloadHolder(mInflater.inflate(R.layout.item_download, parent, false)); + DownloadHolder holder = new DownloadHolder(mInflater.inflate(R.layout.item_download, parent, false)); + + ViewGroup.LayoutParams lp = holder.thumb.getLayoutParams(); + lp.width = mListThumbWidth; + lp.height = mListThumbHeight; + holder.thumb.setLayoutParams(lp); + + return holder; } @Override @@ -1161,8 +1187,7 @@ public void onBindViewHolder(DownloadHolder holder, int position) { // Update transition name if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - long gid = info.gid; - holder.thumb.setTransitionName(TransitionNameFactory.getThumbTransitionName(gid)); + ViewCompat.setTransitionName(holder.thumb, TransitionNameFactory.getThumbTransitionName(info.gid)); } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/EhCallback.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/EhCallback.java index 5f7fd140a..5bc1eacc7 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/EhCallback.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/EhCallback.java @@ -17,9 +17,8 @@ package com.hippo.ehviewer.ui.scene; import android.content.Context; -import android.support.annotation.StringRes; import android.widget.Toast; - +import androidx.annotation.StringRes; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.client.EhClient; import com.hippo.ehviewer.ui.MainActivity; @@ -40,6 +39,14 @@ public EhCallback(Context context, int stageId, String sceneTag) { public abstract boolean isInstance(SceneFragment scene); + public Context getContent() { + Context context = getStageActivity(); + if (context == null) { + context = getApplication(); + } + return context; + } + public EhApplication getApplication() { return mApplication; } @@ -71,4 +78,14 @@ public void showTip(@StringRes int id, int length) { length == BaseScene.LENGTH_LONG ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show(); } } + + public void showTip(String tip, int length) { + StageActivity activity = getStageActivity(); + if (activity instanceof MainActivity) { + ((MainActivity) activity).showTip(tip, length); + } else { + Toast.makeText(getApplication(), tip, + length == BaseScene.LENGTH_LONG ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show(); + } + } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/EnterGalleryDetailTransaction.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/EnterGalleryDetailTransaction.java index 49a6432e7..9f79e84eb 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/EnterGalleryDetailTransaction.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/EnterGalleryDetailTransaction.java @@ -17,12 +17,11 @@ package com.hippo.ehviewer.ui.scene; import android.content.Context; -import android.os.Build; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentTransaction; -import android.transition.TransitionInflater; import android.view.View; - +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; +import androidx.transition.TransitionInflater; import com.hippo.ehviewer.R; import com.hippo.scene.TransitionHelper; @@ -41,16 +40,17 @@ public boolean onTransition(Context context, FragmentTransaction transaction, return false; } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + String transitionName = ViewCompat.getTransitionName(mThumb); + if (transitionName != null) { exit.setSharedElementReturnTransition( TransitionInflater.from(context).inflateTransition(R.transition.trans_move)); exit.setExitTransition( - TransitionInflater.from(context).inflateTransition(android.R.transition.fade)); + TransitionInflater.from(context).inflateTransition(R.transition.trans_fade)); enter.setSharedElementEnterTransition( TransitionInflater.from(context).inflateTransition(R.transition.trans_move)); enter.setEnterTransition( - TransitionInflater.from(context).inflateTransition(android.R.transition.fade)); - transaction.addSharedElement(mThumb, mThumb.getTransitionName()); + TransitionInflater.from(context).inflateTransition(R.transition.trans_fade)); + transaction.addSharedElement(mThumb, transitionName); } return true; } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.java index dd20f0094..bf8a4c953 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.java @@ -21,17 +21,12 @@ import android.content.Context; import android.content.DialogInterface; import android.content.res.Resources; +import android.graphics.Color; import android.graphics.Point; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.FloatingActionButton; -import android.support.v4.widget.DrawerLayout; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.Toolbar; +import android.text.InputType; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextUtils; @@ -43,19 +38,26 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.ListView; import android.widget.TextView; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.Toolbar; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.github.amlcurran.showcaseview.ShowcaseView; import com.github.amlcurran.showcaseview.SimpleShowcaseEventListener; import com.github.amlcurran.showcaseview.targets.PointTarget; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.hippo.android.resource.AttrResources; import com.hippo.annotation.Implemented; +import com.hippo.app.EditTextDialogBuilder; +import com.hippo.drawable.AddDeleteDrawable; import com.hippo.drawable.DrawerArrowDrawable; import com.hippo.easyrecyclerview.EasyRecyclerView; import com.hippo.easyrecyclerview.FastScroller; -import com.hippo.easyrecyclerview.LinearDividerItemDecoration; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.EhDB; import com.hippo.ehviewer.R; @@ -66,28 +68,27 @@ import com.hippo.ehviewer.client.data.FavListUrlBuilder; import com.hippo.ehviewer.client.data.GalleryInfo; import com.hippo.ehviewer.client.parser.FavoritesParser; +import com.hippo.ehviewer.ui.CommonOperations; import com.hippo.ehviewer.ui.MainActivity; import com.hippo.ehviewer.ui.annotation.DrawerLifeCircle; import com.hippo.ehviewer.ui.annotation.ViewLifeCircle; import com.hippo.ehviewer.ui.annotation.WholeLifeCircle; +import com.hippo.ehviewer.widget.EhDrawerLayout; +import com.hippo.ehviewer.widget.GalleryInfoContentHelper; import com.hippo.ehviewer.widget.SearchBar; import com.hippo.refreshlayout.RefreshLayout; import com.hippo.ripple.Ripple; import com.hippo.scene.Announcer; import com.hippo.scene.SceneFragment; -import com.hippo.util.ApiHelper; +import com.hippo.util.AppHelper; import com.hippo.util.DrawableManager; import com.hippo.widget.ContentLayout; import com.hippo.widget.FabLayout; import com.hippo.widget.SearchBarMover; -import com.hippo.yorozuya.LayoutUtils; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.ObjectUtils; -import com.hippo.yorozuya.ResourcesUtils; import com.hippo.yorozuya.SimpleHandler; import com.hippo.yorozuya.ViewUtils; - -import junit.framework.Assert; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -96,7 +97,8 @@ public class FavoritesScene extends BaseScene implements EasyRecyclerView.OnItemClickListener, EasyRecyclerView.OnItemLongClickListener, FastScroller.OnDragHandlerListener, SearchBarMover.Helper, SearchBar.Helper, - FabLayout.OnClickFabListener, EasyRecyclerView.CustomChoiceListener, AdapterView.OnItemClickListener { + FabLayout.OnClickFabListener, FabLayout.OnExpandListener, + EasyRecyclerView.CustomChoiceListener { private static final long ANIMATE_TIME = 300L; @@ -127,14 +129,14 @@ public class FavoritesScene extends BaseScene implements @Nullable @ViewLifeCircle private DrawerArrowDrawable mLeftDrawable; + private AddDeleteDrawable mActionFabDrawable; @Nullable - @DrawerLifeCircle - private ArrayAdapter mDrawerAdapter; + private EhDrawerLayout mDrawerLayout; + @Nullable @DrawerLifeCircle - private List mDrawerList; - + private FavDrawerAdapter mDrawerAdapter; @Nullable @WholeLifeCircle private EhClient mClient; @@ -143,13 +145,16 @@ public class FavoritesScene extends BaseScene implements private String[] mFavCatArray; @Nullable @WholeLifeCircle + private int[] mFavCountArray; + @Nullable + @WholeLifeCircle private FavListUrlBuilder mUrlBuilder; public int current; // -1 for error public int limit; // -1 for error - @Nullable - private int[] mFavCountArray; + private int mFavLocalCount = 0; + private int mFavCountSum = 0; private boolean mHasFirstRefresh; private boolean mSearchMode; @@ -178,9 +183,12 @@ public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); mClient = EhApplication.getEhClient(context); mFavCatArray = Settings.getFavCat(); + mFavCountArray = Settings.getFavCount(); + mFavLocalCount = Settings.getFavLocalCount(); + mFavCountSum = Settings.getFavCloudCount(); if (savedInstanceState == null) { onInit(); @@ -206,7 +214,7 @@ private void onRestore(Bundle savedInstanceState) { } @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); boolean hasFirstRefresh; @@ -227,6 +235,8 @@ public void onDestroy() { mClient = null; mFavCatArray = null; + mFavCountArray = null; + mFavCountSum = 0; mUrlBuilder = null; } @@ -236,6 +246,9 @@ public View onCreateView2(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.scene_favorites, container, false); ContentLayout contentLayout = (ContentLayout) view.findViewById(R.id.content_layout); + MainActivity activity = getActivity2(); + AssertUtils.assertNotNull(activity); + mDrawerLayout = (EhDrawerLayout) ViewUtils.$$(activity, R.id.draw_view); mRecyclerView = contentLayout.getRecyclerView(); FastScroller fastScroller = contentLayout.getFastScroller(); RefreshLayout refreshLayout = contentLayout.getRefreshLayout(); @@ -243,7 +256,7 @@ public View onCreateView2(LayoutInflater inflater, mFabLayout = (FabLayout) ViewUtils.$$(view, R.id.fab_layout); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); Resources resources = context.getResources(); int paddingTopSB = resources.getDimensionPixelOffset(R.dimen.gallery_padding_top_search_bar); @@ -253,9 +266,8 @@ public View onCreateView2(LayoutInflater inflater, contentLayout.getFastScroller().setOnDragHandlerListener(this); mAdapter = new FavoritesAdapter(inflater, resources, mRecyclerView, Settings.getListMode()); - mRecyclerView.setSelector(Ripple.generateRippleDrawable(context, false)); + mRecyclerView.setSelector(Ripple.generateRippleDrawable(context, !AttrResources.getAttrBoolean(context, R.attr.isLightTheme), new ColorDrawable(Color.TRANSPARENT))); mRecyclerView.setDrawSelectorOnTop(true); - mRecyclerView.hasFixedSize(); mRecyclerView.setClipToPadding(false); mRecyclerView.setOnItemClickListener(this); mRecyclerView.setOnItemLongClickListener(this); @@ -267,18 +279,21 @@ public View onCreateView2(LayoutInflater inflater, refreshLayout.setHeaderTranslationY(paddingTopSB); - mLeftDrawable = new DrawerArrowDrawable(context); + mLeftDrawable = new DrawerArrowDrawable(context, AttrResources.getAttrColor(context, R.attr.drawableColorPrimary)); mSearchBar.setLeftDrawable(mLeftDrawable); - mSearchBar.setRightDrawable(DrawableManager.getDrawable(context, R.drawable.v_magnify_x24)); + mSearchBar.setRightDrawable(DrawableManager.getVectorDrawable(context, R.drawable.v_magnify_x24)); mSearchBar.setHelper(this); mSearchBar.setAllowEmptySearch(false); updateSearchBar(); mSearchBarMover = new SearchBarMover(this, mSearchBar, mRecyclerView); + mActionFabDrawable = new AddDeleteDrawable(context, resources.getColor(R.color.primary_drawable_dark)); + mFabLayout.getPrimaryFab().setImageDrawable(mActionFabDrawable); mFabLayout.setExpanded(false, false); - mFabLayout.setAutoCancel(false); - mFabLayout.setHidePrimaryFab(true); + mFabLayout.setAutoCancel(true); + mFabLayout.setHidePrimaryFab(false); mFabLayout.setOnClickFabListener(this); + mFabLayout.setOnExpandListener(this); addAboveSnackView(mFabLayout); // Restore search mode @@ -358,7 +373,7 @@ private void updateSearchBar() { // Update hint if (!ObjectUtils.equal(favCatName, mOldFavCat)) { - Drawable searchImage = DrawableManager.getDrawable(context, R.drawable.v_magnify_x24); + Drawable searchImage = DrawableManager.getVectorDrawable(context, R.drawable.v_magnify_x24); SpannableStringBuilder ssb = new SpannableStringBuilder(" "); ssb.append(getString(R.string.favorites_search_bar_hint, favCatName)); int textSize = (int) (mSearchBar.getEditTextTextSize() * 1.25); @@ -386,6 +401,7 @@ public void onDestroyView() { } if (null != mHelper) { + mHelper.destroy(); if (1 == mHelper.getShownViewIndex()) { mHasFirstRefresh = false; } @@ -410,96 +426,77 @@ public void onDestroyView() { mOldKeyword = null; } - private class InfoHolder extends RecyclerView.ViewHolder { + private class FavDrawerHolder extends RecyclerView.ViewHolder { private final TextView key; private final TextView value; - public InfoHolder(View itemView) { + private FavDrawerHolder(View itemView) { super(itemView); key = (TextView) ViewUtils.$$(itemView, R.id.key); value = (TextView) ViewUtils.$$(itemView, R.id.value); } } - private class InfoAdapter extends RecyclerView.Adapter { - - private static final int TYPE_HEADER = 0; - private static final int TYPE_DATA = 1; + private class FavDrawerAdapter extends RecyclerView.Adapter { private final LayoutInflater mInflater; - public InfoAdapter(LayoutInflater inflater) { + private FavDrawerAdapter(LayoutInflater inflater) { mInflater = inflater; } @Override public int getItemViewType(int position) { - return 0 == position ? TYPE_HEADER : TYPE_DATA; + return position; } + @NonNull @Override - public InfoHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new InfoHolder(mInflater.inflate(TYPE_HEADER == viewType ? - R.layout.item_favorite_info_header : R.layout.item_favorite_info_data, parent, false)); + public FavDrawerHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new FavDrawerHolder(mInflater.inflate(R.layout.item_drawer_favorites, parent, false)); } @Override @SuppressLint("SetTextI18n") - public void onBindViewHolder(InfoHolder holder, int position) { + public void onBindViewHolder(@NonNull FavDrawerHolder holder, int position) { if (0 == position) { - holder.key.setText(R.string.collections); - holder.value.setText(R.string.count); - holder.itemView.setEnabled(false); + holder.key.setText(R.string.local_favorites); + holder.value.setText(Integer.toString(mFavLocalCount)); + holder.itemView.setEnabled(true); + } else if (1 == position){ + holder.key.setText(R.string.cloud_favorites); + holder.value.setText(Integer.toString(mFavCountSum)); + holder.itemView.setEnabled(true); } else { if (null == mFavCatArray || null == mFavCountArray || - mFavCatArray.length < position || mFavCountArray.length < position) { + mFavCatArray.length < (position - 1) || + mFavCountArray.length < (position - 1)) { return; } - holder.key.setText(mFavCatArray[position - 1]); - holder.value.setText(Integer.toString(mFavCountArray[position - 1])); + holder.key.setText(mFavCatArray[position - 2]); + holder.value.setText(Integer.toString(mFavCountArray[position - 2])); holder.itemView.setEnabled(true); } } @Override public int getItemCount() { - return 11; - } - } - - @SuppressLint("InflateParams") - private void showFavoritesInfoDialog() { - Context context = getContext2(); - if (null == context || null == mFavCatArray || null == mFavCountArray) { - return; + if (null == mFavCatArray) { + return 2; + } + return 12; } - - AlertDialog.Builder builder = new AlertDialog.Builder(context); - context = builder.getContext(); - final LayoutInflater inflater = LayoutInflater.from(context); - EasyRecyclerView rv = (EasyRecyclerView) inflater.inflate(R.layout.dialog_recycler_view, null); - rv.setAdapter(new InfoAdapter(inflater)); - rv.setLayoutManager(new LinearLayoutManager(context)); - LinearDividerItemDecoration decoration = new LinearDividerItemDecoration( - LinearDividerItemDecoration.VERTICAL, context.getResources().getColor(R.color.divider), - LayoutUtils.dp2pix(context, 1)); - decoration.setPadding(ResourcesUtils.getAttrDimensionPixelOffset(context, R.attr.dialogPreferredPadding)); - rv.addItemDecoration(decoration); - rv.setSelector(Ripple.generateRippleDrawable(context, false)); - rv.setClipToPadding(false); - builder.setView(rv).show(); } @Override public View onCreateDrawerView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.drawer_list, container, false); + View view = inflater.inflate(R.layout.drawer_list_rv, container, false); + final Context context = getContext2(); Toolbar toolbar = (Toolbar) ViewUtils.$$(view, R.id.toolbar); - ListView listView = (ListView) view.findViewById(R.id.list_view); - final Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); toolbar.setTitle(R.string.collections); toolbar.inflateMenu(R.menu.drawer_favorites); @@ -508,9 +505,6 @@ public View onCreateDrawerView(LayoutInflater inflater, public boolean onMenuItemClick(MenuItem item) { int id = item.getItemId(); switch (id) { - case R.id.action_info: - showFavoritesInfoDialog(); - return true; case R.id.action_default_favorites_slot: String[] items = new String[12]; items[0] = getString(R.string.let_me_select); @@ -531,15 +525,13 @@ public void onClick(DialogInterface dialog, int which) { } }); - mDrawerList = new ArrayList<>(12); - mDrawerList.add(getString(R.string.local_favorites)); - mDrawerList.add(getString(R.string.cloud_favorites)); - if (mFavCatArray != null) { - Collections.addAll(mDrawerList, mFavCatArray); - } - mDrawerAdapter = new ArrayAdapter<>(context, R.layout.item_simple_list, mDrawerList); - listView.setAdapter(mDrawerAdapter); - listView.setOnItemClickListener(this); + EasyRecyclerView recyclerView = (EasyRecyclerView) view.findViewById(R.id.recycler_view_drawer); + recyclerView.setLayoutManager(new LinearLayoutManager(context)); + recyclerView.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL)); + + mDrawerAdapter = new FavDrawerAdapter(inflater); + recyclerView.setAdapter(mDrawerAdapter); + recyclerView.setOnItemClickListener(this); return view; } @@ -549,7 +541,6 @@ public void onDestroyDrawerView() { super.onDestroyDrawerView(); mDrawerAdapter = null; - mDrawerList = null; } @Override @@ -560,6 +551,8 @@ public void onBackPressed() { if (mRecyclerView != null && mRecyclerView.isInCustomChoice()) { mRecyclerView.outOfCustomChoiceMode(); + } else if (mFabLayout != null && mFabLayout.isExpanded()) { + mFabLayout.toggle(); } else if (mSearchMode) { exitSearchMode(true); } else { @@ -590,19 +583,56 @@ public void onEndDragHandler() { @Override @Implemented(EasyRecyclerView.OnItemClickListener.class) public boolean onItemClick(EasyRecyclerView parent, View view, int position, long id) { - if (mRecyclerView != null && mRecyclerView.isInCustomChoice()) { - mRecyclerView.toggleItemChecked(position); - } else if (mHelper != null) { - GalleryInfo gi = mHelper.getDataAt(position); - Bundle args = new Bundle(); - args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO); - args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi); - Announcer announcer = new Announcer(GalleryDetailScene.class).setArgs(args); - View thumb; - if (ApiHelper.SUPPORT_TRANSITION && null != (thumb = view.findViewById(R.id.thumb))) { - announcer.setTranHelper(new EnterGalleryDetailTransaction(thumb)); + if (mDrawerLayout != null && mDrawerLayout.isDrawerOpen(Gravity.RIGHT)){ + // Skip if in search mode + if (mRecyclerView != null && mRecyclerView.isInCustomChoice()) { + return true; + } + + if (mUrlBuilder == null || mHelper == null) { + return true; + } + + // Local favorite position is 0, All favorite position is 1, so position - 2 is OK + int newFavCat = position - 2; + + // Check is the same + if (mUrlBuilder.getFavCat() == newFavCat) { + return true; + } + + // Ensure outOfCustomChoiceMode to avoid error + if (mRecyclerView != null) { + mRecyclerView.isInCustomChoice(); + } + + exitSearchMode(true); + + mUrlBuilder.setKeyword(null); + mUrlBuilder.setFavCat(newFavCat); + updateSearchBar(); + mHelper.refresh(); + + closeDrawer(Gravity.RIGHT); + + } else { + if (mRecyclerView != null && mRecyclerView.isInCustomChoice()) { + mRecyclerView.toggleItemChecked(position); + } else if (mHelper != null) { + GalleryInfo gi = mHelper.getDataAtEx(position); + if (gi == null) { + return true; + } + Bundle args = new Bundle(); + args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO); + args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi); + Announcer announcer = new Announcer(GalleryDetailScene.class).setArgs(args); + View thumb; + if (null != (thumb = view.findViewById(R.id.thumb))) { + announcer.setTranHelper(new EnterGalleryDetailTransaction(thumb)); + } + startScene(announcer); } - startScene(announcer); } return true; } @@ -718,19 +748,86 @@ public void onSearchEditTextBackPressed() { onBackPressed(); } + @Override + public void onExpand(boolean expanded) { + if (expanded) { + mActionFabDrawable.setDelete(ANIMATE_TIME); + } else { + mActionFabDrawable.setAdd(ANIMATE_TIME); + } + } + @Override @Implemented(FabLayout.OnClickFabListener.class) public void onClickPrimaryFab(FabLayout view, FloatingActionButton fab) { - if (mRecyclerView != null) { - mRecyclerView.outOfCustomChoiceMode(); + if (mRecyclerView != null && mFabLayout != null) { + if (mRecyclerView.isInCustomChoice()) { + mRecyclerView.outOfCustomChoiceMode(); + } else { + mFabLayout.toggle(); + } } } + private void showGoToDialog() { + Context context = getContext2(); + if (null == context || null == mHelper) { + return; + } + + final int page = mHelper.getPageForTop(); + final int pages = mHelper.getPages(); + String hint = getString(R.string.go_to_hint, page + 1, pages); + final EditTextDialogBuilder builder = new EditTextDialogBuilder(context, null, hint); + builder.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); + final AlertDialog dialog = builder.setTitle(R.string.go_to) + .setPositiveButton(android.R.string.ok, null) + .show(); + dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> { + if (null == mHelper) { + dialog.dismiss(); + return; + } + + String text = builder.getText().trim(); + int goTo; + try { + goTo = Integer.parseInt(text) - 1; + } catch (NumberFormatException e){ + builder.setError(getString(R.string.error_invalid_number)); + return; + } + if (goTo < 0 || goTo >= pages) { + builder.setError(getString(R.string.error_out_of_range)); + return; + } + builder.setError(null); + mHelper.goTo(goTo); + AppHelper.hideSoftInput(dialog); + dialog.dismiss(); + }); + } + @Override @Implemented(FabLayout.OnClickFabListener.class) public void onClickSecondaryFab(FabLayout view, FloatingActionButton fab, int position) { Context context = getContext2(); - if (null == context || null == mRecyclerView || null == mHelper || !mRecyclerView.isInCustomChoice()) { + if (null == context || null == mRecyclerView || null == mHelper) { + return; + } + + if (!mRecyclerView.isInCustomChoice()) { + switch (position) { + case 0: // Go to + if (mHelper.canGoTo()) { + showGoToDialog(); + } + break; + case 1: // Refresh + mHelper.refresh(); + break; + } + view.setExpanded(false); return; } @@ -738,12 +835,26 @@ public void onClickSecondaryFab(FabLayout view, FloatingActionButton fab, int po SparseBooleanArray stateArray = mRecyclerView.getCheckedItemPositions(); for (int i = 0, n = stateArray.size(); i < n; i++) { if (stateArray.valueAt(i)) { - mModifyGiList.add(mHelper.getDataAt(stateArray.keyAt(i))); + GalleryInfo gi = mHelper.getDataAtEx(stateArray.keyAt(i)); + if (gi != null) { + mModifyGiList.add(gi); + } } } switch (position) { - case 0: { // Delete + case 2: { // Download + Activity activity = getActivity2(); + if (activity != null) { + CommonOperations.startDownload(getActivity2(), mModifyGiList, false); + } + mModifyGiList.clear(); + if (mRecyclerView != null && mRecyclerView.isInCustomChoice()) { + mRecyclerView.outOfCustomChoiceMode(); + } + break; + } + case 3: { // Delete DeleteDialogHelper helper = new DeleteDialogHelper(); new AlertDialog.Builder(context) .setTitle(R.string.delete_favorites_dialog_title) @@ -753,7 +864,7 @@ public void onClickSecondaryFab(FabLayout view, FloatingActionButton fab, int po .show(); break; } - case 1: { // Move + case 4: { // Move MoveDialogHelper helper = new MoveDialogHelper(); // First is local favorite, the other 10 is cloud favorite String[] array = new String[11]; @@ -769,11 +880,45 @@ public void onClickSecondaryFab(FabLayout view, FloatingActionButton fab, int po } } + private Runnable showNormalFabsRunnable = new Runnable() { + @Override + public void run() { + if (mFabLayout != null) { + mFabLayout.setSecondaryFabVisibilityAt(0, true); + mFabLayout.setSecondaryFabVisibilityAt(1, true); + mFabLayout.setSecondaryFabVisibilityAt(2, false); + mFabLayout.setSecondaryFabVisibilityAt(3, false); + mFabLayout.setSecondaryFabVisibilityAt(4, false); + } + } + }; + + private void showNormalFabs() { + // Delay showing normal fabs to avoid mutation + SimpleHandler.getInstance().removeCallbacks(showNormalFabsRunnable); + SimpleHandler.getInstance().postDelayed(showNormalFabsRunnable, 300); + } + + private void showSelectionFabs() { + SimpleHandler.getInstance().removeCallbacks(showNormalFabsRunnable); + + if (mFabLayout != null) { + mFabLayout.setSecondaryFabVisibilityAt(0, false); + mFabLayout.setSecondaryFabVisibilityAt(1, false); + mFabLayout.setSecondaryFabVisibilityAt(2, true); + mFabLayout.setSecondaryFabVisibilityAt(3, true); + mFabLayout.setSecondaryFabVisibilityAt(4, true); + } + } + @Override @Implemented(EasyRecyclerView.CustomChoiceListener.class) public void onIntoCustomChoice(EasyRecyclerView view) { if (mFabLayout != null) { - mFabLayout.setExpanded(true); + showSelectionFabs(); + mFabLayout.setAutoCancel(false); + // Delay expanding action to make layout work fine + SimpleHandler.getInstance().post(() -> mFabLayout.setExpanded(true)); } if (mHelper != null) { mHelper.setRefreshLayoutEnable(false); @@ -787,6 +932,8 @@ public void onIntoCustomChoice(EasyRecyclerView view) { @Implemented(EasyRecyclerView.CustomChoiceListener.class) public void onOutOfCustomChoice(EasyRecyclerView view) { if (mFabLayout != null) { + showNormalFabs(); + mFabLayout.setAutoCancel(true); mFabLayout.setExpanded(false); } if (mHelper != null) { @@ -805,41 +952,6 @@ public void onItemCheckedStateChanged(EasyRecyclerView view, int position, long } } - @Override - @Implemented(AdapterView.OnItemClickListener.class) - public void onItemClick(AdapterView parent, View view, int position, long id) { - // Skip if in search mode - if (mRecyclerView != null && mRecyclerView.isInCustomChoice()) { - return; - } - - if (mUrlBuilder == null || mHelper == null) { - return; - } - - // Local favorite position is 0, All favorite position is 1, so position - 2 is OK - int newFavCat = position - 2; - - // Check is the same - if (mUrlBuilder.getFavCat() == newFavCat) { - return; - } - - // Ensure outOfCustomChoiceMode to avoid error - if (mRecyclerView != null) { - mRecyclerView.isInCustomChoice(); - } - - exitSearchMode(true); - - mUrlBuilder.setKeyword(null); - mUrlBuilder.setFavCat(newFavCat); - updateSearchBar(); - mHelper.refresh(); - - closeDrawer(Gravity.RIGHT); - } - private void enterSearchMode(boolean animation) { if (mSearchMode ||mSearchBar == null || mSearchBarMover == null || mLeftDrawable == null) { return; @@ -864,22 +976,25 @@ private void onGetFavoritesSuccess(FavoritesParser.Result result, int taskId) { if (mHelper != null && mSearchBarMover != null && mHelper.isCurrentTask(taskId)) { - if (mFavCatArray != null && mDrawerList != null) { - for (int i = 0; i < 10; i++) { - mFavCatArray[i] = result.catArray[i]; - mDrawerList.set(i + 2, result.catArray[i]); - } - - if (mDrawerAdapter != null) { - mDrawerAdapter.notifyDataSetChanged(); - } + if (mFavCatArray != null) { + System.arraycopy(result.catArray, 0, mFavCatArray, 0,10); } mFavCountArray = result.countArray; + if (mFavCountArray != null){ + mFavCountSum = 0; + for (int i = 0; i < 10; i++ ){ + mFavCountSum = mFavCountSum + mFavCountArray[i]; + } + Settings.putFavCloudCount(mFavCountSum); + } updateSearchBar(); - mHelper.setPages(taskId, result.pages); - mHelper.onGetPageData(taskId, result.galleryInfoList); + mHelper.onGetPageData(taskId, result.pages, result.nextPage, result.galleryInfoList); + + if (mDrawerAdapter != null) { + mDrawerAdapter.notifyDataSetChanged(); + } } } @@ -899,12 +1014,19 @@ private void onGetFavoritesLocal(String keyword, int taskId) { } else { list = EhDB.searchLocalFavorites(keyword); } + if (list.size() == 0) { - mHelper.setPages(taskId, 0); - mHelper.onGetPageData(taskId, Collections.EMPTY_LIST); + mHelper.onGetPageData(taskId, 0, 0, Collections.EMPTY_LIST); } else { - mHelper.setPages(taskId, 1); - mHelper.onGetPageData(taskId, list); + mHelper.onGetPageData(taskId, 1, 0, list); + } + + if (TextUtils.isEmpty(keyword)) { + mFavLocalCount = list.size(); + Settings.putFavLocalCount(mFavLocalCount); + if (mDrawerAdapter != null) { + mDrawerAdapter.notifyDataSetChanged(); + } } } } @@ -1000,7 +1122,7 @@ private class FavoritesAdapter extends GalleryAdapter { public FavoritesAdapter(@NonNull LayoutInflater inflater, @NonNull Resources resources, @NonNull RecyclerView recyclerView, int type) { - super(inflater, resources, recyclerView, type); + super(inflater, resources, recyclerView, type, false); } @Override @@ -1011,11 +1133,11 @@ public int getItemCount() { @Nullable @Override public GalleryInfo getDataAt(int position) { - return null != mHelper ? mHelper.getDataAt(position) : null; + return null != mHelper ? mHelper.getDataAtEx(position) : null; } } - private class FavoritesHelper extends ContentLayout.ContentHelper { + private class FavoritesHelper extends GalleryInfoContentHelper { @Override protected void getPageData(final int taskId, int type, int page) { @@ -1037,13 +1159,14 @@ protected void getPageData(final int taskId, int type, int page) { gidArray[i] = gi.gid; tokenArray[i] = gi.token; } + List modifyGiListBackup = new ArrayList<>(mModifyGiList); mModifyGiList.clear(); EhRequest request = new EhRequest(); request.setMethod(EhClient.METHOD_ADD_FAVORITES_RANGE); request.setCallback(new AddFavoritesListener(getContext(), activity.getStageId(), getTag(), - taskId, mUrlBuilder.getKeyword())); + taskId, mUrlBuilder.getKeyword(), modifyGiListBackup)); request.setArgs(gidArray, tokenArray, mModifyFavCat); mClient.execute(request); } else { @@ -1129,6 +1252,11 @@ public void onShowView(View hiddenView, View shownView) { } } + @Override + protected boolean isDuplicate(GalleryInfo d1, GalleryInfo d2) { + return d1.gid == d2.gid; + } + @Override protected void onScrollToPosition(int postion) { if (0 == postion) { @@ -1143,12 +1271,14 @@ private static class AddFavoritesListener extends EhCallback mBackup; - public AddFavoritesListener(Context context, int stageId, - String sceneTag, int taskId, String keyword) { + private AddFavoritesListener(Context context, int stageId, + String sceneTag, int taskId, String keyword, List backup) { super(context, stageId, sceneTag); mTaskId = taskId; mKeyword = keyword; + mBackup = backup; } @Override @@ -1161,6 +1291,10 @@ public void onSuccess(Void result) { @Override public void onFailure(Exception e) { + // TODO It's a failure, add all of backup back to db. + // But how to known which one is failed? + EhDB.putLocalFavorites(mBackup); + FavoritesScene scene = getScene(); if (scene != null) { scene.onGetFavoritesLocal(mKeyword, mTaskId); @@ -1183,7 +1317,7 @@ private static class GetFavoritesListener extends EhCallback { private final int mPaddingTopSB; private MarginItemDecoration mListDecoration; private MarginItemDecoration mGirdDecoration; + private final int mListThumbWidth; + private final int mListThumbHeight; private int mType = TYPE_INVALID; + private boolean mShowFavourited; + + private DownloadManager mDownloadManager; public GalleryAdapter(@NonNull LayoutInflater inflater, @NonNull Resources resources, - @NonNull RecyclerView recyclerView, int type) { + @NonNull RecyclerView recyclerView, int type, boolean showFavourited) { mInflater = inflater; mResources = resources; mRecyclerView = recyclerView; mLayoutManager = new AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL); mPaddingTopSB = resources.getDimensionPixelOffset(R.dimen.gallery_padding_top_search_bar); + mShowFavourited = showFavourited; mRecyclerView.setAdapter(this); mRecyclerView.setLayoutManager(mLayoutManager); + View calculator = inflater.inflate(R.layout.item_gallery_list_thumb_height, null); + ViewUtils.measureView(calculator, 1024, ViewGroup.LayoutParams.WRAP_CONTENT); + mListThumbHeight = calculator.getMeasuredHeight(); + mListThumbWidth = mListThumbHeight * 2 / 3; + setType(type); + + mDownloadManager = EhApplication.getDownloadManager(inflater.getContext()); } private void adjustPaddings() { @@ -147,7 +162,17 @@ public GalleryHolder onCreateViewHolder(ViewGroup parent, int viewType) { layoutId = R.layout.item_gallery_grid; break; } - return new GalleryHolder(mInflater.inflate(layoutId, parent, false)); + + GalleryHolder holder = new GalleryHolder(mInflater.inflate(layoutId, parent, false)); + + if (viewType == TYPE_LIST) { + ViewGroup.LayoutParams lp = holder.thumb.getLayoutParams(); + lp.width = mListThumbWidth; + lp.height = mListThumbHeight; + holder.thumb.setLayoutParams(lp); + } + + return holder; } @Override @@ -174,12 +199,27 @@ public void onBindViewHolder(GalleryHolder holder, int position) { holder.rating.setRating(gi.rating); TextView category = holder.category; String newCategoryText = EhUtils.getCategory(gi.category); - if (!newCategoryText.equals(category.getText())) { + if (!newCategoryText.equals(category.getText().toString())) { category.setText(newCategoryText); category.setBackgroundColor(EhUtils.getCategoryColor(gi.category)); } holder.posted.setText(gi.posted); - holder.simpleLanguage.setText(gi.simpleLanguage); + if (gi.pages == 0 || !Settings.getShowGalleryPages()) { + holder.pages.setText(null); + holder.pages.setVisibility(View.GONE); + } else { + holder.pages.setText(Integer.toString(gi.pages) + "P"); + holder.pages.setVisibility(View.VISIBLE); + } + if (TextUtils.isEmpty(gi.simpleLanguage)) { + holder.simpleLanguage.setText(null); + holder.simpleLanguage.setVisibility(View.GONE); + } else { + holder.simpleLanguage.setText(gi.simpleLanguage); + holder.simpleLanguage.setVisibility(View.VISIBLE); + } + holder.favourited.setVisibility((mShowFavourited && gi.favoriteSlot >= -1 && gi.favoriteSlot <= 10) ? View.VISIBLE : View.GONE); + holder.downloaded.setVisibility(mDownloadManager.containDownloadInfo(gi.gid) ? View.VISIBLE : View.GONE); break; } case TYPE_GRID: { @@ -200,9 +240,6 @@ public void onBindViewHolder(GalleryHolder holder, int position) { } // Update transition name - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - long gid = gi.gid; - holder.thumb.setTransitionName(TransitionNameFactory.getThumbTransitionName(gid)); - } + ViewCompat.setTransitionName(holder.thumb, TransitionNameFactory.getThumbTransitionName(gi.gid)); } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryCommentsScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryCommentsScene.java index 22cd8f3b3..621f93537 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryCommentsScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryCommentsScene.java @@ -18,23 +18,17 @@ import android.animation.Animator; import android.annotation.SuppressLint; -import android.app.Activity; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; import android.content.res.Resources; +import android.graphics.Color; import android.graphics.Typeface; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.os.Parcelable; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.FloatingActionButton; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.DefaultItemAnimator; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; +import android.os.Looper; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; @@ -51,7 +45,14 @@ import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.DefaultItemAnimator; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.hippo.android.resource.AttrResources; import com.hippo.easyrecyclerview.EasyRecyclerView; import com.hippo.easyrecyclerview.LinearDividerItemDecoration; import com.hippo.ehviewer.EhApplication; @@ -61,6 +62,8 @@ import com.hippo.ehviewer.client.EhRequest; import com.hippo.ehviewer.client.EhUrl; import com.hippo.ehviewer.client.data.GalleryComment; +import com.hippo.ehviewer.client.data.GalleryCommentList; +import com.hippo.ehviewer.client.data.GalleryDetail; import com.hippo.ehviewer.client.parser.VoteCommentParser; import com.hippo.ehviewer.ui.MainActivity; import com.hippo.reveal.ViewAnimationUtils; @@ -69,6 +72,7 @@ import com.hippo.text.Html; import com.hippo.text.URLImageGetter; import com.hippo.util.DrawableManager; +import com.hippo.util.ExceptionUtils; import com.hippo.util.ReadableTime; import com.hippo.util.TextUrl; import com.hippo.view.ViewTransition; @@ -76,15 +80,13 @@ import com.hippo.widget.LinkifyTextView; import com.hippo.widget.ObservedTextView; import com.hippo.yorozuya.AnimationUtils; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.LayoutUtils; import com.hippo.yorozuya.ResourcesUtils; import com.hippo.yorozuya.SimpleAnimatorListener; import com.hippo.yorozuya.StringUtils; import com.hippo.yorozuya.ViewUtils; import com.hippo.yorozuya.collect.IntList; - -import junit.framework.Assert; - import java.util.ArrayList; import java.util.List; @@ -98,14 +100,14 @@ public final class GalleryCommentsScene extends ToolbarScene public static final String KEY_API_KEY = "api_key"; public static final String KEY_GID = "gid"; public static final String KEY_TOKEN = "token"; - public static final String KEY_COMMENTS = "comments"; + public static final String KEY_COMMENT_LIST = "comment_list"; private long mApiUid; private String mApiKey; private long mGid; private String mToken; @Nullable - private GalleryComment[] mComments; + private GalleryCommentList mCommentList; @Nullable private EasyRecyclerView mRecyclerView; @@ -124,8 +126,15 @@ public final class GalleryCommentsScene extends ToolbarScene @Nullable private ViewTransition mViewTransition; + private Drawable mSendDrawable; + private Drawable mPencilDrawable; + private long mCommentId; + private boolean mInAnimation = false; + private boolean mShowAllComments = false; + private boolean mRefreshingComments = false; + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -146,10 +155,8 @@ private void handleArgs(Bundle args) { mApiKey = args.getString(KEY_API_KEY); mGid = args.getLong(KEY_GID, -1L); mToken = args.getString(KEY_TOKEN, null); - Parcelable[] parcelables = args.getParcelableArray(KEY_COMMENTS); - if (parcelables instanceof GalleryComment[]) { - mComments = (GalleryComment[]) parcelables; - } + mCommentList = args.getParcelable(KEY_COMMENT_LIST); + mShowAllComments = mCommentList != null && !mCommentList.hasMore; } private void onInit() { @@ -161,10 +168,8 @@ private void onRestore(@NonNull Bundle savedInstanceState) { mApiKey = savedInstanceState.getString(KEY_API_KEY); mGid = savedInstanceState.getLong(KEY_GID, -1L); mToken = savedInstanceState.getString(KEY_TOKEN, null); - Parcelable[] parcelables = savedInstanceState.getParcelableArray(KEY_COMMENTS); - if (parcelables instanceof GalleryComment[]) { - mComments = (GalleryComment[]) parcelables; - } + mCommentList = savedInstanceState.getParcelable(KEY_COMMENT_LIST); + mShowAllComments = mCommentList != null && !mCommentList.hasMore; } @Override @@ -174,7 +179,7 @@ public void onSaveInstanceState(Bundle outState) { outState.putString(KEY_API_KEY, mApiKey); outState.putLong(KEY_GID, mGid); outState.putString(KEY_TOKEN, mToken); - outState.putParcelableArray(KEY_COMMENTS, mComments); + outState.putParcelable(KEY_COMMENT_LIST, mCommentList); } @Nullable @@ -191,24 +196,27 @@ public View onCreateView3(LayoutInflater inflater, mFab = (FloatingActionButton) ViewUtils.$$(view, R.id.fab); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); Resources resources = context.getResources(); int paddingBottomFab = resources.getDimensionPixelOffset(R.dimen.gallery_padding_bottom_fab); - Drawable drawable = DrawableManager.getDrawable(context, R.drawable.big_weird_face); + Drawable drawable = DrawableManager.getVectorDrawable(context, R.drawable.big_sad_pandroid); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); tip.setCompoundDrawables(null, drawable, null, null); + mSendDrawable = DrawableManager.getVectorDrawable(context, R.drawable.v_send_dark_x24); + mPencilDrawable = DrawableManager.getVectorDrawable(context, R.drawable.v_pencil_dark_x24); + mAdapter = new CommentAdapter(); mRecyclerView.setAdapter(mAdapter); mRecyclerView.setLayoutManager(new LinearLayoutManager(context, - LinearLayoutManager.VERTICAL, false)); + RecyclerView.VERTICAL, false)); LinearDividerItemDecoration decoration = new LinearDividerItemDecoration( - LinearDividerItemDecoration.VERTICAL, context.getResources().getColor(R.color.divider), + LinearDividerItemDecoration.VERTICAL, AttrResources.getAttrColor(context, R.attr.dividerColor), LayoutUtils.dp2pix(context, 1)); decoration.setShowLastDivider(true); mRecyclerView.addItemDecoration(decoration); - mRecyclerView.setSelector(Ripple.generateRippleDrawable(context, false)); + mRecyclerView.setSelector(Ripple.generateRippleDrawable(context, !AttrResources.getAttrBoolean(context, R.attr.isLightTheme), new ColorDrawable(Color.TRANSPARENT))); mRecyclerView.setHasFixedSize(true); mRecyclerView.setOnItemClickListener(this); mRecyclerView.setPadding(mRecyclerView.getPaddingLeft(), mRecyclerView.getPaddingTop(), @@ -321,7 +329,7 @@ public void showVoteStatusDialog(Context context, String voteStatus) { rv.setAdapter(new RecyclerView.Adapter() { @Override public InfoHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new InfoHolder(inflater.inflate(R.layout.item_favorite_info_data, parent, false)); + return new InfoHolder(inflater.inflate(R.layout.item_drawer_favorites, parent, false)); } @Override @@ -337,36 +345,38 @@ public int getItemCount() { }); rv.setLayoutManager(new LinearLayoutManager(context)); LinearDividerItemDecoration decoration = new LinearDividerItemDecoration( - LinearDividerItemDecoration.VERTICAL, context.getResources().getColor(R.color.divider), + LinearDividerItemDecoration.VERTICAL, AttrResources.getAttrColor(context, R.attr.dividerColor), LayoutUtils.dp2pix(context, 1)); decoration.setPadding(ResourcesUtils.getAttrDimensionPixelOffset(context, R.attr.dialogPreferredPadding)); rv.addItemDecoration(decoration); - rv.setSelector(Ripple.generateRippleDrawable(context, false)); + rv.setSelector(Ripple.generateRippleDrawable(context, !AttrResources.getAttrBoolean(context, R.attr.isLightTheme), new ColorDrawable(Color.TRANSPARENT))); rv.setClipToPadding(false); builder.setView(rv).show(); } private void showCommentDialog(int position) { final Context context = getContext2(); - if (null == context || null == mComments || position >= mComments.length || position < 0) { + if (context == null || mCommentList == null || mCommentList.comments == null || position >= mCommentList.comments.length || position < 0) { return; } - final GalleryComment comment = mComments[position]; + final GalleryComment comment = mCommentList.comments[position]; List menu = new ArrayList<>(); final IntList menuId = new IntList(); Resources resources = context.getResources(); - if (0 == comment.id || mApiUid < 0) { - // 0 id is uploader comment, can't vote - // Not sign in, can't vote - menu.add(resources.getString(R.string.copy_comment_text)); - menuId.add(R.id.copy); - } else { - menu.add(resources.getString(R.string.copy_comment_text)); - menuId.add(R.id.copy); - menu.add(resources.getString(comment.voteUp ? R.string.cancel_vote_up : R.string.vote_up)); + + menu.add(resources.getString(R.string.copy_comment_text)); + menuId.add(R.id.copy); + if (comment.editable) { + menu.add(resources.getString(R.string.edit_comment)); + menuId.add(R.id.edit_comment); + } + if (comment.voteUpAble) { + menu.add(resources.getString(comment.voteUpEd ? R.string.cancel_vote_up : R.string.vote_up)); menuId.add(R.id.vote_up); - menu.add(resources.getString(comment.voteDown ? R.string.cancel_vote_down : R.string.vote_down)); + } + if (comment.voteDownAble) { + menu.add(resources.getString(comment.voteDownEd ? R.string.cancel_vote_down : R.string.vote_down)); menuId.add(R.id.vote_down); } if (!TextUtils.isEmpty(comment.voteState)) { @@ -397,6 +407,12 @@ public void onClick(DialogInterface dialog, int which) { case R.id.check_vote_status: showVoteStatusDialog(context, comment.voteState); break; + case R.id.edit_comment: + prepareEditComment(comment.id); + if (!mInAnimation && mEditPanel != null && mEditPanel.getVisibility() != View.VISIBLE) { + showEditPanel(true); + } + break; } } }).show(); @@ -404,24 +420,37 @@ public void onClick(DialogInterface dialog, int which) { @Override public boolean onItemClick(EasyRecyclerView parent, View view, int position, long id) { - Activity activity = getActivity2(); + MainActivity activity = getActivity2(); if (null == activity) { return false; } RecyclerView.ViewHolder holder = parent.getChildViewHolder(view); - if (holder instanceof CommentHolder) { - CommentHolder commentHolder = (CommentHolder) holder; + if (holder instanceof ActualCommentHolder) { + ActualCommentHolder commentHolder = (ActualCommentHolder) holder; ClickableSpan span = commentHolder.comment.getCurrentSpan(); commentHolder.comment.clearCurrentSpan(); if (span instanceof URLSpan) { UrlOpener.openUrl(activity, ((URLSpan) span).getURL(), true); - return true; + } else { + showCommentDialog(position); } - } + } else if (holder instanceof MoreCommentHolder && !mRefreshingComments && mAdapter != null) { + mRefreshingComments = true; + mShowAllComments = true; + mAdapter.notifyItemChanged(position); - showCommentDialog(position); + String url = getGalleryDetailUrl(); + if (url != null) { + // Request + EhRequest request = new EhRequest() + .setMethod(EhClient.METHOD_GET_GALLERY_DETAIL) + .setArgs(url) + .setCallback(new RefreshCommentListener(activity, activity.getStageId(), getTag())); + EhApplication.getEhClient(activity).execute(request); + } + } return true; } @@ -431,13 +460,27 @@ private void updateView(boolean animation) { return; } - if (mComments == null || mComments.length <= 0) { + if (mCommentList == null || mCommentList.comments == null || mCommentList.comments.length <= 0) { mViewTransition.showView(1, animation); } else { mViewTransition.showView(0, animation); } } + private void prepareNewComment() { + mCommentId = 0; + if (mSendImage != null) { + mSendImage.setImageDrawable(mSendDrawable); + } + } + + private void prepareEditComment(long commentId) { + mCommentId = commentId; + if (mSendImage != null) { + mSendImage.setImageDrawable(mPencilDrawable); + } + } + private void showEditPanelWithAnimation() { if (null == mFab || null == mEditPanel) { return; @@ -459,7 +502,7 @@ public void onAnimationEnd(Animator animation) { return; } - mFab.setVisibility(View.INVISIBLE); + ((View) mFab).setVisibility(View.INVISIBLE); mEditPanel.setVisibility(View.VISIBLE); int halfW = mEditPanel.getWidth() / 2; int halfH = mEditPanel.getHeight() / 2; @@ -484,7 +527,7 @@ private void showEditPanel(boolean animation) { return; } - mFab.setVisibility(View.INVISIBLE); + ((View) mFab).setVisibility(View.INVISIBLE); mEditPanel.setVisibility(View.VISIBLE); } } @@ -506,8 +549,15 @@ public void onAnimationEnd(Animator a) { return; } + if (Looper.myLooper() != Looper.getMainLooper()) { + // Some devices may run this block in non-UI thread. + // It might be a bug of Android OS. + // Check it here to avoid crash. + return; + } + mEditPanel.setVisibility(View.GONE); - mFab.setVisibility(View.VISIBLE); + ((View) mFab).setVisibility(View.VISIBLE); int fabStartX = mEditPanel.getLeft() + (mEditPanel.getWidth() / 2) - (mFab.getWidth() / 2); int fabStartY = mEditPanel.getTop() + (mEditPanel.getHeight() / 2) - (mFab.getHeight() / 2); mFab.setX(fabStartX); @@ -536,7 +586,7 @@ private void hideEditPanel(boolean animation) { return; } - mFab.setVisibility(View.VISIBLE); + ((View) mFab).setVisibility(View.VISIBLE); mEditPanel.setVisibility(View.INVISIBLE); } } @@ -544,7 +594,7 @@ private void hideEditPanel(boolean animation) { @Nullable private String getGalleryDetailUrl() { if (mGid != -1 && mToken != null) { - return EhUrl.getGalleryDetailUrl(mGid, mToken, 0, false); + return EhUrl.getGalleryDetailUrl(mGid, mToken, 0, mShowAllComments); } else { return null; } @@ -560,6 +610,7 @@ public void onClick(View v) { if (mFab == v) { if (!mInAnimation) { + prepareNewComment(); showEditPanel(true); } } else if (mSendImage == v) { @@ -576,9 +627,9 @@ public void onClick(View v) { // Request EhRequest request = new EhRequest() .setMethod(EhClient.METHOD_GET_COMMENT_GALLERY) - .setArgs(url, comment) + .setArgs(url, comment, mCommentId != 0 ? Long.toString(mCommentId) : null) .setCallback(new CommentGalleryListener(context, - activity.getStageId(), getTag())); + activity.getStageId(), getTag(), mCommentId)); EhApplication.getEhClient(context).execute(request); hideSoftInput(); hideEditPanel(true); @@ -598,37 +649,32 @@ public void onBackPressed() { } } - private class CommentHolder extends RecyclerView.ViewHolder { - - public final TextView user; - public final TextView time; - public final LinkifyTextView comment; + private static final int TYPE_COMMENT = 0; + private static final int TYPE_MORE = 1; + private static final int TYPE_PROGRESS = 2; - public CommentHolder(View itemView) { - super(itemView); - user = (TextView) itemView.findViewById(R.id.user); - time = (TextView) itemView.findViewById(R.id.time); - comment = (LinkifyTextView) itemView.findViewById(R.id.comment); + private abstract class CommentHolder extends RecyclerView.ViewHolder { + public CommentHolder(LayoutInflater inflater, int resId, ViewGroup parent) { + super(inflater.inflate(resId, parent, false)); } } - private class CommentAdapter extends RecyclerView.Adapter { + private class ActualCommentHolder extends CommentHolder { - private final LayoutInflater mInflater; - - public CommentAdapter() { - mInflater = getLayoutInflater2(); - Assert.assertNotNull(mInflater); - } + private final TextView user; + private final TextView time; + private final LinkifyTextView comment; - @Override - public CommentHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new CommentHolder(mInflater.inflate(R.layout.item_gallery_comment, parent, false)); + public ActualCommentHolder(LayoutInflater inflater, ViewGroup parent) { + super(inflater, R.layout.item_gallery_comment, parent); + user = itemView.findViewById(R.id.user); + time = itemView.findViewById(R.id.time); + comment = itemView.findViewById(R.id.comment); } - public CharSequence generateComment(Context context, ObservedTextView textView, GalleryComment comment) { + private CharSequence generateComment(Context context, ObservedTextView textView, GalleryComment comment) { SpannableStringBuilder ssb = Html.fromHtml(comment.comment, new URLImageGetter(textView, - EhApplication.getConaco(context)), null); + EhApplication.getConaco(context)), null); if (0 != comment.id && 0 != comment.score) { int score = comment.score; @@ -636,42 +682,136 @@ public CharSequence generateComment(Context context, ObservedTextView textView, SpannableString ss = new SpannableString(scoreString); ss.setSpan(new RelativeSizeSpan(0.8f), 0, scoreString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); ss.setSpan(new StyleSpan(Typeface.BOLD), 0, scoreString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - ss.setSpan(new ForegroundColorSpan(context.getResources().getColor(R.color.secondary_text_default_light)) - , 0, scoreString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + ss.setSpan(new ForegroundColorSpan(AttrResources.getAttrColor(context, android.R.attr.textColorSecondary)) + , 0, scoreString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); ssb.append(" ").append(ss); } + if (comment.lastEdited != 0) { + String str = context.getString(R.string.last_edited, ReadableTime.getTimeAgo(comment.lastEdited)); + SpannableString ss = new SpannableString(str); + ss.setSpan(new RelativeSizeSpan(0.8f), 0, str.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + ss.setSpan(new StyleSpan(Typeface.BOLD), 0, str.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + ss.setSpan(new ForegroundColorSpan(AttrResources.getAttrColor(context, android.R.attr.textColorSecondary)), + 0, str.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + ssb.append("\n\n").append(ss); + } + return TextUrl.handleTextUrl(ssb); } + public void bind(GalleryComment value) { + user.setText(value.user); + time.setText(ReadableTime.getTimeAgo(value.time)); + comment.setText(generateComment(comment.getContext(), comment, value)); + } + } + + private class MoreCommentHolder extends CommentHolder { + public MoreCommentHolder(LayoutInflater inflater, ViewGroup parent) { + super(inflater, R.layout.item_gallery_comment_more, parent); + } + } + + private class ProgressCommentHolder extends CommentHolder { + public ProgressCommentHolder(LayoutInflater inflater, ViewGroup parent) { + super(inflater, R.layout.item_gallery_comment_progress, parent); + } + } + + private class CommentAdapter extends RecyclerView.Adapter { + + private final LayoutInflater mInflater; + + public CommentAdapter() { + mInflater = getLayoutInflater2(); + AssertUtils.assertNotNull(mInflater); + } + @Override - public void onBindViewHolder(CommentHolder holder, int position) { + public CommentHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + case TYPE_COMMENT: + return new ActualCommentHolder(mInflater, parent); + case TYPE_MORE: + return new MoreCommentHolder(mInflater, parent); + case TYPE_PROGRESS: + return new ProgressCommentHolder(mInflater, parent); + default: + throw new IllegalStateException("Invalid view type: " + viewType); + } + } + + @Override + public void onBindViewHolder(@NonNull CommentHolder holder, int position) { Context context = getContext2(); - if (null == context || null == mComments) { + if (context == null || mCommentList == null) { return; } - GalleryComment comment = mComments[position]; - holder.user.setText(comment.user); - holder.time.setText(ReadableTime.getTimeAgo(comment.time)); - holder.comment.setText(generateComment(context, holder.comment, comment)); + if (holder instanceof ActualCommentHolder) { + ((ActualCommentHolder) holder).bind(mCommentList.comments[position]); + } } @Override public int getItemCount() { - return mComments == null ? 0 : mComments.length; + if (mCommentList == null || mCommentList.comments == null) { + return 0; + } else if (mCommentList.hasMore) { + return mCommentList.comments.length + 1; + } else { + return mCommentList.comments.length; + } + } + + @Override + public int getItemViewType(int position) { + if (position >= mCommentList.comments.length) { + return mRefreshingComments ? TYPE_PROGRESS : TYPE_MORE; + } else { + return TYPE_COMMENT; + } + } + } + + private void onRefreshGallerySuccess(GalleryCommentList result) { + if (mAdapter == null) { + return; + } + + mRefreshingComments = false; + mCommentList = result; + mAdapter.notifyDataSetChanged(); + + updateView(true); + + Bundle re = new Bundle(); + re.putParcelable(KEY_COMMENT_LIST, result); + setResult(SceneFragment.RESULT_OK, re); + } + + private void onRefreshGalleryFailure() { + if (mAdapter == null) { + return; + } + + mRefreshingComments = false; + int position = mAdapter.getItemCount() - 1; + if (position >= 0) { + mAdapter.notifyItemChanged(position); } } - private void onCommentGallerySuccess(GalleryComment[] result) { - if (null == mAdapter) { + private void onCommentGallerySuccess(GalleryCommentList result) { + if (mAdapter == null) { return; } - mComments = result; + mCommentList = result; mAdapter.notifyDataSetChanged(); Bundle re = new Bundle(); - re.putParcelableArray(KEY_COMMENTS, result); + re.putParcelable(KEY_COMMENT_LIST, result); setResult(SceneFragment.RESULT_OK, re); // Remove text @@ -683,13 +823,13 @@ private void onCommentGallerySuccess(GalleryComment[] result) { } private void onVoteCommentSuccess(VoteCommentParser.Result result) { - if (null == mAdapter || null == mComments) { + if (mAdapter == null || mCommentList == null || mCommentList.comments == null ) { return; } int position = -1; - for (int i = 0, n = mComments.length; i < n; i++) { - GalleryComment comment = mComments[i]; + for (int i = 0, n = mCommentList.comments.length; i < n; i++) { + GalleryComment comment = mCommentList.comments[i]; if (comment.id == result.id) { position = i; break; @@ -702,32 +842,66 @@ private void onVoteCommentSuccess(VoteCommentParser.Result result) { } // Update comment - GalleryComment comment = mComments[position]; + GalleryComment comment = mCommentList.comments[position]; comment.score = result.score; if (result.expectVote > 0) { - comment.voteUp = 0 != result.vote; - comment.voteDown = false; + comment.voteUpEd = 0 != result.vote; + comment.voteDownEd = false; } else { - comment.voteDown = 0 != result.vote; - comment.voteUp = false; + comment.voteDownEd = 0 != result.vote; + comment.voteUpEd = false; } mAdapter.notifyItemChanged(position); Bundle re = new Bundle(); - re.putParcelableArray(KEY_COMMENTS, mComments); + re.putParcelable(KEY_COMMENT_LIST, mCommentList); setResult(SceneFragment.RESULT_OK, re); } - private static class CommentGalleryListener extends EhCallback { + private static class RefreshCommentListener extends EhCallback { + + public RefreshCommentListener(Context context, int stageId, String sceneTag) { + super(context, stageId, sceneTag); + } + + @Override + public void onSuccess(GalleryDetail result) { + GalleryCommentsScene scene = getScene(); + if (scene != null) { + scene.onRefreshGallerySuccess(result.comments); + } + } + + @Override + public void onFailure(Exception e) { + GalleryCommentsScene scene = getScene(); + if (scene != null) { + scene.onRefreshGalleryFailure(); + } + } + + @Override + public void onCancel() { } + + @Override + public boolean isInstance(SceneFragment scene) { + return scene instanceof GalleryCommentsScene; + } + } + + private static class CommentGalleryListener extends EhCallback { + + private long mCommentId; - public CommentGalleryListener(Context context, int stageId, String sceneTag) { + public CommentGalleryListener(Context context, int stageId, String sceneTag, long commentId) { super(context, stageId, sceneTag); + mCommentId = commentId; } @Override - public void onSuccess(GalleryComment[] result) { - showTip(R.string.comment_successfully, LENGTH_SHORT); + public void onSuccess(GalleryCommentList result) { + showTip(mCommentId != 0 ? R.string.edit_comment_successfully : R.string.comment_successfully, LENGTH_SHORT); GalleryCommentsScene scene = getScene(); if (scene != null) { @@ -737,7 +911,7 @@ public void onSuccess(GalleryComment[] result) { @Override public void onFailure(Exception e) { - showTip(R.string.comment_failed, LENGTH_SHORT); + showTip(getContent().getString(mCommentId != 0 ? R.string.edit_comment_failed : R.string.comment_failed) + "\n" + ExceptionUtils.getReadableString(e), LENGTH_LONG); } @Override @@ -771,7 +945,7 @@ public void onSuccess(VoteCommentParser.Result result) { @Override public void onFailure(Exception e) { - showTip(R.string.vote_failed, LENGTH_SHORT); + showTip(R.string.vote_failed, LENGTH_LONG); } @Override diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java old mode 100644 new mode 100755 index dd0a68fa8..96d826800 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.java @@ -26,26 +26,17 @@ import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.os.Environment; -import android.os.Parcelable; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentTransaction; -import android.support.v4.view.ViewCompat; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.PopupMenu; import android.text.TextUtils; -import android.transition.TransitionInflater; import android.util.Pair; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MenuItem; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.ViewParent; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ImageView; @@ -53,9 +44,19 @@ import android.widget.ListView; import android.widget.RatingBar; import android.widget.TextView; - +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.PopupMenu; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; +import androidx.transition.TransitionInflater; +import com.hippo.android.resource.AttrResources; import com.hippo.beerbelly.BeerBelly; import com.hippo.drawable.RoundSideRectDrawable; +import com.hippo.drawerlayout.DrawerLayout; import com.hippo.ehviewer.AppConfig; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.EhDB; @@ -66,9 +67,11 @@ import com.hippo.ehviewer.client.EhClient; import com.hippo.ehviewer.client.EhFilter; import com.hippo.ehviewer.client.EhRequest; +import com.hippo.ehviewer.client.EhTagDatabase; import com.hippo.ehviewer.client.EhUrl; import com.hippo.ehviewer.client.EhUtils; import com.hippo.ehviewer.client.data.GalleryComment; +import com.hippo.ehviewer.client.data.GalleryCommentList; import com.hippo.ehviewer.client.data.GalleryDetail; import com.hippo.ehviewer.client.data.GalleryInfo; import com.hippo.ehviewer.client.data.GalleryTagGroup; @@ -90,7 +93,6 @@ import com.hippo.scene.TransitionHelper; import com.hippo.text.Html; import com.hippo.text.URLImageGetter; -import com.hippo.util.ApiHelper; import com.hippo.util.AppHelper; import com.hippo.util.DrawableManager; import com.hippo.util.ExceptionUtils; @@ -101,14 +103,12 @@ import com.hippo.widget.ObservedTextView; import com.hippo.widget.ProgressView; import com.hippo.widget.SimpleGridAutoSpanLayout; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.FileUtils; import com.hippo.yorozuya.IOUtils; import com.hippo.yorozuya.IntIdGenerator; import com.hippo.yorozuya.SimpleHandler; import com.hippo.yorozuya.ViewUtils; - -import junit.framework.Assert; - import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -116,6 +116,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; +import okhttp3.HttpUrl; public class GalleryDetailScene extends BaseScene implements View.OnClickListener, com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener, @@ -145,6 +146,8 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene private static final String KEY_GALLERY_DETAIL = "gallery_detail"; private static final String KEY_REQUEST_ID = "request_id"; + private static final boolean TRANSITION_ANIMATION_DISABLED = true; + /*--------------- View life cycle ---------------*/ @@ -409,7 +412,7 @@ public View onCreateView2(LayoutInflater inflater, @Nullable ViewGroup container long gid = getGid(); if (gid != -1) { Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); mDownloadState = EhApplication.getDownloadManager(context).getDownloadState(gid); } else { mDownloadState = DownloadInfo.STATE_INVALID; @@ -424,13 +427,43 @@ public View onCreateView2(LayoutInflater inflater, @Nullable ViewGroup container mViewTransition = new ViewTransition(mainView, progressView, mTip); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); + + View actionsScrollView = ViewUtils.$$(view, R.id.actions_scroll_view); + setDrawerGestureBlocker(new DrawerLayout.GestureBlocker() { + private void transformPointToViewLocal(int[] point, View child) { + ViewParent viewParent = child.getParent(); + + while (viewParent instanceof View) { + View view = (View) viewParent; + point[0] += view.getScrollX() - child.getLeft(); + point[1] += view.getScrollY() - child.getTop(); + + if (view instanceof DrawerLayout) { + break; + } - Drawable drawable = DrawableManager.getDrawable(context, R.drawable.big_weird_face); + child = view; + viewParent = child.getParent(); + } + } + + @Override + public boolean shouldBlockGesture(MotionEvent ev) { + int[] point = new int[] {(int) ev.getX(), (int) ev.getY()}; + transformPointToViewLocal(point, actionsScrollView); + return !isDrawersVisible() + && point[0] > 0 && point[0] < actionsScrollView.getWidth() + && point[1] > 0 && point[1] < actionsScrollView.getHeight(); + } + }); + + Drawable drawable = DrawableManager.getVectorDrawable(context, R.drawable.big_sad_pandroid); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); mTip.setCompoundDrawables(null, drawable, null, null); mTip.setOnClickListener(this); + boolean isDarkTheme = !AttrResources.getAttrBoolean(context, R.attr.isLightTheme); mHeader = ViewUtils.$$(mainView, R.id.header); mColorBg = ViewUtils.$$(mHeader, R.id.color_bg); mThumb = (LoadImageView) ViewUtils.$$(mHeader, R.id.thumb); @@ -441,9 +474,9 @@ public View onCreateView2(LayoutInflater inflater, @Nullable ViewGroup container mActionGroup = (ViewGroup) ViewUtils.$$(mHeader, R.id.action_card); mDownload = (TextView) ViewUtils.$$(mActionGroup, R.id.download); mRead = ViewUtils.$$(mActionGroup, R.id.read); - Ripple.addRipple(mOtherActions, false); - Ripple.addRipple(mDownload, false); - Ripple.addRipple(mRead, false); + Ripple.addRipple(mOtherActions, isDarkTheme); + Ripple.addRipple(mDownload, isDarkTheme); + Ripple.addRipple(mRead, isDarkTheme); mUploader.setOnClickListener(this); mCategory.setOnClickListener(this); mOtherActions.setOnClickListener(this); @@ -462,7 +495,7 @@ public View onCreateView2(LayoutInflater inflater, @Nullable ViewGroup container mSize = (TextView) ViewUtils.$$(mInfo, R.id.size); mPosted = (TextView) ViewUtils.$$(mInfo, R.id.posted); mFavoredTimes = (TextView) ViewUtils.$$(mInfo, R.id.favoredTimes); - Ripple.addRipple(mInfo, false); + Ripple.addRipple(mInfo, isDarkTheme); mInfo.setOnClickListener(this); mActions = ViewUtils.$$(belowHeader, R.id.actions); @@ -477,13 +510,13 @@ public View onCreateView2(LayoutInflater inflater, @Nullable ViewGroup container mRate = (TextView) ViewUtils.$$(mActions, R.id.rate); mSimilar = (TextView) ViewUtils.$$(mActions, R.id.similar); mSearchCover = (TextView) ViewUtils.$$(mActions, R.id.search_cover); - Ripple.addRipple(mHeartGroup, false); - Ripple.addRipple(mTorrent, false); - Ripple.addRipple(mArchive, false); - Ripple.addRipple(mShare, false); - Ripple.addRipple(mRate, false); - Ripple.addRipple(mSimilar, false); - Ripple.addRipple(mSearchCover, false); + Ripple.addRipple(mHeartGroup, isDarkTheme); + Ripple.addRipple(mTorrent, isDarkTheme); + Ripple.addRipple(mArchive, isDarkTheme); + Ripple.addRipple(mShare, isDarkTheme); + Ripple.addRipple(mRate, isDarkTheme); + Ripple.addRipple(mSimilar, isDarkTheme); + Ripple.addRipple(mSearchCover, isDarkTheme); mHeartGroup.setOnClickListener(this); mTorrent.setOnClickListener(this); mArchive.setOnClickListener(this); @@ -498,13 +531,13 @@ public View onCreateView2(LayoutInflater inflater, @Nullable ViewGroup container mComments = (LinearLayout) ViewUtils.$$(belowHeader, R.id.comments); mCommentsText = (TextView) ViewUtils.$$(mComments, R.id.comments_text); - Ripple.addRipple(mComments, false); + Ripple.addRipple(mComments, isDarkTheme); mComments.setOnClickListener(this); mPreviews = ViewUtils.$$(belowHeader, R.id.previews); mGridLayout = (SimpleGridAutoSpanLayout) ViewUtils.$$(mPreviews, R.id.grid_layout); mPreviewText = (TextView) ViewUtils.$$(mPreviews, R.id.preview_text); - Ripple.addRipple(mPreviews, false); + Ripple.addRipple(mPreviews, isDarkTheme); mPreviews.setOnClickListener(this); mProgress = ViewUtils.$$(mainView, R.id.progress); @@ -543,9 +576,11 @@ public void onDestroyView() { super.onDestroyView(); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); EhApplication.getDownloadManager(context).removeDownloadInfoListener(this); + setDrawerGestureBlocker(null); + mTip = null; mViewTransition = null; @@ -600,7 +635,7 @@ public void onDestroyView() { private boolean prepareData() { Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); if (mGalleryDetail != null) { return true; @@ -653,21 +688,21 @@ private void setActionDrawable(TextView text, Drawable drawable) { } private void ensureActionDrawable(Context context) { - Drawable heart = DrawableManager.getDrawable(context, R.drawable.v_heart_primary_x48); + Drawable heart = DrawableManager.getVectorDrawable(context, R.drawable.v_heart_primary_x48); setActionDrawable(mHeart, heart); - Drawable heartOutline = DrawableManager.getDrawable(context, R.drawable.v_heart_outline_primary_x48); + Drawable heartOutline = DrawableManager.getVectorDrawable(context, R.drawable.v_heart_outline_primary_x48); setActionDrawable(mHeartOutline, heartOutline); - Drawable torrent = DrawableManager.getDrawable(context, R.drawable.v_utorrent_primary_x48); + Drawable torrent = DrawableManager.getVectorDrawable(context, R.drawable.v_utorrent_primary_x48); setActionDrawable(mTorrent, torrent); - Drawable archive = DrawableManager.getDrawable(context, R.drawable.v_archive_primary_x48); + Drawable archive = DrawableManager.getVectorDrawable(context, R.drawable.v_archive_primary_x48); setActionDrawable(mArchive, archive); - Drawable share = DrawableManager.getDrawable(context, R.drawable.v_share_primary_x48); + Drawable share = DrawableManager.getVectorDrawable(context, R.drawable.v_share_primary_x48); setActionDrawable(mShare, share); - Drawable rate = DrawableManager.getDrawable(context, R.drawable.v_thumb_up_primary_x48); + Drawable rate = DrawableManager.getVectorDrawable(context, R.drawable.v_thumb_up_primary_x48); setActionDrawable(mRate, rate); - Drawable similar = DrawableManager.getDrawable(context, R.drawable.v_similar_primary_x48); + Drawable similar = DrawableManager.getVectorDrawable(context, R.drawable.v_similar_primary_x48); setActionDrawable(mSimilar, similar); - Drawable searchCover = DrawableManager.getDrawable(context, R.drawable.v_file_find_primary_x48); + Drawable searchCover = DrawableManager.getVectorDrawable(context, R.drawable.v_file_find_primary_x48); setActionDrawable(mSearchCover, searchCover); } @@ -679,8 +714,19 @@ private boolean createCircularReveal() { int w = mColorBg.getWidth(); int h = mColorBg.getHeight(); if (ViewCompat.isAttachedToWindow(mColorBg) && w != 0 && h != 0) { - ViewAnimationUtils.createCircularReveal(mColorBg, w / 2, h / 2, 0, - (float) Math.hypot(w / 2, h / 2)).setDuration(300).start(); + Resources resources = getContext2().getResources(); + int keylineMargin = resources.getDimensionPixelSize(R.dimen.keyline_margin); + int thumbWidth = resources.getDimensionPixelSize(R.dimen.gallery_detail_thumb_width); + int thumbHeight = resources.getDimensionPixelSize(R.dimen.gallery_detail_thumb_height); + + int x = thumbWidth / 2 + keylineMargin; + int y = thumbHeight / 2 + keylineMargin; + + int radiusX = Math.max(Math.abs(x), Math.abs(w - x)); + int radiusY = Math.max(Math.abs(y), Math.abs(h - y)); + float radius = (float) Math.hypot(radiusX, radiusY); + + ViewAnimationUtils.createCircularReveal(mColorBg, x, y, 0, radius).setDuration(300).start(); return true; } else { return false; @@ -698,6 +744,8 @@ private void adjustViewVisibility(int state, boolean animation) { int oldState = mState; mState = state; + animation = !TRANSITION_ANIMATION_DISABLED && animation; + switch (state) { case STATE_NORMAL: // Show mMainView @@ -724,14 +772,9 @@ private void adjustViewVisibility(int state, boolean animation) { } if ((oldState == STATE_INIT || oldState == STATE_FAILED || oldState == STATE_REFRESH) && - (state == STATE_NORMAL || state == STATE_REFRESH_HEADER)) { + (state == STATE_NORMAL || state == STATE_REFRESH_HEADER) && AttrResources.getAttrBoolean(getContext2(), R.attr.isLightTheme)) { if (!createCircularReveal()) { - SimpleHandler.getInstance().post(new Runnable() { - @Override - public void run() { - createCircularReveal(); - } - }); + SimpleHandler.getInstance().post(this::createCircularReveal); } } } @@ -766,6 +809,11 @@ private void updateFavoriteDrawable() { if (gd.isFavorited || EhDB.containLocalFavorites(gd.gid)) { mHeart.setVisibility(View.VISIBLE); + if (gd.favoriteName == null) { + mHeart.setText(R.string.local_favorites); + } else { + mHeart.setText(gd.favoriteName); + } mHeartOutline.setVisibility(View.GONE); } else { mHeart.setVisibility(View.GONE); @@ -785,7 +833,7 @@ private void bindViewSecond() { } Resources resources = getResources2(); - Assert.assertNotNull(resources); + AssertUtils.assertNotNull(resources); mThumb.load(EhCacheKeyFactory.getThumbKey(gd.gid), gd.thumb); mTitle.setText(EhUtils.getSuitableTitle(gd)); @@ -809,7 +857,7 @@ private void bindViewSecond() { mTorrent.setText(resources.getString(R.string.torrent_count, gd.torrentCount)); bindTags(gd.tags); - bindComments(gd.comments); + bindComments(gd.comments.comments); bindPreviews(gd); } @@ -830,25 +878,42 @@ private void bindTags(GalleryTagGroup[] tagGroups) { mNoTags.setVisibility(View.GONE); } - int colorTag = resources.getColor(R.color.colorPrimary); - int colorName = resources.getColor(R.color.purple_a400); + EhTagDatabase ehTags = Settings.getShowTagTranslations() ? EhTagDatabase.getInstance(context) : null; + int colorTag = AttrResources.getAttrColor(context, R.attr.tagBackgroundColor); + int colorName = AttrResources.getAttrColor(context, R.attr.tagGroupBackgroundColor); for (GalleryTagGroup tg : tagGroups) { LinearLayout ll = (LinearLayout) inflater.inflate(R.layout.gallery_tag_group, mTags, false); ll.setOrientation(LinearLayout.HORIZONTAL); mTags.addView(ll); + String readableTagName = null; + if (ehTags != null) { + readableTagName = ehTags.getTranslation("n:" + tg.groupName); + } + TextView tgName = (TextView) inflater.inflate(R.layout.item_gallery_tag, ll, false); ll.addView(tgName); - tgName.setText(tg.groupName); + tgName.setText(readableTagName != null ? readableTagName : tg.groupName); tgName.setBackgroundDrawable(new RoundSideRectDrawable(colorName)); + String prefix = EhTagDatabase.namespaceToPrefix(tg.groupName); + if (prefix == null) { + prefix = ""; + } + AutoWrapLayout awl = new AutoWrapLayout(context); ll.addView(awl, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); for (int j = 0, z = tg.size(); j < z; j++) { TextView tag = (TextView) inflater.inflate(R.layout.item_gallery_tag, awl, false); awl.addView(tag); String tagStr = tg.getTagAt(j); - tag.setText(tagStr); + + String readableTag = null; + if (ehTags != null) { + readableTag = ehTags.getTranslation(prefix + tagStr); + } + + tag.setText(readableTag != null ? readableTag : tagStr); tag.setBackgroundDrawable(new RoundSideRectDrawable(colorTag)); tag.setTag(R.id.tag, tg.groupName + ":" + tagStr); tag.setOnClickListener(this); @@ -961,20 +1026,19 @@ private static String getRatingText(float rating, Resources resources) { private String getAllRatingText(float rating, int ratingCount) { Resources resources = getResources2(); - Assert.assertNotNull(resources); + AssertUtils.assertNotNull(resources); return resources.getString(R.string.rating_text, getRatingText(rating, resources), rating, ratingCount); } private void setTransitionName() { long gid = getGid(); - if (gid != -1 && ApiHelper.SUPPORT_TRANSITION && mThumb != null && - mTitle != null && mUploader != null && mCategory != null && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - mThumb.setTransitionName(TransitionNameFactory.getThumbTransitionName(gid)); - mTitle.setTransitionName(TransitionNameFactory.getTitleTransitionName(gid)); - mUploader.setTransitionName(TransitionNameFactory.getUploaderTransitionName(gid)); - mCategory.setTransitionName(TransitionNameFactory.getCategoryTransitionName(gid)); + if (gid != -1 && mThumb != null && + mTitle != null && mUploader != null && mCategory != null) { + ViewCompat.setTransitionName(mThumb, TransitionNameFactory.getThumbTransitionName(gid)); + ViewCompat.setTransitionName(mTitle, TransitionNameFactory.getTitleTransitionName(gid)); + ViewCompat.setTransitionName(mUploader, TransitionNameFactory.getUploaderTransitionName(gid)); + ViewCompat.setTransitionName(mCategory, TransitionNameFactory.getCategoryTransitionName(gid)); } } @@ -984,7 +1048,7 @@ private void ensurePopMenu() { } Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); PopupMenu popup = new PopupMenu(context, mOtherActions, Gravity.TOP); mPopupMenu = popup; popup.getMenuInflater().inflate(R.menu.scene_gallery_detail, popup.getMenu()); @@ -1033,7 +1097,7 @@ private void showSimilarGalleryList() { if (null != keyword) { ListUrlBuilder lub = new ListUrlBuilder(); lub.setMode(ListUrlBuilder.MODE_NORMAL); - lub.setKeyword(keyword); + lub.setKeyword("\"" + keyword + "\""); GalleryListScene.startScene(this, lub); return; } @@ -1123,7 +1187,15 @@ public void onClick(View v) { } else if (mDownload == v) { GalleryInfo galleryInfo = getGalleryInfo(); if (galleryInfo != null) { - CommonOperations.startDownload(activity, galleryInfo, false); + if (EhApplication.getDownloadManager(context).getDownloadState(galleryInfo.gid) == DownloadInfo.STATE_INVALID) { + CommonOperations.startDownload(activity, galleryInfo, false); + } else { + new AlertDialog.Builder(context) + .setTitle(R.string.download_remove_dialog_title) + .setMessage(getString(R.string.download_remove_dialog_message, galleryInfo.title)) + .setPositiveButton(android.R.string.ok, (dialog1, which1) -> EhApplication.getDownloadManager(context).deleteDownload(galleryInfo.gid)) + .show(); + } } } else if (mRead == v) { GalleryInfo galleryInfo = null; @@ -1145,11 +1217,7 @@ public void onClick(View v) { } else if (mHeartGroup == v) { if (mGalleryDetail != null && !mModifingFavorites) { boolean remove = false; - if (EhDB.containLocalFavorites(mGalleryDetail.gid)) { - EhDB.removeLocalFavorites(mGalleryDetail.gid); - remove = true; - } - if (mGalleryDetail.isFavorited) { + if (EhDB.containLocalFavorites(mGalleryDetail.gid) || mGalleryDetail.isFavorited) { mModifingFavorites = true; CommonOperations.removeFromFavorites(activity, mGalleryDetail, new ModifyFavoritesListener(context, @@ -1185,7 +1253,7 @@ public void onClick(View v) { return; } if (mGalleryDetail.apiUid < 0) { - showTip(R.string.sign_in_first, LENGTH_SHORT); + showTip(R.string.sign_in_first, LENGTH_LONG); return; } ArchiveListDialogHelper helper = new ArchiveListDialogHelper(); @@ -1200,7 +1268,7 @@ public void onClick(View v) { return; } if (mGalleryDetail.apiUid < 0) { - showTip(R.string.sign_in_first, LENGTH_SHORT); + showTip(R.string.sign_in_first, LENGTH_LONG); return; } RateDialogHelper helper = new RateDialogHelper(); @@ -1224,7 +1292,7 @@ public void onClick(View v) { args.putString(GalleryCommentsScene.KEY_API_KEY, mGalleryDetail.apiKey); args.putLong(GalleryCommentsScene.KEY_GID, mGalleryDetail.gid); args.putString(GalleryCommentsScene.KEY_TOKEN, mGalleryDetail.token); - args.putParcelableArray(GalleryCommentsScene.KEY_COMMENTS, mGalleryDetail.comments); + args.putParcelable(GalleryCommentsScene.KEY_COMMENT_LIST, mGalleryDetail.comments); startScene(new Announcer(GalleryCommentsScene.class) .setArgs(args) .setRequestCode(this, REQUEST_CODE_COMMENT_GALLERY)); @@ -1259,41 +1327,49 @@ public void onClick(View v) { } } - private void showBlockUploaderDialog() { + private void showFilterUploaderDialog() { Context context = getContext2(); - if (null == context) { - return; - } - final String uploader = getUploader(); - if (null == uploader) { + String uploader = getUploader(); + if (context == null || uploader == null) { return; } new AlertDialog.Builder(context) - .setMessage(getString(R.string.block_the_uploader, uploader)) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (DialogInterface.BUTTON_POSITIVE != which) { - return; - } + .setMessage(getString(R.string.filter_the_uploader, uploader)) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + if (which != DialogInterface.BUTTON_POSITIVE) { + return; + } - Filter filter = new Filter(); - filter.mode = EhFilter.MODE_UPLOADER; - filter.text = uploader; - EhFilter.getInstance().addFilter(filter); + Filter filter = new Filter(); + filter.mode = EhFilter.MODE_UPLOADER; + filter.text = uploader; + EhFilter.getInstance().addFilter(filter); - showTip(R.string.filter_added, LENGTH_SHORT); - } + showTip(R.string.filter_added, LENGTH_SHORT); }).show(); } - private void addTagFilter(String tag) { - Filter filter = new Filter(); - filter.mode = EhFilter.MODE_TAG; - filter.text = tag; - EhFilter.getInstance().addFilter(filter); - showTip(R.string.filter_added, LENGTH_SHORT); + private void showFilterTagDialog(String tag) { + Context context = getContext2(); + if (context == null) { + return; + } + + new AlertDialog.Builder(context) + .setMessage(getString(R.string.filter_the_tag, tag)) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + if (which != DialogInterface.BUTTON_POSITIVE) { + return; + } + + Filter filter = new Filter(); + filter.mode = EhFilter.MODE_TAG; + filter.text = tag; + EhFilter.getInstance().addFilter(filter); + + showTip(R.string.filter_added, LENGTH_SHORT); + }).show(); } private void showTagDialog(final String tag) { @@ -1312,17 +1388,14 @@ private void showTagDialog(final String tag) { new AlertDialog.Builder(context) .setTitle(tag) - .setItems(R.array.tag_menu_entries, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - switch (which) { - case 0: - UrlOpener.openUrl(context, EhUrl.getTagDefinitionUrl(tag2), false); - break; - case 1: - addTagFilter(tag); - break; - } + .setItems(R.array.tag_menu_entries, (dialog, which) -> { + switch (which) { + case 0: + UrlOpener.openUrl(context, EhUrl.getTagDefinitionUrl(tag2), false); + break; + case 1: + showFilterTagDialog(tag); + break; } }).show(); } @@ -1335,7 +1408,7 @@ public boolean onLongClick(View v) { } if (mUploader == v) { - showBlockUploaderDialog(); + showFilterUploaderDialog(); } else if (mDownload == v) { GalleryInfo galleryInfo = getGalleryInfo(); if (galleryInfo != null) { @@ -1354,7 +1427,7 @@ public boolean onLongClick(View v) { @Override public void onBackPressed() { - if (ApiHelper.SUPPORT_TRANSITION && mViewTransition != null && mThumb != null && + if (mViewTransition != null && mThumb != null && mViewTransition.getShownViewIndex() == 0 && mThumb.isShown()) { int[] location = new int[2]; mThumb.getLocationInWindow(location); @@ -1375,16 +1448,12 @@ protected void onSceneResult(int requestCode, int resultCode, Bundle data) { if (resultCode != RESULT_OK || data == null){ break; } - Parcelable[] array = data.getParcelableArray(GalleryCommentsScene.KEY_COMMENTS); - if (!(array instanceof GalleryComment[])) { - break; - } - GalleryComment[] comments = (GalleryComment[]) array; - if (mGalleryDetail == null) { + GalleryCommentList comments = data.getParcelable(GalleryCommentsScene.KEY_COMMENT_LIST); + if (mGalleryDetail == null && comments == null) { break; } mGalleryDetail.comments = comments; - bindComments(comments); + bindComments(comments.comments); break; default: super.onSceneResult(requestCode, resultCode, data); @@ -1485,16 +1554,17 @@ public boolean onTransition(Context context, return false; } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + String transitionName = ViewCompat.getTransitionName(mThumb); + if (transitionName != null) { exit.setSharedElementReturnTransition( TransitionInflater.from(context).inflateTransition(R.transition.trans_move)); exit.setExitTransition( - TransitionInflater.from(context).inflateTransition(android.R.transition.fade)); + TransitionInflater.from(context).inflateTransition(R.transition.trans_fade)); enter.setSharedElementEnterTransition( TransitionInflater.from(context).inflateTransition(R.transition.trans_move)); enter.setEnterTransition( - TransitionInflater.from(context).inflateTransition(android.R.transition.fade)); - transaction.addSharedElement(mThumb, mThumb.getTransitionName()); + TransitionInflater.from(context).inflateTransition(R.transition.trans_fade)); + transaction.addSharedElement(mThumb, transitionName); } return true; } @@ -1533,11 +1603,19 @@ private void onRateGallerySuccess(RateGalleryParser.Result result) { private void onModifyFavoritesSuccess(boolean addOrRemove) { mModifingFavorites = false; if (mGalleryDetail != null) { - mGalleryDetail.isFavorited = !addOrRemove; + mGalleryDetail.isFavorited = !addOrRemove && mGalleryDetail.favoriteName != null; updateFavoriteDrawable(); } } + private void onModifyFavoritesFailure(boolean addOrRemove) { + mModifingFavorites = false; + } + + private void onModifyFavoritesCancel(boolean addOrRemove) { + mModifingFavorites = false; + } + private class ModifyFavoritesListener extends EhCallback { private final boolean mAddOrRemove; @@ -1563,11 +1641,20 @@ public void onSuccess(Void result) { @Override public void onFailure(Exception e) { showTip(mAddOrRemove ? R.string.remove_from_favorite_failure : - R.string.add_to_favorite_failure, LENGTH_SHORT); + R.string.add_to_favorite_failure, LENGTH_LONG); + GalleryDetailScene scene = getScene(); + if (scene != null) { + scene.onModifyFavoritesFailure(mAddOrRemove); + } } @Override - public void onCancel() {} + public void onCancel() { + GalleryDetailScene scene = getScene(); + if (scene != null) { + scene.onModifyFavoritesCancel(mAddOrRemove); + } + } @Override public boolean isInstance(SceneFragment scene) { @@ -1589,9 +1676,9 @@ public void onSuccess(Void result) { @Override public void onFailure(Exception e) { if (e instanceof NoHAtHClientException) { - showTip(R.string.download_archive_failure_no_hath, LENGTH_SHORT); + showTip(R.string.download_archive_failure_no_hath, LENGTH_LONG); } else { - showTip(R.string.download_archive_failure, LENGTH_SHORT); + showTip(R.string.download_archive_failure, LENGTH_LONG); } } @@ -1631,7 +1718,7 @@ public void setDialog(@Nullable Dialog dialog, String url) { mErrorText.setVisibility(View.GONE); mListView.setVisibility(View.GONE); mRequest = new EhRequest().setMethod(EhClient.METHOD_ARCHIVE_LIST) - .setArgs(url) + .setArgs(url, mGid, mToken) .setCallback(this); EhApplication.getEhClient(context).execute(mRequest); } else { @@ -1746,7 +1833,7 @@ public void setDialog(@Nullable Dialog dialog, String url) { mErrorText.setVisibility(View.GONE); mListView.setVisibility(View.GONE); mRequest = new EhRequest().setMethod(EhClient.METHOD_GET_TORRENT_LIST) - .setArgs(url) + .setArgs(url, mGid, mToken) .setCallback(this); EhApplication.getEhClient(context).execute(mRequest); } else { @@ -1789,8 +1876,15 @@ public void onItemClick(AdapterView parent, View view, int position, long id) FileUtils.sanitizeFilename(name + ".torrent")); r.allowScanningByMediaScanner(); r.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + r.addRequestHeader("Cookie", EhApplication.getEhCookieStore(context).getCookieHeader(HttpUrl.get(url))); DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - dm.enqueue(r); + if (dm != null) { + try { + dm.enqueue(r); + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); + } + } } if (mDialog != null) { @@ -1952,7 +2046,7 @@ public void onSuccess(RateGalleryParser.Result result) { @Override public void onFailure(Exception e) { e.printStackTrace(); - showTip(R.string.rate_failed, LENGTH_SHORT); + showTip(R.string.rate_failed, LENGTH_LONG); } @Override diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryHolder.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryHolder.java index 8844cf8e3..925f77eb4 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryHolder.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryHolder.java @@ -16,10 +16,10 @@ package com.hippo.ehviewer.ui.scene; -import android.support.v7.widget.RecyclerView; import android.view.View; +import android.widget.ImageView; import android.widget.TextView; - +import androidx.recyclerview.widget.RecyclerView; import com.hippo.ehviewer.R; import com.hippo.ehviewer.widget.SimpleRatingView; import com.hippo.widget.LoadImageView; @@ -32,17 +32,23 @@ class GalleryHolder extends RecyclerView.ViewHolder { public final SimpleRatingView rating; public final TextView category; public final TextView posted; + public final TextView pages; public final TextView simpleLanguage; + public final ImageView favourited; + public final ImageView downloaded; public GalleryHolder(View itemView) { super(itemView); - thumb = (LoadImageView) itemView.findViewById(R.id.thumb); - title = (TextView) itemView.findViewById(R.id.title); - uploader = (TextView) itemView.findViewById(R.id.uploader); - rating = (SimpleRatingView) itemView.findViewById(R.id.rating); - category = (TextView) itemView.findViewById(R.id.category); - posted = (TextView) itemView.findViewById(R.id.posted); - simpleLanguage = (TextView) itemView.findViewById(R.id.simple_language); + thumb = itemView.findViewById(R.id.thumb); + title = itemView.findViewById(R.id.title); + uploader = itemView.findViewById(R.id.uploader); + rating = itemView.findViewById(R.id.rating); + category = itemView.findViewById(R.id.category); + posted = itemView.findViewById(R.id.posted); + pages = itemView.findViewById(R.id.pages); + simpleLanguage = itemView.findViewById(R.id.simple_language); + favourited = itemView.findViewById(R.id.favourited); + downloaded = itemView.findViewById(R.id.downloaded); } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryInfoScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryInfoScene.java index 2aca00e92..060a0d371 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryInfoScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryInfoScene.java @@ -20,28 +20,30 @@ import android.content.ClipboardManager; import android.content.Context; import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.hippo.android.resource.AttrResources; import com.hippo.easyrecyclerview.EasyRecyclerView; import com.hippo.easyrecyclerview.LinearDividerItemDecoration; import com.hippo.ehviewer.R; +import com.hippo.ehviewer.Settings; +import com.hippo.ehviewer.UrlOpener; import com.hippo.ehviewer.client.EhUrl; import com.hippo.ehviewer.client.EhUtils; import com.hippo.ehviewer.client.data.GalleryDetail; import com.hippo.ripple.Ripple; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.LayoutUtils; import com.hippo.yorozuya.ViewUtils; - -import junit.framework.Assert; - import java.util.ArrayList; public final class GalleryInfoScene extends ToolbarScene implements EasyRecyclerView.OnItemClickListener { @@ -50,6 +52,9 @@ public final class GalleryInfoScene extends ToolbarScene implements EasyRecycler public static final String KEY_KEYS = "keys"; public static final String KEY_VALUES = "values"; + private static final int INDEX_URL = 3; + private static final int INDEX_PARENT = 10; + /*--------------- Whole life cycle ---------------*/ @@ -85,7 +90,7 @@ private void handlerArgs(Bundle args) { } Resources resources = getResources2(); - Assert.assertNotNull(resources); + AssertUtils.assertNotNull(resources); mKeys.add(resources.getString(R.string.header_key)); mValues.add(resources.getString(R.string.header_value)); mKeys.add(resources.getString(R.string.key_gid)); @@ -128,6 +133,8 @@ private void handlerArgs(Bundle args) { mValues.add(Integer.toString(gd.torrentCount)); mKeys.add(resources.getString(R.string.key_torrent_url)); mValues.add(gd.torrentUrl); + mKeys.add(resources.getString(R.string.favorite_name)); + mValues.add(gd.favoriteName); } protected void onInit() { @@ -156,18 +163,19 @@ public View onCreateView3(LayoutInflater inflater, View view = inflater.inflate(R.layout.scene_gallery_info, container, false); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); mRecyclerView = (EasyRecyclerView) ViewUtils.$$(view, R.id.recycler_view); InfoAdapter adapter = new InfoAdapter(); mRecyclerView.setAdapter(adapter); - mRecyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)); + mRecyclerView.setLayoutManager(new LinearLayoutManager(context, RecyclerView.VERTICAL, false)); LinearDividerItemDecoration decoration = new LinearDividerItemDecoration( - LinearDividerItemDecoration.VERTICAL, context.getResources().getColor(R.color.divider), + LinearDividerItemDecoration.VERTICAL, + AttrResources.getAttrColor(context, R.attr.dividerColor), LayoutUtils.dp2pix(context, 1)); decoration.setPadding(context.getResources().getDimensionPixelOffset(R.dimen.keyline_margin)); mRecyclerView.addItemDecoration(decoration); - mRecyclerView.setSelector(Ripple.generateRippleDrawable(context, false)); + mRecyclerView.setSelector(Ripple.generateRippleDrawable(context, !AttrResources.getAttrBoolean(context, R.attr.isLightTheme), new ColorDrawable(Color.TRANSPARENT))); mRecyclerView.setClipToPadding(false); mRecyclerView.setHasFixedSize(true); mRecyclerView.setOnItemClickListener(this); @@ -195,9 +203,19 @@ public void onDestroyView() { public boolean onItemClick(EasyRecyclerView parent, View view, int position, long id) { Context context = getContext2(); if (null != context && 0 != position && null != mValues) { - ClipboardManager cmb = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - cmb.setPrimaryClip(ClipData.newPlainText(null, mValues.get(position))); - showTip(R.string.copied_to_clipboard, LENGTH_SHORT); + if (position == INDEX_PARENT) { + UrlOpener.openUrl(context, mValues.get(position), true); + } else { + ClipboardManager cmb = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + cmb.setPrimaryClip(ClipData.newPlainText(null, mValues.get(position))); + + if (position == INDEX_URL) { + // Save it to avoid detect the gallery + Settings.putClipboardTextHashCode(mValues.get(position).hashCode()); + } + + showTip(R.string.copied_to_clipboard, LENGTH_SHORT); + } return true; } else { return false; @@ -231,7 +249,7 @@ private class InfoAdapter extends RecyclerView.Adapter { public InfoAdapter() { mInflater = getLayoutInflater2(); - Assert.assertNotNull(mInflater); + AssertUtils.assertNotNull(mInflater); } @Override diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java index 59fe97e5e..a3f37c45a 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java @@ -22,17 +22,11 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.res.Resources; +import android.graphics.Color; import android.graphics.Point; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.FloatingActionButton; -import android.support.v4.widget.DrawerLayout; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.Toolbar; import android.text.InputType; import android.text.Spannable; import android.text.SpannableStringBuilder; @@ -49,18 +43,27 @@ import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; - +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.Toolbar; +import androidx.recyclerview.widget.RecyclerView; import com.github.amlcurran.showcaseview.ShowcaseView; import com.github.amlcurran.showcaseview.SimpleShowcaseEventListener; import com.github.amlcurran.showcaseview.targets.PointTarget; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.hippo.android.resource.AttrResources; import com.hippo.app.CheckBoxDialogBuilder; import com.hippo.app.EditTextDialogBuilder; import com.hippo.drawable.AddDeleteDrawable; import com.hippo.drawable.DrawerArrowDrawable; +import com.hippo.drawerlayout.DrawerLayout; import com.hippo.easyrecyclerview.EasyRecyclerView; import com.hippo.easyrecyclerview.FastScroller; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.EhDB; +import com.hippo.ehviewer.FavouriteStatusRouter; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.client.EhClient; @@ -70,17 +73,23 @@ import com.hippo.ehviewer.client.data.GalleryInfo; import com.hippo.ehviewer.client.data.ListUrlBuilder; import com.hippo.ehviewer.client.exception.EhException; +import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser; import com.hippo.ehviewer.client.parser.GalleryListParser; +import com.hippo.ehviewer.client.parser.GalleryPageUrlParser; +import com.hippo.ehviewer.dao.DownloadInfo; import com.hippo.ehviewer.dao.QuickSearch; +import com.hippo.ehviewer.download.DownloadManager; import com.hippo.ehviewer.ui.CommonOperations; +import com.hippo.ehviewer.ui.GalleryActivity; import com.hippo.ehviewer.ui.MainActivity; +import com.hippo.ehviewer.ui.dialog.SelectItemWithIconAdapter; +import com.hippo.ehviewer.widget.GalleryInfoContentHelper; import com.hippo.ehviewer.widget.SearchBar; import com.hippo.ehviewer.widget.SearchLayout; import com.hippo.refreshlayout.RefreshLayout; import com.hippo.ripple.Ripple; import com.hippo.scene.Announcer; import com.hippo.scene.SceneFragment; -import com.hippo.util.ApiHelper; import com.hippo.util.AppHelper; import com.hippo.util.DrawableManager; import com.hippo.view.ViewTransition; @@ -88,16 +97,15 @@ import com.hippo.widget.FabLayout; import com.hippo.widget.SearchBarMover; import com.hippo.yorozuya.AnimationUtils; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.MathUtils; import com.hippo.yorozuya.SimpleAnimatorListener; import com.hippo.yorozuya.StringUtils; import com.hippo.yorozuya.ViewUtils; - -import junit.framework.Assert; - import java.io.File; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Collections; import java.util.List; public final class GalleryListScene extends BaseScene @@ -116,6 +124,7 @@ public final class GalleryListScene extends BaseScene public final static String KEY_ACTION = "action"; public final static String ACTION_HOMEPAGE = "action_homepage"; + public final static String ACTION_SUBSCRIPTION = "action_subscription"; public final static String ACTION_WHATS_HOT = "action_whats_hot"; public final static String ACTION_LIST_URL_BUILDER = "action_list_url_builder"; @@ -171,7 +180,7 @@ public final class GalleryListScene extends BaseScene @Override public void onAnimationEnd(Animator animation) { if (null != mFabLayout) { - mFabLayout.getPrimaryFab().setVisibility(View.INVISIBLE); + ((View) mFabLayout.getPrimaryFab()).setVisibility(View.INVISIBLE); } } }; @@ -216,6 +225,11 @@ public void onScrolled(RecyclerView recyclerView, int dx, int dy) { private ShowcaseView mShowcaseView; + private DownloadManager mDownloadManager; + private DownloadManager.DownloadInfoListener mDownloadInfoListener; + private FavouriteStatusRouter mFavouriteStatusRouter; + private FavouriteStatusRouter.Listener mFavouriteStatusRouterListener; + @Override public int getNavCheckedItem() { return mNavCheckedId; @@ -229,7 +243,11 @@ private void handleArgs(Bundle args) { String action = args.getString(KEY_ACTION); if (ACTION_HOMEPAGE.equals(action)) { mUrlBuilder.reset(); + } else if (ACTION_SUBSCRIPTION.equals(action)) { + mUrlBuilder.reset(); + mUrlBuilder.setMode(ListUrlBuilder.MODE_SUBSCRIPTION); } else if (ACTION_WHATS_HOT.equals(action)) { + mUrlBuilder.reset(); mUrlBuilder.setMode(ListUrlBuilder.MODE_WHATS_HOT); } else if (ACTION_LIST_URL_BUILDER.equals(action)) { ListUrlBuilder builder = args.getParcelable(KEY_LIST_URL_BUILDER); @@ -257,8 +275,53 @@ public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); mClient = EhApplication.getEhClient(context); + mDownloadManager = EhApplication.getDownloadManager(context); + mFavouriteStatusRouter = EhApplication.getFavouriteStatusRouter(context); + + mDownloadInfoListener = new DownloadManager.DownloadInfoListener() { + @Override + public void onAdd(@NonNull DownloadInfo info, @NonNull List list, int position) { + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + } + @Override + public void onUpdate(@NonNull DownloadInfo info, @NonNull List list) { } + @Override + public void onUpdateAll() { } + @Override + public void onReload() { + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + } + @Override + public void onChange() { + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + } + @Override + public void onRenameLabel(String from, String to) { } + @Override + public void onRemove(@NonNull DownloadInfo info, @NonNull List list, int position) { + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + } + @Override + public void onUpdateLabels() { } + }; + mDownloadManager.addDownloadInfoListener(mDownloadInfoListener); + + mFavouriteStatusRouterListener = (gid, slot) -> { + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + }; + mFavouriteStatusRouter.addListener(mFavouriteStatusRouterListener); if (savedInstanceState == null) { onInit(); @@ -300,11 +363,13 @@ public void onDestroy() { mClient = null; mUrlBuilder = null; + mDownloadManager.removeDownloadInfoListener(mDownloadInfoListener); + mFavouriteStatusRouter.removeListener(mFavouriteStatusRouterListener); } private void setSearchBarHint(Context context, SearchBar searchBar) { Resources resources = context.getResources(); - Drawable searchImage = DrawableManager.getDrawable(context, R.drawable.v_magnify_x24); + Drawable searchImage = DrawableManager.getVectorDrawable(context, R.drawable.v_magnify_x24); SpannableStringBuilder ssb = new SpannableStringBuilder(" "); ssb.append(resources.getString(EhUrl.SITE_EX == Settings.getGallerySite() ? R.string.gallery_list_search_bar_hint_exhentai : @@ -317,6 +382,92 @@ private void setSearchBarHint(Context context, SearchBar searchBar) { searchBar.setEditTextHint(ssb); } + private void setSearchBarSuggestionProvider(SearchBar searchBar) { + searchBar.setSuggestionProvider(text -> { + GalleryDetailUrlParser.Result result1 = GalleryDetailUrlParser.parse(text, false); + if (result1 != null) { + return Collections.singletonList(new GalleryDetailUrlSuggestion(result1.gid, result1.token)); + } + GalleryPageUrlParser.Result result2 = GalleryPageUrlParser.parse(text, false); + if (result2 != null) { + return Collections.singletonList(new GalleryPageUrlSuggestion(result2.gid, result2.pToken, result2.page)); + } + return null; + }); + } + + private abstract class UrlSuggestion extends SearchBar.Suggestion { + @Override + public CharSequence getText(float textSize) { + Drawable bookImage = DrawableManager.getVectorDrawable(getContext2(), R.drawable.v_book_open_x24); + SpannableStringBuilder ssb = new SpannableStringBuilder(" "); + ssb.append(getResources2().getString(R.string.gallery_list_search_bar_open_gallery)); + int imageSize = (int) (textSize * 1.25); + if (bookImage != null) { + bookImage.setBounds(0, 0, imageSize, imageSize); + ssb.setSpan(new ImageSpan(bookImage), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + return ssb; + } + + @Override + public void onClick() { + startScene(createAnnouncer()); + + if (mState == STATE_SIMPLE_SEARCH) { + setState(STATE_NORMAL); + } else if (mState == STATE_SEARCH_SHOW_LIST) { + setState(STATE_SEARCH); + } + } + + public abstract Announcer createAnnouncer(); + + @Override + public void onLongClick() { } + } + + private class GalleryDetailUrlSuggestion extends UrlSuggestion { + private long mGid; + private String mToken; + + private GalleryDetailUrlSuggestion(long gid, String token) { + mGid = gid; + mToken = token; + } + + @Override + public Announcer createAnnouncer() { + Bundle args = new Bundle(); + args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN); + args.putLong(GalleryDetailScene.KEY_GID, mGid); + args.putString(GalleryDetailScene.KEY_TOKEN, mToken); + return new Announcer(GalleryDetailScene.class).setArgs(args); + } + } + + private class GalleryPageUrlSuggestion extends UrlSuggestion { + private long mGid; + private String mPToken; + private int mPage; + + private GalleryPageUrlSuggestion(long gid, String pToken, int page) { + mGid = gid; + mPToken = pToken; + mPage = page; + } + + @Override + public Announcer createAnnouncer() { + Bundle args = new Bundle(); + args.putString(ProgressScene.KEY_ACTION, ProgressScene.ACTION_GALLERY_TOKEN); + args.putLong(ProgressScene.KEY_GID, mGid); + args.putString(ProgressScene.KEY_PTOKEN, mPToken); + args.putInt(ProgressScene.KEY_PAGE, mPage); + return new Announcer(ProgressScene.class).setArgs(args); + } + } + @Nullable private static String getSuitableTitleForUrlBuilder( Resources resources, ListUrlBuilder urlBuilder, boolean appName) { @@ -325,8 +476,20 @@ private static String getSuitableTitleForUrlBuilder( if (ListUrlBuilder.MODE_NORMAL == urlBuilder.getMode() && EhUtils.NONE == category && - TextUtils.isEmpty(keyword)) { + TextUtils.isEmpty(keyword) && + urlBuilder.getAdvanceSearch() == -1 && + urlBuilder.getMinRating() == -1 && + urlBuilder.getPageFrom() == -1 && + urlBuilder.getPageTo() == -1) { return resources.getString(appName ? R.string.app_name : R.string.homepage); + } else if (ListUrlBuilder.MODE_SUBSCRIPTION == urlBuilder.getMode() && + EhUtils.NONE == category && + TextUtils.isEmpty(keyword) && + urlBuilder.getAdvanceSearch() == -1 && + urlBuilder.getMinRating() == -1 && + urlBuilder.getPageFrom() == -1 && + urlBuilder.getPageTo() == -1) { + return resources.getString(R.string.subscription); } else if (ListUrlBuilder.MODE_WHATS_HOT == urlBuilder.getMode()) { return resources.getString(R.string.whats_hot); } else if (!TextUtils.isEmpty(keyword)) { @@ -338,19 +501,48 @@ private static String getSuitableTitleForUrlBuilder( } } + private String wrapTagKeyword(String keyword) { + keyword = keyword.trim(); + + int index1 = keyword.indexOf(':'); + if (index1 == -1 || index1 >= keyword.length() - 1) { + // Can't find :, or : is the last char + return keyword; + } + if (keyword.charAt(index1 + 1) == '"') { + // The char after : is ", the word must be quoted + return keyword; + } + int index2 = keyword.indexOf(' '); + if (index2 <= index1) { + // Can't find space, or space is before : + return keyword; + } + + return keyword.substring(0, index1 + 1) + "\"" + keyword.substring(index1 + 1) + "$\""; + } + // Update search bar title, drawer checked item private void onUpdateUrlBuilder() { ListUrlBuilder builder = mUrlBuilder; Resources resources = getResources2(); - if (null == resources || null == builder) { + if (resources == null || builder == null || mSearchLayout == null) { return; } String keyword = builder.getKeyword(); int category = builder.getCategory(); + // Update normal search mode + mSearchLayout.setNormalSearchMode(builder.getMode() == ListUrlBuilder.MODE_SUBSCRIPTION + ? R.id.search_subscription_search + : R.id.search_normal_search); + // Update search edit text if (!TextUtils.isEmpty(keyword) && null != mSearchBar) { + if (builder.getMode() == ListUrlBuilder.MODE_TAG) { + keyword = wrapTagKeyword(keyword); + } mSearchBar.setText(keyword); mSearchBar.cursorToEnd(); } @@ -370,6 +562,8 @@ private void onUpdateUrlBuilder() { EhUtils.NONE == category && TextUtils.isEmpty(keyword)) { checkedItemId = R.id.nav_homepage; + } else if (ListUrlBuilder.MODE_SUBSCRIPTION == builder.getMode()) { + checkedItemId = R.id.nav_subscription; } else if (ListUrlBuilder.MODE_WHATS_HOT == builder.getMode()) { checkedItemId = R.id.nav_whats_hot; } else { @@ -386,7 +580,7 @@ public View onCreateView2(LayoutInflater inflater, @Nullable ViewGroup container View view = inflater.inflate(R.layout.scene_gallery_list, container, false); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); Resources resources = context.getResources(); mHideActionFabSlop = ViewConfiguration.get(context).getScaledTouchSlop(); @@ -408,15 +602,13 @@ public View onCreateView2(LayoutInflater inflater, @Nullable ViewGroup container mViewTransition = new ViewTransition(contentLayout, mSearchLayout); mHelper = new GalleryListHelper(); - mHelper.setEmptyString(resources.getString(R.string.gallery_list_empty_hit)); contentLayout.setHelper(mHelper); contentLayout.getFastScroller().setOnDragHandlerListener(this); mAdapter = new GalleryListAdapter(inflater, resources, mRecyclerView, Settings.getListMode()); - mRecyclerView.setSelector(Ripple.generateRippleDrawable(context, false)); + mRecyclerView.setSelector(Ripple.generateRippleDrawable(context, !AttrResources.getAttrBoolean(context, R.attr.isLightTheme), new ColorDrawable(Color.TRANSPARENT))); mRecyclerView.setDrawSelectorOnTop(true); - mRecyclerView.hasFixedSize(); mRecyclerView.setClipToPadding(false); mRecyclerView.setOnItemClickListener(this); mRecyclerView.setOnItemLongClickListener(this); @@ -427,13 +619,14 @@ public View onCreateView2(LayoutInflater inflater, @Nullable ViewGroup container refreshLayout.setHeaderTranslationY(paddingTopSB); - mLeftDrawable = new DrawerArrowDrawable(context); - mRightDrawable = new AddDeleteDrawable(context); + mLeftDrawable = new DrawerArrowDrawable(context, AttrResources.getAttrColor(context, R.attr.drawableColorPrimary)); + mRightDrawable = new AddDeleteDrawable(context, AttrResources.getAttrColor(context, R.attr.drawableColorPrimary)); mSearchBar.setLeftDrawable(mLeftDrawable); mSearchBar.setRightDrawable(mRightDrawable); mSearchBar.setHelper(this); mSearchBar.setOnStateChangeListener(this); setSearchBarHint(context, mSearchBar); + setSearchBarSuggestionProvider(mSearchBar); mSearchLayout.setHelper(this); mSearchLayout.setPadding(mSearchLayout.getPaddingLeft(), mSearchLayout.getPaddingTop() + paddingTopSB, @@ -446,8 +639,7 @@ public View onCreateView2(LayoutInflater inflater, @Nullable ViewGroup container mFabLayout.setOnExpandListener(this); addAboveSnackView(mFabLayout); - mActionFabDrawable = new AddDeleteDrawable(context); - mActionFabDrawable.setColor(resources.getColor(R.color.primary_drawable_dark)); + mActionFabDrawable = new AddDeleteDrawable(context, resources.getColor(R.color.primary_drawable_dark)); mFabLayout.getPrimaryFab().setImageDrawable(mActionFabDrawable); mSearchFab.setOnClickListener(this); @@ -515,6 +707,7 @@ public void onDestroyView() { mSearchBarMover = null; } if (null != mHelper) { + mHelper.destroy(); if (1 == mHelper.getShownViewIndex()) { mHasFirstRefresh = false; } @@ -568,14 +761,14 @@ private void showAddQuickSearchDialog(final List list, // Can't add image search as quick search if (ListUrlBuilder.MODE_IMAGE_SEARCH == urlBuilder.getMode()) { - showTip(R.string.image_search_not_quick_search, LENGTH_SHORT); + showTip(R.string.image_search_not_quick_search, LENGTH_LONG); return; } // Check duplicate for (QuickSearch q: list) { if (urlBuilder.equalsQuickSearch(q)) { - showTip(getString(R.string.duplicate_quick_search, q.name), LENGTH_SHORT); + showTip(getString(R.string.duplicate_quick_search, q.name), LENGTH_LONG); return; } } @@ -632,7 +825,7 @@ public View onCreateDrawerView(LayoutInflater inflater, @Nullable ViewGroup cont final ListView listView = (ListView) ViewUtils.$$(view, R.id.list_view); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); final List list = EhDB.getAllQuickSearch(); final ArrayAdapter adapter = new ArrayAdapter<>(context, R.layout.item_simple_list, list); @@ -745,13 +938,17 @@ public boolean onItemClick(EasyRecyclerView parent, View view, int position, lon return false; } - GalleryInfo gi = mHelper.getDataAt(position); + GalleryInfo gi = mHelper.getDataAtEx(position); + if (gi == null) { + return true; + } + Bundle args = new Bundle(); args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO); args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi); Announcer announcer = new Announcer(GalleryDetailScene.class).setArgs(args); View thumb; - if (ApiHelper.SUPPORT_TRANSITION && null != (thumb = view.findViewById(R.id.thumb))) { + if (null != (thumb = view.findViewById(R.id.thumb))) { announcer.setTranHelper(new EnterGalleryDetailTransaction(thumb)); } startScene(announcer); @@ -762,6 +959,7 @@ public boolean onItemClick(EasyRecyclerView parent, View view, int position, lon public void onClick(View v) { if (STATE_NORMAL != mState && null != mSearchBar) { mSearchBar.applySearch(); + hideSoftInput(); } } @@ -859,22 +1057,54 @@ public boolean onItemLongClick(EasyRecyclerView parent, View view, int position, return false; } - final GalleryInfo gi = mHelper.getDataAt(position); + final GalleryInfo gi = mHelper.getDataAtEx(position); + if (gi == null) { + return true; + } + + boolean downloaded = mDownloadManager.getDownloadState(gi.gid) != DownloadInfo.STATE_INVALID; + boolean favourited = gi.favoriteSlot != -2; + + CharSequence[] items = new CharSequence[] { + context.getString(R.string.read), + context.getString(downloaded ? R.string.delete_downloads : R.string.download), + context.getString(favourited ? R.string.remove_from_favourites : R.string.add_to_favourites), + }; + + int[] icons = new int[] { + R.drawable.v_book_open_x24, + downloaded ? R.drawable.v_delete_x24 : R.drawable.v_download_x24, + favourited ? R.drawable.v_heart_broken_x24 : R.drawable.v_heart_x24, + }; + new AlertDialog.Builder(context) .setTitle(EhUtils.getSuitableTitle(gi)) - .setItems(R.array.gallery_list_menu_entries, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - switch (which) { - case 0: // Download + .setAdapter(new SelectItemWithIconAdapter(context, items, icons), (dialog, which) -> { + switch (which) { + case 0: // Read + Intent intent = new Intent(activity, GalleryActivity.class); + intent.setAction(GalleryActivity.ACTION_EH); + intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, gi); + startActivity(intent); + break; + case 1: // Download + if (downloaded) { + new AlertDialog.Builder(context) + .setTitle(R.string.download_remove_dialog_title) + .setMessage(getString(R.string.download_remove_dialog_message, gi.title)) + .setPositiveButton(android.R.string.ok, (dialog1, which1) -> mDownloadManager.deleteDownload(gi.gid)) + .show(); + } else { CommonOperations.startDownload(activity, gi, false); - break; - case 1: // Favorites - CommonOperations.addToFavorites(activity, gi, - new addToFavoriteListener(context, - activity.getStageId(), getTag())); - break; - } + } + break; + case 2: // Favorites + if (favourited) { + CommonOperations.removeFromFavorites(activity, gi, new RemoveFromFavoriteListener(context, activity.getStageId(), getTag())); + } else { + CommonOperations.addToFavorites(activity, gi, new AddToFavoriteListener(context, activity.getStageId(), getTag())); + } + break; } }).show(); return true; @@ -1106,39 +1336,20 @@ public void onApplySearch(String query) { } if (mState == STATE_SEARCH || mState == STATE_SEARCH_SHOW_LIST) { - if (mSearchLayout.isSpecifyGallery()) { - int index = query.indexOf(' '); - if (index <= 0 || index >= query.length() - 1) { - showTip(R.string.error_invalid_specify_gallery, LENGTH_LONG); - return; - } - - long gid; - String token; - try { - gid = Long.parseLong(query.substring(0, index)); - } catch (NumberFormatException e) { - showTip(R.string.error_invalid_specify_gallery, LENGTH_LONG); - return; - } - token = query.substring(index + 1); - - Bundle args = new Bundle(); - args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN); - args.putLong(GalleryDetailScene.KEY_GID, gid); - args.putString(GalleryDetailScene.KEY_TOKEN, token); - startScene(new Announcer(GalleryDetailScene.class).setArgs(args)); + try { + mSearchLayout.formatListUrlBuilder(mUrlBuilder, query); + } catch (EhException e) { + showTip(e.getMessage(), LENGTH_LONG); return; - } else { - try { - mSearchLayout.formatListUrlBuilder(mUrlBuilder, query); - } catch (EhException e) { - showTip(e.getMessage(), LENGTH_LONG); - return; - } } } else { + int oldMode = mUrlBuilder.getMode(); + // If it's MODE_SUBSCRIPTION, keep it + int newMode = oldMode == ListUrlBuilder.MODE_SUBSCRIPTION + ? ListUrlBuilder.MODE_SUBSCRIPTION + : ListUrlBuilder.MODE_NORMAL; mUrlBuilder.reset(); + mUrlBuilder.setMode(newMode); mUrlBuilder.setKeyword(query); } onUpdateUrlBuilder(); @@ -1267,7 +1478,7 @@ private class GalleryListAdapter extends GalleryAdapter { public GalleryListAdapter(@NonNull LayoutInflater inflater, @NonNull Resources resources, @NonNull RecyclerView recyclerView, int type) { - super(inflater, resources, recyclerView, type); + super(inflater, resources, recyclerView, type, true); } @Override @@ -1278,11 +1489,11 @@ public int getItemCount() { @Nullable @Override public GalleryInfo getDataAt(int position) { - return null != mHelper ? mHelper.getDataAt(position) : null; + return null != mHelper ? mHelper.getDataAtEx(position) : null; } } - private class GalleryListHelper extends ContentLayout.ContentHelper { + private class GalleryListHelper extends GalleryInfoContentHelper { @Override protected void getPageData(int taskId, int type, int page) { @@ -1292,14 +1503,7 @@ protected void getPageData(int taskId, int type, int page) { } mUrlBuilder.setPageIndex(page); - if (ListUrlBuilder.MODE_WHATS_HOT == mUrlBuilder.getMode()) { - EhRequest request = new EhRequest(); - request.setMethod(EhClient.METHOD_GET_WHATS_HOT); - request.setCallback(new GetWhatsHotListener(getContext(), - activity.getStageId(), getTag(), taskId)); - request.setArgs(); - mClient.execute(request); - } else if (ListUrlBuilder.MODE_IMAGE_SEARCH == mUrlBuilder.getMode()) { + if (ListUrlBuilder.MODE_IMAGE_SEARCH == mUrlBuilder.getMode()) { EhRequest request = new EhRequest(); request.setMethod(EhClient.METHOD_IMAGE_SEARCH); request.setCallback(new GetGalleryListListener(getContext(), @@ -1353,6 +1557,11 @@ public void onShowView(View hiddenView, View shownView) { showActionFab(); } + @Override + protected boolean isDuplicate(GalleryInfo d1, GalleryInfo d2) { + return d1.gid == d2.gid; + } + @Override protected void onScrollToPosition(int postion) { if (0 == postion) { @@ -1367,8 +1576,11 @@ protected void onScrollToPosition(int postion) { private void onGetGalleryListSuccess(GalleryListParser.Result result, int taskId) { if (mHelper != null && mSearchBarMover != null && mHelper.isCurrentTask(taskId)) { - mHelper.setPages(taskId, result.pages); - mHelper.onGetPageData(taskId, result.galleryInfoList); + String emptyString = getResources2().getString(mUrlBuilder.getMode() == ListUrlBuilder.MODE_SUBSCRIPTION && result.noWatchedTags + ? R.string.gallery_list_empty_hit_subscription + : R.string.gallery_list_empty_hit); + mHelper.setEmptyString(emptyString); + mHelper.onGetPageData(taskId, result.pages, result.nextPage, result.galleryInfoList); } } @@ -1379,35 +1591,20 @@ private void onGetGalleryListFailure(Exception e, int taskId) { } } - private void onGetWhatsHotSuccess(List result, int taskId) { - if (mHelper != null && mSearchBarMover != null && - mHelper.isCurrentTask(taskId)) { - mHelper.setPages(taskId, 1); - mHelper.onGetPageData(taskId, result); - } - } - - private void onGetWhatsHotFailure(Exception e, int taskId) { - if (mHelper != null && mSearchBarMover != null && - mHelper.isCurrentTask(taskId)) { - mHelper.onGetException(taskId, e); - } - } - - private static class GetWhatsHotListener extends EhCallback> { + private static class GetGalleryListListener extends EhCallback { private final int mTaskId; - public GetWhatsHotListener(Context context, int stageId, String sceneTag, int taskId) { + public GetGalleryListListener(Context context, int stageId, String sceneTag, int taskId) { super(context, stageId, sceneTag); mTaskId = taskId; } @Override - public void onSuccess(List result) { + public void onSuccess(GalleryListParser.Result result) { GalleryListScene scene = getScene(); if (scene != null) { - scene.onGetWhatsHotSuccess(result, mTaskId); + scene.onGetGalleryListSuccess(result, mTaskId); } } @@ -1415,7 +1612,7 @@ public void onSuccess(List result) { public void onFailure(Exception e) { GalleryListScene scene = getScene(); if (scene != null) { - scene.onGetWhatsHotFailure(e, mTaskId); + scene.onGetGalleryListFailure(e, mTaskId); } } @@ -1428,29 +1625,20 @@ public boolean isInstance(SceneFragment scene) { } } - private static class GetGalleryListListener extends EhCallback { + private static class AddToFavoriteListener extends EhCallback { - private final int mTaskId; - - public GetGalleryListListener(Context context, int stageId, String sceneTag, int taskId) { + public AddToFavoriteListener(Context context, int stageId, String sceneTag) { super(context, stageId, sceneTag); - mTaskId = taskId; } @Override - public void onSuccess(GalleryListParser.Result result) { - GalleryListScene scene = getScene(); - if (scene != null) { - scene.onGetGalleryListSuccess(result, mTaskId); - } + public void onSuccess(Void result) { + showTip(R.string.add_to_favorite_success, LENGTH_SHORT); } @Override public void onFailure(Exception e) { - GalleryListScene scene = getScene(); - if (scene != null) { - scene.onGetGalleryListFailure(e, mTaskId); - } + showTip(R.string.add_to_favorite_failure, LENGTH_LONG); } @Override @@ -1462,20 +1650,20 @@ public boolean isInstance(SceneFragment scene) { } } - private static class addToFavoriteListener extends EhCallback { + private static class RemoveFromFavoriteListener extends EhCallback { - public addToFavoriteListener(Context context, int stageId, String sceneTag) { + public RemoveFromFavoriteListener(Context context, int stageId, String sceneTag) { super(context, stageId, sceneTag); } @Override public void onSuccess(Void result) { - showTip(R.string.add_to_favorite_success, LENGTH_SHORT); + showTip(R.string.remove_from_favorite_success, LENGTH_SHORT); } @Override public void onFailure(Exception e) { - showTip(R.string.add_to_favorite_failure, LENGTH_SHORT); + showTip(R.string.remove_from_favorite_failure, LENGTH_LONG); } @Override diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryPreviewsScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryPreviewsScene.java index fcef5e2de..81b6aa9af 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryPreviewsScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryPreviewsScene.java @@ -23,17 +23,16 @@ import android.content.Intent; import android.content.res.Resources; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.RecyclerView; import android.util.Pair; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.RecyclerView; import com.hippo.easyrecyclerview.EasyRecyclerView; import com.hippo.easyrecyclerview.MarginItemDecoration; import com.hippo.ehviewer.EhApplication; @@ -53,11 +52,9 @@ import com.hippo.widget.LoadImageView; import com.hippo.widget.Slider; import com.hippo.widget.recyclerview.AutoGridLayoutManager; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.LayoutUtils; import com.hippo.yorozuya.ViewUtils; - -import junit.framework.Assert; - import java.util.ArrayList; import java.util.Locale; @@ -91,7 +88,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); mClient = EhApplication.getEhClient(context); if (savedInstanceState == null) { onInit(); @@ -138,7 +135,7 @@ public View onCreateView3(LayoutInflater inflater, mRecyclerView = contentLayout.getRecyclerView(); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); Resources resources = context.getResources(); mAdapter = new GalleryPreviewAdapter(); @@ -232,12 +229,14 @@ public void onNavigationClick() { public boolean onItemClick(EasyRecyclerView parent, View view, int position, long id) { Context context = getContext2(); if (null != context && null != mHelper && null != mGalleryInfo) { - GalleryPreview p = mHelper.getDataAt(position); - Intent intent = new Intent(context, GalleryActivity.class); - intent.setAction(GalleryActivity.ACTION_EH); - intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, mGalleryInfo); - intent.putExtra(GalleryActivity.KEY_PAGE, p.getPosition()); - startActivity(intent); + GalleryPreview p = mHelper.getDataAtEx(position); + if (p != null) { + Intent intent = new Intent(context, GalleryActivity.class); + intent.setAction(GalleryActivity.ACTION_EH); + intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, mGalleryInfo); + intent.putExtra(GalleryActivity.KEY_PAGE, p.getPosition()); + startActivity(intent); + } } return true; } @@ -261,7 +260,7 @@ private class GalleryPreviewAdapter extends RecyclerView.Adapter result, int taskId) { @@ -340,8 +346,7 @@ private void onGetPreviewSetSuccess(Pair result, int taskId list.add(previewSet.getGalleryPreview(mGalleryInfo.gid, i)); } - mHelper.setPages(taskId, result.second); - mHelper.onGetPageData(taskId, list); + mHelper.onGetPageData(taskId, result.second, 0, list); } } @@ -430,7 +435,7 @@ public void onClick(View v) { mDialog = null; } } else { - showTip(R.string.error_out_of_range, LENGTH_SHORT); + showTip(R.string.error_out_of_range, LENGTH_LONG); } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/HistoryScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/HistoryScene.java index f15eea519..19866e366 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/HistoryScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/HistoryScene.java @@ -19,19 +19,21 @@ import android.content.Context; import android.content.DialogInterface; import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.StaggeredGridLayoutManager; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator; import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator; import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager; @@ -42,6 +44,7 @@ import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem; import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager; import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractSwipeableItemViewHolder; +import com.hippo.android.resource.AttrResources; import com.hippo.easyrecyclerview.EasyRecyclerView; import com.hippo.easyrecyclerview.FastScroller; import com.hippo.easyrecyclerview.HandlerDrawable; @@ -59,16 +62,12 @@ import com.hippo.ripple.Ripple; import com.hippo.scene.Announcer; import com.hippo.scene.SceneFragment; -import com.hippo.util.ApiHelper; import com.hippo.util.DrawableManager; import com.hippo.view.ViewTransition; import com.hippo.widget.LoadImageView; import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager; -import com.hippo.yorozuya.ResourcesUtils; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.ViewUtils; - -import junit.framework.Assert; - import de.greenrobot.dao.query.LazyList; public class HistoryScene extends ToolbarScene @@ -104,10 +103,10 @@ public View onCreateView3(LayoutInflater inflater, mViewTransition = new ViewTransition(content, tip); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); Resources resources = context.getResources(); - Drawable drawable = DrawableManager.getDrawable(context, R.drawable.big_history); + Drawable drawable = DrawableManager.getVectorDrawable(context, R.drawable.big_history); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); tip.setCompoundDrawables(null, drawable, null, null); @@ -127,9 +126,8 @@ public View onCreateView3(LayoutInflater inflater, layoutManager.setColumnSize(resources.getDimensionPixelOffset(Settings.getDetailSizeResId())); layoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_MIN_SIZE); mRecyclerView.setLayoutManager(layoutManager); - mRecyclerView.setSelector(Ripple.generateRippleDrawable(context, false)); + mRecyclerView.setSelector(Ripple.generateRippleDrawable(context, !AttrResources.getAttrBoolean(context, R.attr.isLightTheme), new ColorDrawable(Color.TRANSPARENT))); mRecyclerView.setDrawSelectorOnTop(true); - mRecyclerView.hasFixedSize(); mRecyclerView.setClipToPadding(false); mRecyclerView.setOnItemClickListener(this); mRecyclerView.setOnItemLongClickListener(this); @@ -144,7 +142,7 @@ public View onCreateView3(LayoutInflater inflater, fastScroller.attachToRecyclerView(mRecyclerView); HandlerDrawable handlerDrawable = new HandlerDrawable(); - handlerDrawable.setColor(ResourcesUtils.getAttrColor(context, R.attr.colorAccent)); + handlerDrawable.setColor(AttrResources.getAttrColor(context, R.attr.widgetColorThemeAccent)); fastScroller.setHandlerDrawable(handlerDrawable); updateLazyList(); @@ -167,6 +165,9 @@ public void onDestroyView() { if (null != mLazyList) { mLazyList.close(); mLazyList = null; + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } } if (null != mRecyclerView) { mRecyclerView.stopScroll(); @@ -255,7 +256,7 @@ public boolean onItemClick(EasyRecyclerView parent, View view, int position, lon args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, mLazyList.get(position)); Announcer announcer = new Announcer(GalleryDetailScene.class).setArgs(args); View thumb; - if (ApiHelper.SUPPORT_TRANSITION && null != (thumb = view.findViewById(R.id.thumb))) { + if (null != (thumb = view.findViewById(R.id.thumb))) { announcer.setTranHelper(new EnterGalleryDetailTransaction(thumb)); } startScene(announcer); @@ -293,6 +294,7 @@ public void onClick(DialogInterface dialog, int which) { private class HistoryHolder extends AbstractSwipeableItemViewHolder { + public final View card; public final LoadImageView thumb; public final TextView title; public final TextView uploader; @@ -304,6 +306,7 @@ private class HistoryHolder extends AbstractSwipeableItemViewHolder { public HistoryHolder(View itemView) { super(itemView); + card = itemView.findViewById(R.id.card); thumb = (LoadImageView) itemView.findViewById(R.id.thumb); title = (TextView) itemView.findViewById(R.id.title); uploader = (TextView) itemView.findViewById(R.id.uploader); @@ -315,7 +318,7 @@ public HistoryHolder(View itemView) { @Override public View getSwipeableContainerView() { - return itemView; + return card; } } @@ -323,9 +326,16 @@ private class HistoryAdapter extends RecyclerView.Adapter implements SwipeableItemAdapter { private final LayoutInflater mInflater; + private final int mListThumbWidth; + private final int mListThumbHeight; public HistoryAdapter() { mInflater = getLayoutInflater2(); + + View calculator = mInflater.inflate(R.layout.item_gallery_list_thumb_height, null); + ViewUtils.measureView(calculator, 1024, ViewGroup.LayoutParams.WRAP_CONTENT); + mListThumbHeight = calculator.getMeasuredHeight(); + mListThumbWidth = mListThumbHeight * 2 / 3; } @Override @@ -339,7 +349,14 @@ public long getItemId(int position) { @Override public HistoryHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new HistoryHolder(mInflater.inflate(R.layout.item_gallery_list, parent, false)); + HistoryHolder holder = new HistoryHolder(mInflater.inflate(R.layout.item_history, parent, false)); + + ViewGroup.LayoutParams lp = holder.thumb.getLayoutParams(); + lp.width = mListThumbWidth; + lp.height = mListThumbHeight; + holder.thumb.setLayoutParams(lp); + + return holder; } @Override @@ -365,7 +382,7 @@ public void onBindViewHolder(HistoryHolder holder, int position) { // Update transition name if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { long gid = gi.gid; - holder.thumb.setTransitionName(TransitionNameFactory.getThumbTransitionName(gid)); + ViewCompat.setTransitionName(holder.thumb, TransitionNameFactory.getThumbTransitionName(gid)); } } @@ -376,9 +393,12 @@ public int getItemCount() { @Override public int onGetSwipeReactionType(HistoryHolder holder, int position, int x, int y) { - return SwipeableItemConstants.REACTION_CAN_SWIPE_BOTH_H; + return SwipeableItemConstants.REACTION_CAN_SWIPE_LEFT; } + @Override + public void onSwipeItemStarted(HistoryHolder holder, int position) { } + @Override public void onSetSwipeBackground(HistoryHolder holder, int position, int type) {} @@ -386,8 +406,8 @@ public void onSetSwipeBackground(HistoryHolder holder, int position, int type) { public SwipeResultAction onSwipeItem(HistoryHolder holder, int position, int result) { switch (result) { case SwipeableItemConstants.RESULT_SWIPED_LEFT: - case SwipeableItemConstants.RESULT_SWIPED_RIGHT: return new SwipeResultActionClear(position); + case SwipeableItemConstants.RESULT_SWIPED_RIGHT: case SwipeableItemConstants.RESULT_CANCELED: default: return new SwipeResultActionDefault(); @@ -413,7 +433,7 @@ protected void onPerformAction() { HistoryInfo info = mLazyList.get(mPosition); EhDB.deleteHistoryInfo(info); updateLazyList(); - mAdapter.notifyItemRemoved(mPosition); + mAdapter.notifyDataSetChanged(); updateView(true); } } @@ -431,7 +451,7 @@ public void onSuccess(Void result) { @Override public void onFailure(Exception e) { - showTip(R.string.add_to_favorite_failure, LENGTH_SHORT); + showTip(R.string.add_to_favorite_failure, LENGTH_LONG); } @Override diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/ProgressScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/ProgressScene.java index b286ed08c..e9252ce43 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/ProgressScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/ProgressScene.java @@ -19,13 +19,12 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.R; import com.hippo.ehviewer.client.EhClient; @@ -36,10 +35,9 @@ import com.hippo.util.DrawableManager; import com.hippo.util.ExceptionUtils; import com.hippo.view.ViewTransition; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.ViewUtils; -import junit.framework.Assert; - /** * Only show a progress with jobs in background */ @@ -170,9 +168,9 @@ public View onCreateView2(LayoutInflater inflater, mTip = (TextView) ViewUtils.$$(view, R.id.tip); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); - Drawable drawable = DrawableManager.getDrawable(context, R.drawable.big_weird_face); + Drawable drawable = DrawableManager.getVectorDrawable(context, R.drawable.big_sad_pandroid); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); mTip.setCompoundDrawables(null, drawable, null, null); mTip.setOnClickListener(this); diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/QuickSearchScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/QuickSearchScene.java index f187304fc..848a43f33 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/QuickSearchScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/QuickSearchScene.java @@ -21,38 +21,28 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.NinePatchDrawable; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.h6ah4i.android.widget.advrecyclerview.animator.DraggableItemAnimator; import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator; -import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator; import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemAdapter; import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange; import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultAction; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionDefault; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionMoveToSwipedDirection; -import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager; -import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractDraggableSwipeableItemViewHolder; +import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractDraggableItemViewHolder; import com.hippo.easyrecyclerview.EasyRecyclerView; import com.hippo.ehviewer.EhDB; import com.hippo.ehviewer.R; import com.hippo.ehviewer.dao.QuickSearch; import com.hippo.util.DrawableManager; import com.hippo.view.ViewTransition; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.ViewUtils; - -import junit.framework.Assert; - import java.util.List; public final class QuickSearchScene extends ToolbarScene { @@ -97,36 +87,28 @@ public View onCreateView3(LayoutInflater inflater, mViewTransition = new ViewTransition(mRecyclerView, tip); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); - Drawable drawable = DrawableManager.getDrawable(context, R.drawable.big_search); + Drawable drawable = DrawableManager.getVectorDrawable(context, R.drawable.big_search); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); tip.setCompoundDrawables(null, drawable, null, null); tip.setText(R.string.no_quick_search); - // touch guard manager (this class is required to suppress scrolling while swipe-dismiss animation is running) - RecyclerViewTouchActionGuardManager guardManager = new RecyclerViewTouchActionGuardManager(); - guardManager.setInterceptVerticalScrollingWhileAnimationRunning(true); - guardManager.setEnabled(true); // drag & drop manager RecyclerViewDragDropManager dragDropManager = new RecyclerViewDragDropManager(); dragDropManager.setDraggingItemShadowDrawable( (NinePatchDrawable) context.getResources().getDrawable(R.drawable.shadow_8dp)); - // swipe manager - RecyclerViewSwipeManager swipeManager = new RecyclerViewSwipeManager(); + RecyclerView.Adapter adapter = new QuickSearchAdapter(); adapter.setHasStableIds(true); adapter = dragDropManager.createWrappedAdapter(adapter); // wrap for dragging - adapter = swipeManager.createWrappedAdapter(adapter); // wrap for swiping mAdapter = adapter; - final GeneralItemAnimator animator = new SwipeDismissItemAnimator(); - animator.setSupportsChangeAnimations(false); - mRecyclerView.hasFixedSize(); + + final GeneralItemAnimator animator = new DraggableItemAnimator(); mRecyclerView.setLayoutManager(new LinearLayoutManager(context)); mRecyclerView.setAdapter(adapter); mRecyclerView.setItemAnimator(animator); - guardManager.attachRecyclerView(mRecyclerView); - swipeManager.attachRecyclerView(mRecyclerView); + dragDropManager.attachRecyclerView(mRecyclerView); updateView(); @@ -151,7 +133,6 @@ public void onDestroyView() { } mViewTransition = null; - mAdapter = null; } @Override @@ -169,41 +150,66 @@ private void updateView() { } } - private class QuickSearchHolder extends AbstractDraggableSwipeableItemViewHolder { + private class QuickSearchHolder extends AbstractDraggableItemViewHolder implements View.OnClickListener { - public final View swipeHandler; public final TextView label; public final View dragHandler; + public final View delete; public QuickSearchHolder(View itemView) { super(itemView); - swipeHandler = ViewUtils.$$(itemView, R.id.swipe_handler); label = (TextView) ViewUtils.$$(itemView, R.id.label); dragHandler = ViewUtils.$$(itemView, R.id.drag_handler); + delete = ViewUtils.$$(itemView, R.id.delete); + + delete.setOnClickListener(this); } @Override - public View getSwipeableContainerView() { - return swipeHandler; + public void onClick(View v) { + int position = getAdapterPosition(); + Context context = getContext2(); + if (position == RecyclerView.NO_POSITION || mQuickSearchList == null) { + return; + } + + final QuickSearch quickSearch = mQuickSearchList.get(position); + new AlertDialog.Builder(context) + .setTitle(R.string.delete_quick_search_title) + .setMessage(getString(R.string.delete_quick_search_message, quickSearch.name)) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + EhDB.deleteQuickSearch(quickSearch); + mQuickSearchList.remove(position); + } + }) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + if (null != mAdapter) { + mAdapter.notifyDataSetChanged(); + } + updateView(); + } + }).show(); } } - private class QuickSearchAdapter extends RecyclerView.Adapter - implements DraggableItemAdapter, - SwipeableItemAdapter { + implements DraggableItemAdapter { private final LayoutInflater mInflater; public QuickSearchAdapter() { mInflater = getLayoutInflater2(); - Assert.assertNotNull(mInflater); + AssertUtils.assertNotNull(mInflater); } @Override public QuickSearchHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new QuickSearchHolder(mInflater.inflate(R.layout.item_label_list, parent, false)); + return new QuickSearchHolder(mInflater.inflate(R.layout.item_quick_search, parent, false)); } @Override @@ -211,7 +217,6 @@ public void onBindViewHolder(QuickSearchHolder holder, int position) { if (mQuickSearchList != null) { holder.label.setText(mQuickSearchList.get(position).name); } - holder.setSwipeItemHorizontalSlideAmount(0); } @Override @@ -246,7 +251,6 @@ public void onMoveItem(int fromPosition, int toPosition) { EhDB.moveQuickSearch(fromPosition, toPosition); final QuickSearch item = mQuickSearchList.remove(fromPosition); mQuickSearchList.add(toPosition, item); - notifyItemMoved(fromPosition, toPosition); } @Override @@ -255,71 +259,9 @@ public boolean onCheckCanDrop(int draggingPosition, int dropPosition) { } @Override - public int onGetSwipeReactionType(QuickSearchHolder holder, int position, int x, int y) { - if (ViewUtils.isViewUnder(holder.getSwipeableContainerView(), x, y, 0)) { - return SwipeableItemConstants.REACTION_CAN_SWIPE_LEFT; - } else { - return SwipeableItemConstants.REACTION_CAN_NOT_SWIPE_BOTH_H; - } - } - - @Override - public void onSetSwipeBackground(QuickSearchHolder holder, int position, int type) {} + public void onItemDragStarted(int position) { } @Override - public SwipeResultAction onSwipeItem(QuickSearchHolder holder, int position, int result) { - switch (result) { - // swipe left --- pin - case SwipeableItemConstants.RESULT_SWIPED_LEFT: - return new SwipeLeftResultAction(position); - // other --- do nothing - case SwipeableItemConstants.RESULT_SWIPED_RIGHT: - case SwipeableItemConstants.RESULT_CANCELED: - default: - return new SwipeResultActionDefault(); - } - } - } - - private class SwipeLeftResultAction extends SwipeResultActionMoveToSwipedDirection { - - private final int mPosition; - - public SwipeLeftResultAction(int position) { - mPosition = position; - } - - @Override - protected void onPerformAction() { - super.onPerformAction(); - - Context context = getContext2(); - final List quickSearchList = mQuickSearchList; - if (null == context || null == quickSearchList || mPosition < 0 || mPosition >= quickSearchList.size()) { - return; - } - - final QuickSearch quickSearch = quickSearchList.get(mPosition); - - new AlertDialog.Builder(context) - .setTitle(R.string.delete_quick_search_title) - .setMessage(getString(R.string.delete_quick_search_message, quickSearch.name)) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - EhDB.deleteQuickSearch(quickSearch); - quickSearchList.remove(mPosition); - } - }) - .setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialog) { - if (null != mAdapter) { - mAdapter.notifyDataSetChanged(); - } - updateView(); - } - }).show(); - } + public void onItemDragFinished(int fromPosition, int toPosition, boolean result) { } } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/SecurityScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/SecurityScene.java index 57ce348cd..7ea37e845 100755 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/SecurityScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/SecurityScene.java @@ -23,23 +23,21 @@ import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; -import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; - +import androidx.annotation.Nullable; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.ui.MainActivity; +import com.hippo.ehviewer.ui.SetSecurityActivity; import com.hippo.hardware.ShakeDetector; import com.hippo.widget.lockpattern.LockPatternUtils; import com.hippo.widget.lockpattern.LockPatternView; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.ObjectUtils; import com.hippo.yorozuya.ViewUtils; - -import junit.framework.Assert; - import java.util.List; public class SecurityScene extends SolidScene implements @@ -58,6 +56,7 @@ public class SecurityScene extends SolidScene implements private SensorManager mSensorManager; private Sensor mAccelerometer; private ShakeDetector mShakeDetector; + @Nullable private FingerprintManager mFingerprintManager; private CancellationSignal mFingerprintCancellationSignal; @@ -74,7 +73,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); if (null != mSensorManager) { mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); @@ -130,7 +129,7 @@ public void onAuthenticationFailed() { @Override public void onAuthenticationSucceeded( FingerprintManager.AuthenticationResult result) { - mFingerprintIcon.setImageResource(R.drawable.ic_fingerprint_success); + mFingerprintIcon.setImageResource(R.drawable.fingerprint_success); mFingerprintIcon.postDelayed(new Runnable() { @Override public void run() { @@ -237,8 +236,7 @@ private boolean isFingerprintAuthAvailable() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.getEnableFingerprint() && mFingerprintManager != null - && mFingerprintManager.isHardwareDetected() - && mFingerprintManager.hasEnrolledFingerprints(); + && SetSecurityActivity.hasEnrolledFingerprints(mFingerprintManager); } private Runnable mResetFingerprintRunnable = new Runnable() { @@ -251,7 +249,7 @@ public void run() { private void fingerprintError(boolean unrecoverable) { // Do not decrease mRetryTimes here since Android system will handle it :) - mFingerprintIcon.setImageResource(R.drawable.ic_fingerprint_error); + mFingerprintIcon.setImageResource(R.drawable.fingerprint_error); mFingerprintIcon.removeCallbacks(mResetFingerprintRunnable); if (unrecoverable) { mFingerprintIcon.postDelayed(new Runnable() { diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/SelectSiteScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/SelectSiteScene.java index b9a680719..8a08baec5 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/SelectSiteScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/SelectSiteScene.java @@ -17,13 +17,12 @@ package com.hippo.ehviewer.ui.scene; import android.os.Bundle; -import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.RadioGroup; import android.widget.Toast; - +import androidx.annotation.Nullable; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.client.EhUrl; diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/SignInScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/SignInScene.java index d1eb2684f..cf4e9c2ea 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/SignInScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/SignInScene.java @@ -19,9 +19,6 @@ import android.content.Context; import android.graphics.Paint; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.design.widget.TextInputLayout; -import android.support.v7.app.AlertDialog; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; @@ -29,7 +26,9 @@ import android.view.inputmethod.EditorInfo; import android.widget.EditText; import android.widget.TextView; - +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import com.google.android.material.textfield.TextInputLayout; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; @@ -40,14 +39,13 @@ import com.hippo.ehviewer.client.EhUtils; import com.hippo.ehviewer.client.parser.ProfileParser; import com.hippo.ehviewer.ui.MainActivity; -import com.hippo.ehviewer.widget.RecaptchaView; import com.hippo.scene.Announcer; import com.hippo.scene.SceneFragment; +import com.hippo.util.ExceptionUtils; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.IntIdGenerator; import com.hippo.yorozuya.ViewUtils; -import junit.framework.Assert; - public final class SignInScene extends SolidScene implements EditText.OnEditorActionListener, View.OnClickListener { @@ -69,8 +67,6 @@ public final class SignInScene extends SolidScene implements EditText.OnEditorAc private EditText mUsername; @Nullable private EditText mPassword; - private EditText mRecaptcha; - private RecaptchaView mRecaptchaView; @Nullable private View mRegister; @Nullable @@ -124,12 +120,10 @@ public View onCreateView2(LayoutInflater inflater, @Nullable ViewGroup container mProgress = ViewUtils.$$(view, R.id.progress); mUsernameLayout = (TextInputLayout) ViewUtils.$$(loginForm, R.id.username_layout); mUsername = mUsernameLayout.getEditText(); - Assert.assertNotNull(mUsername); + AssertUtils.assertNotNull(mUsername); mPasswordLayout = (TextInputLayout) ViewUtils.$$(loginForm, R.id.password_layout); mPassword = mPasswordLayout.getEditText(); - Assert.assertNotNull(mPassword); - mRecaptcha = (EditText) ViewUtils.$$(loginForm, R.id.recaptcha); - mRecaptchaView = (RecaptchaView) ViewUtils.$$(loginForm, R.id.recaptcha_image); + AssertUtils.assertNotNull(mPassword); mRegister = ViewUtils.$$(loginForm, R.id.register); mSignIn = ViewUtils.$$(loginForm, R.id.sign_in); mSignInViaWebView = (TextView) ViewUtils.$$(loginForm, R.id.sign_in_via_webview); @@ -149,7 +143,7 @@ public View onCreateView2(LayoutInflater inflater, @Nullable ViewGroup container mSkipSigningIn.setOnClickListener(this); Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); EhApplication application = (EhApplication) context.getApplicationContext(); if (application.containGlobalStuff(mRequestId)) { mSigningIn = true; @@ -285,15 +279,12 @@ private void signIn() { // Clean up for sign in EhUtils.signOut(context); - String challenge = mRecaptchaView.getChallenge(); - String response = mRecaptcha.getText().toString(); - EhCallback callback = new SignInListener(context, activity.getStageId(), getTag()); mRequestId = ((EhApplication) context.getApplicationContext()).putGlobalStuff(callback); EhRequest request = new EhRequest() .setMethod(EhClient.METHOD_SIGN_IN) - .setArgs(username, password, challenge, response) + .setArgs(username, password) .setCallback(callback); EhApplication.getEhClient(context).execute(request); @@ -328,7 +319,7 @@ private void redirectTo() { finish(); } - private void whetherToSkip() { + private void whetherToSkip(Exception e) { Context context = getContext2(); if (null == context) { return; @@ -336,12 +327,12 @@ private void whetherToSkip() { new AlertDialog.Builder(context) .setTitle(R.string.sign_in_failed) - .setMessage(R.string.sign_in_failed_plain) + .setMessage(ExceptionUtils.getReadableString(e) + "\n\n" + getString(R.string.sign_in_failed_tip)) .setPositiveButton(R.string.get_it, null) .show(); } - public void onSignInEnd() { + public void onSignInEnd(Exception e) { Context context = getContext2(); if (null == context) { return; @@ -352,7 +343,7 @@ public void onSignInEnd() { } else { mSigningIn = false; hideProgress(); - whetherToSkip(); + whetherToSkip(e); } } @@ -375,7 +366,7 @@ public void onSuccess(String result) { SignInScene scene = getScene(); if (scene != null) { - scene.onSignInEnd(); + scene.onSignInEnd(null); } } @@ -386,7 +377,7 @@ public void onFailure(Exception e) { SignInScene scene = getScene(); if (scene != null) { - scene.onSignInEnd(); + scene.onSignInEnd(e); } } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/SolidScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/SolidScene.java index 4bba3cb0a..a60c3e0a4 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/SolidScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/SolidScene.java @@ -18,8 +18,6 @@ import android.os.Bundle; import android.util.Log; - -import com.hippo.ehviewer.Crash; import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.client.EhUtils; import com.hippo.scene.Announcer; @@ -34,9 +32,8 @@ public class SolidScene extends BaseScene { public static final int CHECK_STEP_SECURITY = 0; public static final int CHECK_STEP_WARNING = 1; public static final int CHECK_STEP_ANALYTICS = 2; - public static final int CHECK_STEP_CRASH = 3; - public static final int CHECK_STEP_SIGN_IN = 4; - public static final int CHECK_STEP_SELECT_SITE = 5; + public static final int CHECK_STEP_SIGN_IN = 3; + public static final int CHECK_STEP_SELECT_SITE = 4; public static final String KEY_TARGET_SCENE = "target_scene"; public static final String KEY_TARGET_ARGS = "target_args"; @@ -54,11 +51,6 @@ public void startSceneForCheckStep(int checkStep, Bundle args) { break; } case CHECK_STEP_ANALYTICS: - if (Crash.hasCrashFile()) { - startScene(new Announcer(CrashScene.class).setArgs(args)); - break; - } - case CHECK_STEP_CRASH: if (EhUtils.needSignedIn(getContext2())) { startScene(new Announcer(SignInScene.class).setArgs(args)); break; @@ -89,7 +81,7 @@ public void startSceneForCheckStep(int checkStep, Bundle args) { startScene(new Announcer(clazz).setArgs(targetArgs)); } else { Bundle newArgs = new Bundle(); - newArgs.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_HOMEPAGE); + newArgs.putString(GalleryListScene.KEY_ACTION, Settings.getLaunchPageGalleryListSceneAction()); startScene(new Announcer(GalleryListScene.class).setArgs(newArgs)); } break; diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/ToolbarScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/ToolbarScene.java index b8f9272ce..48865d011 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/ToolbarScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/ToolbarScene.java @@ -18,17 +18,16 @@ import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.support.annotation.DrawableRes; -import android.support.annotation.Nullable; -import android.support.annotation.StringRes; -import android.support.v7.widget.Toolbar; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; - +import androidx.annotation.DrawableRes; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.widget.Toolbar; import com.hippo.ehviewer.R; public abstract class ToolbarScene extends BaseScene { diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/WarningScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/WarningScene.java index d0e0dcb8d..72300354d 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/WarningScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/WarningScene.java @@ -17,11 +17,10 @@ package com.hippo.ehviewer.ui.scene; import android.os.Bundle; -import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - +import androidx.annotation.Nullable; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.ui.MainActivity; diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/WebViewSignInScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/WebViewSignInScene.java index e33b4b21c..a526e6bc4 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/WebViewSignInScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/WebViewSignInScene.java @@ -20,7 +20,6 @@ import android.content.Context; import android.os.Build; import android.os.Bundle; -import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -28,14 +27,15 @@ import android.webkit.CookieSyncManager; import android.webkit.WebView; import android.webkit.WebViewClient; +import androidx.annotation.Nullable; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.client.EhCookieStore; import com.hippo.ehviewer.client.EhUrl; import com.hippo.ehviewer.client.EhUtils; +import com.hippo.yorozuya.AssertUtils; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import junit.framework.Assert; import okhttp3.Cookie; import okhttp3.HttpUrl; @@ -59,7 +59,7 @@ public boolean needShowLeftDrawer() { public View onCreateView2(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { Context context = getContext2(); - Assert.assertNotNull(context); + AssertUtils.assertNotNull(context); EhUtils.signOut(context); @@ -132,23 +132,18 @@ public void onPageFinished(WebView view, String url) { return; } - String cookieString = CookieManager.getInstance().getCookie(url); + String cookieString = CookieManager.getInstance().getCookie(EhUrl.HOST_E); List cookies = parseCookies(httpUrl, cookieString); boolean getId = false; boolean getHash = false; for (Cookie cookie: cookies) { if (EhCookieStore.KEY_IPD_MEMBER_ID.equals(cookie.name())) { getId = true; - addCookie(context, EhUrl.DOMAIN_EX, cookie); - addCookie(context, EhUrl.DOMAIN_E, cookie); } else if (EhCookieStore.KEY_IPD_PASS_HASH.equals(cookie.name())) { getHash = true; - addCookie(context, EhUrl.DOMAIN_EX, cookie); - addCookie(context, EhUrl.DOMAIN_E, cookie); - } else if (EhCookieStore.KEY_IGNEOUS.equals(cookie.name())) { - addCookie(context, EhUrl.DOMAIN_EX, cookie); - addCookie(context, EhUrl.DOMAIN_E, cookie); } + addCookie(context, EhUrl.DOMAIN_EX, cookie); + addCookie(context, EhUrl.DOMAIN_E, cookie); } if (getId && getHash) { diff --git a/app/src/main/java/com/hippo/ehviewer/widget/AdvanceSearchTable.java b/app/src/main/java/com/hippo/ehviewer/widget/AdvanceSearchTable.java index aaea7ef06..480b91327 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/AdvanceSearchTable.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/AdvanceSearchTable.java @@ -21,19 +21,22 @@ import android.os.Parcelable; import android.util.AttributeSet; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.LinearLayout; import android.widget.Spinner; -import android.widget.TableLayout; - import com.hippo.ehviewer.R; import com.hippo.yorozuya.NumberUtils; -public class AdvanceSearchTable extends TableLayout { +public class AdvanceSearchTable extends LinearLayout { private static final String STATE_KEY_SUPER = "super"; private static final String STATE_KEY_ADVANCE_SEARCH = "advance_search"; private static final String STATE_KEY_MIN_RATING = "min_rating"; + private static final String STATE_KEY_PAGE_FROM = "page_from"; + private static final String STATE_KEY_PAGE_TO = "page_to"; public static final int SNAME = 0x1; public static final int STAGS = 0x2; @@ -43,6 +46,9 @@ public class AdvanceSearchTable extends TableLayout { public static final int SDT1 = 0x20; public static final int SDT2 = 0x40; public static final int SH = 0x80; + public static final int SFL = 0x100; + public static final int SFU = 0x200; + public static final int SFT = 0x400; private CheckBox mSname; private CheckBox mStags; @@ -54,6 +60,12 @@ public class AdvanceSearchTable extends TableLayout { private CheckBox mSh; private CheckBox mSr; private Spinner mMinRating; + private CheckBox mSp; + private EditText mSpf; + private EditText mSpt; + private CheckBox mSfl; + private CheckBox mSfu; + private CheckBox mSft; public AdvanceSearchTable(Context context) { super(context); @@ -66,6 +78,8 @@ public AdvanceSearchTable(Context context, AttributeSet attrs) { } public void init(Context context) { + setOrientation(LinearLayout.VERTICAL); + LayoutInflater inflater = LayoutInflater.from(getContext()); inflater.inflate(R.layout.widget_advance_search_table, this); @@ -88,6 +102,25 @@ public void init(Context context) { ViewGroup row4 = (ViewGroup) getChildAt(4); mSr = (CheckBox) row4.getChildAt(0); mMinRating = (Spinner) row4.getChildAt(1); + + ViewGroup row5 = (ViewGroup) getChildAt(5); + mSp = (CheckBox) row5.getChildAt(0); + mSpf = (EditText) row5.getChildAt(1); + mSpt = (EditText) row5.getChildAt(3); + + ViewGroup row7 = (ViewGroup) getChildAt(7); + mSfl = (CheckBox) row7.getChildAt(0); + mSfu = (CheckBox) row7.getChildAt(1); + mSft = (CheckBox) row7.getChildAt(2); + + // Avoid java.lang.IllegalStateException: focus search returned a view that wasn't able to take focus! + mSpt.setOnEditorActionListener((v, actionId, event) -> { + View nextView = v.focusSearch(View.FOCUS_DOWN); + if (nextView != null) { + nextView.requestFocus(View.FOCUS_DOWN); + } + return true; + }); } public int getAdvanceSearch() { @@ -100,6 +133,9 @@ public int getAdvanceSearch() { if (mSdt1.isChecked()) advanceSearch |= SDT1; if (mSdt2.isChecked()) advanceSearch |= SDT2; if (mSh.isChecked()) advanceSearch |= SH; + if (mSfl.isChecked()) advanceSearch |= SFL; + if (mSfu.isChecked()) advanceSearch |= SFU; + if (mSft.isChecked()) advanceSearch |= SFT; return advanceSearch; } @@ -112,6 +148,20 @@ public int getMinRating() { } } + public int getPageFrom() { + if (mSp.isChecked()) { + return NumberUtils.parseIntSafely(mSpf.getText().toString(), -1); + } + return -1; + } + + public int getPageTo() { + if (mSp.isChecked()) { + return NumberUtils.parseIntSafely(mSpt.getText().toString(), -1); + } + return -1; + } + public void setAdvanceSearch(int advanceSearch) { mSname.setChecked(NumberUtils.int2boolean(advanceSearch & SNAME)); mStags.setChecked(NumberUtils.int2boolean(advanceSearch & STAGS)); @@ -121,6 +171,9 @@ public void setAdvanceSearch(int advanceSearch) { mSdt1.setChecked(NumberUtils.int2boolean(advanceSearch & SDT1)); mSdt2.setChecked(NumberUtils.int2boolean(advanceSearch & SDT2)); mSh.setChecked(NumberUtils.int2boolean(advanceSearch & SH)); + mSfl.setChecked(NumberUtils.int2boolean(advanceSearch & SFL)); + mSfu.setChecked(NumberUtils.int2boolean(advanceSearch & SFU)); + mSft.setChecked(NumberUtils.int2boolean(advanceSearch & SFT)); } public void setMinRating(int minRating) { @@ -132,12 +185,34 @@ public void setMinRating(int minRating) { } } + public void setPageFrom(int pageFrom) { + if (pageFrom > 0) { + mSpf.setText(Integer.toString(pageFrom)); + mSp.setChecked(true); + } else { + mSp.setChecked(false); + mSpf.setText(null); + } + } + + public void setPageTo(int pageTo) { + if (pageTo > 0) { + mSpt.setText(Integer.toString(pageTo)); + mSp.setChecked(true); + } else { + mSp.setChecked(false); + mSpt.setText(null); + } + } + @Override public Parcelable onSaveInstanceState() { final Bundle state = new Bundle(); state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState()); state.putInt(STATE_KEY_ADVANCE_SEARCH, getAdvanceSearch()); state.putInt(STATE_KEY_MIN_RATING, getMinRating()); + state.putInt(STATE_KEY_PAGE_FROM, getPageFrom()); + state.putInt(STATE_KEY_PAGE_TO, getPageTo()); return state; } @@ -148,6 +223,8 @@ public void onRestoreInstanceState(Parcelable state) { super.onRestoreInstanceState(savedState.getParcelable(STATE_KEY_SUPER)); setAdvanceSearch(savedState.getInt(STATE_KEY_ADVANCE_SEARCH)); setMinRating(savedState.getInt(STATE_KEY_MIN_RATING)); + setPageFrom(savedState.getInt(STATE_KEY_PAGE_FROM)); + setPageTo(savedState.getInt(STATE_KEY_PAGE_TO)); } } } diff --git a/app/src/main/java/com/hippo/ehviewer/widget/CategoryTable.java b/app/src/main/java/com/hippo/ehviewer/widget/CategoryTable.java index e11832ca8..7db60f842 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/CategoryTable.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/CategoryTable.java @@ -21,15 +21,15 @@ import android.os.Parcelable; import android.util.AttributeSet; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; import android.widget.TableLayout; - import com.hippo.ehviewer.R; import com.hippo.ehviewer.client.EhConfig; import com.hippo.widget.CheckTextView; import com.hippo.yorozuya.NumberUtils; -public class CategoryTable extends TableLayout { +public class CategoryTable extends TableLayout implements View.OnLongClickListener { private static final String STATE_KEY_SUPER = "super"; private static final String STATE_KEY_CATEGORY = "category"; @@ -45,6 +45,8 @@ public class CategoryTable extends TableLayout { private CheckTextView mAsianPorn; private CheckTextView mMisc; + private CheckTextView[] mOptions; + public CategoryTable(Context context) { super(context); init(); @@ -78,6 +80,29 @@ public void init() { ViewGroup row4 = (ViewGroup) getChildAt(4); mAsianPorn = (CheckTextView) row4.getChildAt(0); mMisc = (CheckTextView) row4.getChildAt(1); + + mOptions = new CheckTextView[] { + mDoujinshi, mManga, mArtistCG, mGameCG, mWestern, + mNonH, mImageSets, mCosplay, mAsianPorn, mMisc + }; + + for (CheckTextView option : mOptions) { + option.setOnLongClickListener(this); + } + } + + @Override + public boolean onLongClick(View v) { + if (v instanceof CheckTextView) { + boolean checked = ((CheckTextView) v).isChecked(); + for (CheckTextView option : mOptions) { + if (option != v) { + option.setChecked(!checked, false); + } + } + } + + return true; } /** diff --git a/app/src/main/java/com/hippo/ehviewer/widget/DialogWebChromeClient.java b/app/src/main/java/com/hippo/ehviewer/widget/DialogWebChromeClient.java new file mode 100644 index 000000000..41dcb092f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/DialogWebChromeClient.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.widget; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.webkit.JsPromptResult; +import android.webkit.JsResult; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import android.widget.EditText; +import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; +import com.hippo.ehviewer.R; + +public class DialogWebChromeClient extends WebChromeClient { + + private Context context; + + public DialogWebChromeClient(Context context) { + this.context = context; + } + + @Override + public boolean onJsAlert(WebView view, String url, String message, JsResult result) { + new AlertDialog.Builder(view.getContext()) + .setMessage(message) + .setPositiveButton(android.R.string.ok, (dialog, which) -> result.confirm()) + .setOnCancelListener(dialog -> result.cancel()) + .show(); + return true; + } + + @Override + public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { + new AlertDialog.Builder(view.getContext()) + .setMessage(message) + .setPositiveButton(android.R.string.ok, (dialog, which) -> result.confirm()) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> result.cancel()) + .setOnCancelListener(dialog -> result.cancel()) + .show(); + return true; + } + + @Override + public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { + LayoutInflater inflater = LayoutInflater.from(context); + View promptView = inflater.inflate(R.layout.dialog_js_prompt, null, false); + TextView messageView = promptView.findViewById(R.id.message); + messageView.setText(message); + final EditText valueView = promptView.findViewById(R.id.value); + valueView.setText(defaultValue); + + new AlertDialog.Builder(context) + .setView(promptView) + .setPositiveButton(android.R.string.ok, (dialog, which) -> result.confirm(valueView.getText().toString())) + .setOnCancelListener(dialog -> result.cancel()) + .show(); + + return true; + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/EhDrawerLayout.java b/app/src/main/java/com/hippo/ehviewer/widget/EhDrawerLayout.java index 725cb9fbb..068202014 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/EhDrawerLayout.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/EhDrawerLayout.java @@ -19,17 +19,15 @@ import android.animation.ValueAnimator; import android.content.Context; import android.os.Build; -import android.support.annotation.Nullable; -import android.support.design.widget.CoordinatorLayout; -import android.support.design.widget.Snackbar; -import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.view.View; - +import androidx.annotation.Nullable; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.view.ViewCompat; +import com.google.android.material.snackbar.Snackbar; import com.hippo.drawerlayout.DrawerLayout; import com.hippo.ehviewer.R; import com.hippo.yorozuya.AnimationUtils; - import java.util.ArrayList; import java.util.List; diff --git a/app/src/main/java/com/hippo/ehviewer/widget/EhDrawerView.java b/app/src/main/java/com/hippo/ehviewer/widget/EhDrawerView.java index 966b548c1..73907f078 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/EhDrawerView.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/EhDrawerView.java @@ -24,7 +24,8 @@ public class EhDrawerView extends DrawerView implements DrawerLayoutChild { - private int mFitPaddingTop; + private int mWindowPaddingTop; + private int mWindowPaddingBottom; public EhDrawerView(Context context) { super(context); @@ -39,17 +40,18 @@ public EhDrawerView(Context context, AttributeSet attrs, int defStyleAttr) { } @Override - public void setFitPadding(int top, int bottom) { - mFitPaddingTop = top; + public void onGetWindowPadding(int top, int bottom) { + mWindowPaddingTop = top; + mWindowPaddingBottom = bottom; } @Override - public int getLayoutPaddingTop() { - return mFitPaddingTop; + public int getAdditionalTopMargin() { + return mWindowPaddingTop; } @Override - public int getLayoutPaddingBottom() { - return 0; + public int getAdditionalBottomMargin() { + return mWindowPaddingBottom; } } diff --git a/app/src/main/java/com/hippo/ehviewer/widget/EhNavigationView.java b/app/src/main/java/com/hippo/ehviewer/widget/EhNavigationView.java index f4724a474..5851df0e6 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/EhNavigationView.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/EhNavigationView.java @@ -20,18 +20,18 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.os.Build; -import android.support.annotation.NonNull; -import android.support.design.widget.NavigationView; import android.util.AttributeSet; - +import android.widget.LinearLayout; +import androidx.annotation.NonNull; import com.hippo.drawerlayout.DrawerLayoutChild; -public class EhNavigationView extends NavigationView implements DrawerLayoutChild { +public class EhNavigationView extends LinearLayout implements DrawerLayoutChild { private static final int SCRIM_COLOR = 0x44000000; private static final boolean DRAW_SCRIM = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; private Paint mPaint; - private int mFitPaddingTop; + private int mWindowPaddingTop; + private int mWindowPaddingBottom; public EhNavigationView(Context context) { super(context); @@ -58,27 +58,28 @@ private void init() { public void draw(@NonNull Canvas canvas) { super.draw(canvas); - if (DRAW_SCRIM && mFitPaddingTop > 0) { + if (DRAW_SCRIM && mWindowPaddingTop > 0) { if (null == mPaint) { mPaint = new Paint(); mPaint.setColor(SCRIM_COLOR); } - canvas.drawRect(0, 0, getWidth(), mFitPaddingTop, mPaint); + canvas.drawRect(0, 0, getWidth(), mWindowPaddingTop, mPaint); } } @Override - public void setFitPadding(int top, int bottom) { - mFitPaddingTop = top; + public void onGetWindowPadding(int top, int bottom) { + mWindowPaddingTop = top; + mWindowPaddingBottom = bottom; } @Override - public int getLayoutPaddingTop() { + public int getAdditionalTopMargin() { return 0; } @Override - public int getLayoutPaddingBottom() { - return 0; + public int getAdditionalBottomMargin() { + return mWindowPaddingBottom; } } diff --git a/app/src/main/java/com/hippo/ehviewer/widget/EhStageLayout.java b/app/src/main/java/com/hippo/ehviewer/widget/EhStageLayout.java index 518e84bef..e49bbe9b4 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/EhStageLayout.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/EhStageLayout.java @@ -24,7 +24,8 @@ public class EhStageLayout extends StageLayout implements DrawerLayoutChild { - private int mFitPaddingTop; + private int mWindowPaddingTop; + private int mWindowPaddingBottom; public EhStageLayout(Context context) { super(context); @@ -39,17 +40,18 @@ public EhStageLayout(Context context, AttributeSet attrs, int defStyleAttr) { } @Override - public void setFitPadding(int top, int bottom) { - mFitPaddingTop = top; + public void onGetWindowPadding(int top, int bottom) { + mWindowPaddingTop = top; + mWindowPaddingBottom = bottom; } @Override - public int getLayoutPaddingTop() { - return mFitPaddingTop; + public int getAdditionalTopMargin() { + return mWindowPaddingTop; } @Override - public int getLayoutPaddingBottom() { - return 0; + public int getAdditionalBottomMargin() { + return mWindowPaddingBottom; } } diff --git a/app/src/main/java/com/hippo/ehviewer/widget/FixedThumb.java b/app/src/main/java/com/hippo/ehviewer/widget/FixedThumb.java new file mode 100644 index 000000000..fd662c48f --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/FixedThumb.java @@ -0,0 +1,79 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import com.hippo.ehviewer.R; +import com.hippo.widget.LoadImageView; + +public class FixedThumb extends LoadImageView { + + private float minAspect; + private float maxAspect; + + public FixedThumb(Context context) { + super(context); + init(context, null, 0, 0); + } + + public FixedThumb(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0, 0); + } + + public FixedThumb(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle, 0); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FixedThumb, defStyleAttr, defStyleRes); + minAspect = a.getFloat(R.styleable.FixedThumb_minAspect, 0.0f); + maxAspect = a.getFloat(R.styleable.FixedThumb_maxAspect, 0.0f); + a.recycle(); + } + + public void setFix(float minAspect, float maxAspect) { + this.minAspect = minAspect; + this.maxAspect = maxAspect; + } + + @Override + public void onPreSetImageDrawable(Drawable drawable, boolean isTarget) { + if (isTarget && drawable != null) { + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + if (width > 0 && height > 0) { + float aspect = (float) width / (float) height; + if (aspect < maxAspect && aspect > minAspect) { + setScaleType(ScaleType.CENTER_CROP); + return; + } + } + } + + setScaleType(ScaleType.FIT_CENTER); + } + + @Override + public void onPreSetImageResource(int resId, boolean isTarget) { + setScaleType(ScaleType.FIT_CENTER); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/GalleryGuideView.java b/app/src/main/java/com/hippo/ehviewer/widget/GalleryGuideView.java index b65b9f8d3..8b90a5f32 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/GalleryGuideView.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/GalleryGuideView.java @@ -18,15 +18,13 @@ import android.content.Context; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.Paint; -import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - +import com.hippo.android.resource.AttrResources; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; import com.hippo.yorozuya.ViewUtils; @@ -60,9 +58,9 @@ public GalleryGuideView(Context context, AttributeSet attrs, int defStyleAttr) { } private void init(Context context) { - mBgColor = ContextCompat.getColor(context, R.color.guide_bg); + mBgColor = AttrResources.getAttrColor(context, R.attr.guideBackgroundColor); mPaint = new Paint(); - mPaint.setColor(Color.WHITE); + mPaint.setColor(AttrResources.getAttrColor(context, R.attr.guideTitleColor)); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(context.getResources().getDimension(R.dimen.gallery_guide_divider_width)); setOnClickListener(this); diff --git a/app/src/main/java/com/hippo/ehviewer/widget/GalleryHeader.java b/app/src/main/java/com/hippo/ehviewer/widget/GalleryHeader.java new file mode 100644 index 000000000..662951e40 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/GalleryHeader.java @@ -0,0 +1,205 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.widget; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.util.AttributeSet; +import android.view.DisplayCutout; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.hippo.ehviewer.R; +import com.hippo.yorozuya.ObjectUtils; + +public class GalleryHeader extends ViewGroup { + + private DisplayCutout displayCutout; + + private View battery; + private View progress; + private View clock; + + private Rect batteryRect = new Rect(); + private Rect progressRect = new Rect(); + private Rect clockRect = new Rect(); + + private int[] location = new int[2]; + + private int lastX = 0; + private int lastY = 0; + + public GalleryHeader(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @RequiresApi(api = Build.VERSION_CODES.P) + public void setDisplayCutout(@Nullable DisplayCutout displayCutout) { + if (!ObjectUtils.equal(this.displayCutout, displayCutout)) { + this.displayCutout = displayCutout; + requestLayout(); + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + battery = findViewById(R.id.battery); + progress = findViewById(R.id.progress); + clock = findViewById(R.id.clock); + } + + private void measureChild(Rect rect, View view, int width, int paddingLeft, int paddingRight) { + int left; + MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams(); + if (view == battery) { + left = paddingLeft + lp.leftMargin; + } else if (view == progress) { + left = paddingLeft + (width - paddingLeft - paddingRight) / 2 - view.getMeasuredWidth() / 2; + } else { + left = width - paddingRight - lp.rightMargin - view.getMeasuredWidth(); + } + rect.set(left, lp.topMargin, left + view.getMeasuredWidth(), lp.topMargin + view.getMeasuredHeight()); + } + + @RequiresApi(api = Build.VERSION_CODES.P) + private boolean offsetVertically(Rect rect, View view, int width) { + int offset = 0; + + measureChild(rect, view, width, 0, 0); + rect.offset(lastX, lastY); + + for (Rect notch : displayCutout.getBoundingRects()) { + if (Rect.intersects(notch, rect)) { + offset = Math.max(offset, notch.bottom - lastY); + } + } + + if (offset != 0) { + rect.offset(-lastX, -lastY); + rect.offset(0, offset); + return true; + } else { + return false; + } + } + + @RequiresApi(api = Build.VERSION_CODES.P) + private int getOffsetLeft(Rect rect, View view, int width) { + int offset = 0; + + measureChild(rect, view, width, 0, 0); + rect.offset(lastX, lastY); + + for (Rect notch : displayCutout.getBoundingRects()) { + if (Rect.intersects(notch, rect)) { + offset = Math.max(offset, notch.right - lastX); + } + } + + return offset; + } + + @RequiresApi(api = Build.VERSION_CODES.P) + private int getOffsetRight(Rect rect, View view, int width) { + int offset = 0; + + measureChild(rect, view, width, 0, 0); + rect.offset(lastX, lastY); + + for (Rect notch : displayCutout.getBoundingRects()) { + if (Rect.intersects(notch, rect)) { + offset = Math.max(offset, lastX + width - notch.left); + } + } + + return offset; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) { + throw new IllegalStateException(); + } + int width = MeasureSpec.getSize(widthMeasureSpec); + + int height = 0; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + measureChild(child, widthMeasureSpec, heightMeasureSpec); + MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + height = Math.max(height, child.getMeasuredHeight() + lp.topMargin); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && displayCutout != null) { + // Check progress covered + if (offsetVertically(progressRect, progress, width)) { + offsetVertically(batteryRect, battery, width); + offsetVertically(clockRect, clock, width); + height = Math.max(progressRect.bottom, Math.max(batteryRect.bottom, clockRect.bottom)); + } else { + // Clamp left and right + int paddingLeft = getOffsetLeft(batteryRect, battery, width); + int paddingRight = getOffsetRight(clockRect, clock, width); + measureChild(batteryRect, battery, width, paddingLeft, paddingRight); + measureChild(progressRect, progress, width, paddingLeft, paddingRight); + measureChild(clockRect, clock, width, paddingLeft, paddingRight); + } + } else { + measureChild(batteryRect, battery, width, 0, 0); + measureChild(progressRect, progress, width, 0, 0); + measureChild(clockRect, clock, width, 0, 0); + } + + setMeasuredDimension(width, height); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + battery.layout(batteryRect.left, batteryRect.top, batteryRect.right, batteryRect.bottom); + progress.layout(progressRect.left, progressRect.top, progressRect.right, progressRect.bottom); + clock.layout(clockRect.left, clockRect.top, clockRect.right, clockRect.bottom); + + getLocationOnScreen(location); + if (lastX != location[0] || lastY != location[1]) { + lastX = location[0]; + lastY = location[1]; + requestLayout(); + } + } + + @Override + public MarginLayoutParams generateLayoutParams(AttributeSet attrs) { + return new MarginLayoutParams(getContext(), attrs); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof MarginLayoutParams; + } + + @Override + protected MarginLayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { + if (lp instanceof MarginLayoutParams) { + return new MarginLayoutParams((MarginLayoutParams) lp); + } + return new MarginLayoutParams(lp); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/GalleryInfoContentHelper.java b/app/src/main/java/com/hippo/ehviewer/widget/GalleryInfoContentHelper.java new file mode 100644 index 000000000..fbe0dcdc3 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/widget/GalleryInfoContentHelper.java @@ -0,0 +1,109 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.widget; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.os.Parcelable; +import com.hippo.ehviewer.EhApplication; +import com.hippo.ehviewer.FavouriteStatusRouter; +import com.hippo.ehviewer.client.data.GalleryInfo; +import com.hippo.widget.ContentLayout; +import com.hippo.yorozuya.IntIdGenerator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public abstract class GalleryInfoContentHelper extends ContentLayout.ContentHelper { + + private static final String KEY_DATA_MAP = "data_map"; + + @SuppressLint("UseSparseArrays") + private Map map = new HashMap<>(); + private FavouriteStatusRouter.Listener listener; + + public GalleryInfoContentHelper() { + listener = (gid, slot) -> { + GalleryInfo info = map.get(gid); + if (info != null) { + info.favoriteSlot = slot; + } + }; + EhApplication.getFavouriteStatusRouter().addListener(listener); + } + + public void destroy() { + EhApplication.getFavouriteStatusRouter().removeListener(listener); + } + + @Override + protected void onAddData(GalleryInfo data) { + map.put(data.gid, data); + } + + @Override + protected void onAddData(List data) { + for (GalleryInfo info : data) { + map.put(info.gid, info); + } + } + + @Override + protected void onRemoveData(GalleryInfo data) { + map.remove(data.gid); + } + + @Override + protected void onRemoveData(List data) { + for (GalleryInfo info : data) { + map.remove(info.gid); + } + } + + @Override + protected void onClearData() { + map.clear(); + } + + @Override + protected Parcelable saveInstanceState(Parcelable superState) { + Bundle bundle = (Bundle) super.saveInstanceState(superState); + + // TODO It's a bad design + FavouriteStatusRouter router = EhApplication.getFavouriteStatusRouter(); + int id = router.saveDataMap(map); + bundle.putInt(KEY_DATA_MAP, id); + + return bundle; + } + + @Override + protected Parcelable restoreInstanceState(Parcelable state) { + Bundle bundle = (Bundle) state; + + int id = bundle.getInt(KEY_DATA_MAP, IntIdGenerator.INVALID_ID); + if (id != IntIdGenerator.INVALID_ID) { + FavouriteStatusRouter router = EhApplication.getFavouriteStatusRouter(); + Map map = router.restoreDataMap(id); + if (map != null) { + this.map = map; + } + } + + return super.restoreInstanceState(state); + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/GalleryRatingBar.java b/app/src/main/java/com/hippo/ehviewer/widget/GalleryRatingBar.java index 3d6f7e7f3..ec8e2dfe4 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/GalleryRatingBar.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/GalleryRatingBar.java @@ -18,9 +18,9 @@ import android.content.Context; import android.graphics.Canvas; -import android.support.v7.widget.AppCompatRatingBar; import android.util.AttributeSet; import android.widget.RatingBar; +import androidx.appcompat.widget.AppCompatRatingBar; public class GalleryRatingBar extends AppCompatRatingBar implements RatingBar.OnRatingBarChangeListener { diff --git a/app/src/main/java/com/hippo/ehviewer/widget/ImageSearchLayout.java b/app/src/main/java/com/hippo/ehviewer/widget/ImageSearchLayout.java index 8bf53908b..b4c4b6014 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/ImageSearchLayout.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/ImageSearchLayout.java @@ -22,7 +22,6 @@ import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; -import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.Log; import android.view.AbsSavedState; @@ -31,7 +30,7 @@ import android.widget.CheckBox; import android.widget.ImageView; import android.widget.LinearLayout; - +import androidx.annotation.Nullable; import com.hippo.ehviewer.AppConfig; import com.hippo.ehviewer.R; import com.hippo.ehviewer.client.data.ListUrlBuilder; @@ -41,7 +40,6 @@ import com.hippo.util.BitmapUtils; import com.hippo.yorozuya.IOUtils; import com.hippo.yorozuya.ViewUtils; - import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; diff --git a/app/src/main/java/com/hippo/ehviewer/widget/RecaptchaView.java b/app/src/main/java/com/hippo/ehviewer/widget/RecaptchaView.java deleted file mode 100644 index 8598a4ab1..000000000 --- a/app/src/main/java/com/hippo/ehviewer/widget/RecaptchaView.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2017 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.ehviewer.widget; - -/* - * Created by Hippo on 2017/8/20. - */ - -import android.content.Context; -import android.graphics.Color; -import android.os.Parcel; -import android.os.Parcelable; -import android.support.annotation.NonNull; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; -import com.hippo.android.recaptcha.RecaptchaV1; -import com.hippo.drawable.TextDrawable; -import com.hippo.ehviewer.R; -import com.hippo.widget.LoadImageView; -import com.hippo.yorozuya.SimpleHandler; - -public class RecaptchaView extends LoadImageView implements RecaptchaV1.RecaptchaCallback, View.OnClickListener { - - private static final String CHALLENGE = "6LdtfgYAAAAAALjIPPiCgPJJah8MhAUpnHcKF8u_"; - - private boolean loading = false; - private String challenge; - private String image; - - private TextDrawable waitingDrawable; - private TextDrawable loadingDrawable; - private TextDrawable failureDrawable; - - public RecaptchaView(Context context) { - super(context); - init(); - } - - public RecaptchaView(Context context, AttributeSet attrs) { - super(context, attrs); - init(); - } - - private void init() { - setOnClickListener(this); - if (waitingDrawable == null) { - waitingDrawable = new TextDrawable(getContext().getString(R.string.recaptcha_none), 0.6f); - waitingDrawable.setBackgroundColor(Color.GRAY); - waitingDrawable.setTextColor(Color.WHITE); - } - load(waitingDrawable); - } - - public String getChallenge() { - return challenge; - } - - public void load() { - if (loading) { - return; - } - loading = false; - challenge = null; - image = null; - - if (loadingDrawable == null) { - loadingDrawable = new TextDrawable(getContext().getString(R.string.recaptcha_loading), 0.6f); - loadingDrawable.setBackgroundColor(Color.GRAY); - loadingDrawable.setTextColor(Color.WHITE); - } - load(loadingDrawable); - - RecaptchaV1.recaptcha(getContext(), CHALLENGE, SimpleHandler.getInstance(), this); - } - - @Override - public void onClick(@NonNull View v) { - load(); - } - - @Override - public void onSuccess(@NonNull String challenge, @NonNull String image) { - this.loading = false; - this.challenge = challenge; - this.image = image; - - load(image, image); - } - - @Override - public void onFailure() { - this.loading = false; - this.challenge = null; - this.image = null; - - if (failureDrawable == null) { - failureDrawable = new TextDrawable(getContext().getString(R.string.recaptcha_failure), 0.6f); - failureDrawable.setBackgroundColor(Color.GRAY); - failureDrawable.setTextColor(Color.WHITE); - } - load(failureDrawable); - } - - @Override - protected Parcelable onSaveInstanceState() { - SavedState ss = new SavedState(super.onSaveInstanceState()); - ss.loading = loading; - ss.challenge = challenge; - ss.image = image; - return ss; - } - - @Override - protected void onRestoreInstanceState(Parcelable state) { - if (!(state instanceof SavedState)) { - super.onRestoreInstanceState(state); - return; - } - - SavedState ss = (SavedState) state; - super.onRestoreInstanceState(ss.getSuperState()); - - if (ss.loading) { - load(); - } else if (!TextUtils.isEmpty(ss.challenge) && !TextUtils.isEmpty(ss.image)) { - onSuccess(ss.challenge, ss.image); - } - } - - private static class SavedState extends BaseSavedState { - - private boolean loading; - private String challenge; - private String image; - - public SavedState(Parcelable superState) { - super(superState); - } - - @Override - public void writeToParcel(Parcel out, int flags) { - super.writeToParcel(out, flags); - out.writeByte(loading ? (byte) 1 : (byte) 0); - out.writeString(challenge); - out.writeString(image); - } - - public static final Parcelable.Creator CREATOR - = new Parcelable.Creator() { - @Override - public SavedState createFromParcel(Parcel in) { - return new SavedState(in); - } - - @Override - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; - - public SavedState(Parcel source) { - super(source); - loading = source.readByte() != 0; - challenge = source.readString(); - image = source.readString(); - } - } -} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/ReversibleSeekBar.java b/app/src/main/java/com/hippo/ehviewer/widget/ReversibleSeekBar.java index 7a5d40a26..e8e05706f 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/ReversibleSeekBar.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/ReversibleSeekBar.java @@ -18,9 +18,9 @@ import android.content.Context; import android.graphics.Canvas; -import android.support.v7.widget.AppCompatSeekBar; import android.util.AttributeSet; import android.view.MotionEvent; +import androidx.appcompat.widget.AppCompatSeekBar; public class ReversibleSeekBar extends AppCompatSeekBar { diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SearchBar.java b/app/src/main/java/com/hippo/ehviewer/widget/SearchBar.java index d9d91f2ef..4331734bb 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/SearchBar.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/SearchBar.java @@ -25,7 +25,6 @@ import android.os.Build; import android.os.Bundle; import android.os.Parcelable; -import android.support.annotation.NonNull; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; @@ -37,24 +36,22 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.FrameLayout; +import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; - +import androidx.annotation.NonNull; +import androidx.cardview.widget.CardView; import com.hippo.ehviewer.R; import com.hippo.view.ViewTransition; import com.hippo.yorozuya.AnimationUtils; import com.hippo.yorozuya.MathUtils; import com.hippo.yorozuya.SimpleAnimatorListener; import com.hippo.yorozuya.ViewUtils; - import java.util.ArrayList; -import java.util.Collections; import java.util.List; -public class SearchBar extends FrameLayout implements View.OnClickListener, +public class SearchBar extends CardView implements View.OnClickListener, TextView.OnEditorActionListener, TextWatcher, SearchEditText.SearchEditTextListener { @@ -79,17 +76,19 @@ public class SearchBar extends FrameLayout implements View.OnClickListener, private TextView mTitleTextView; private ImageView mActionButton; private SearchEditText mEditText; + private ListView mListView; private View mListContainer; private View mListHeader; private ViewTransition mViewTransition; private SearchDatabase mSearchDatabase; - private List mSuggestionList; - private ArrayAdapter mSuggestionAdapter; + private List mSuggestionList; + private SuggestionAdapter mSuggestionAdapter; private Helper mHelper; private OnStateChangeListener mOnStateChangeListener; + private SuggestionProvider mSuggestionProvider; private boolean mAllowEmptySearch = true; @@ -111,8 +110,6 @@ public SearchBar(Context context, AttributeSet attrs, int defStyleAttr) { } private void init(Context context) { - setBackgroundResource(R.drawable.card_white_no_padding_2dp); - mSearchDatabase = SearchDatabase.getInstance(getContext()); LayoutInflater inflater = LayoutInflater.from(context); @@ -122,7 +119,7 @@ private void init(Context context) { mActionButton = (ImageView) ViewUtils.$$(this, R.id.search_action); mEditText = (SearchEditText) ViewUtils.$$(this, R.id.search_edit_text); mListContainer = ViewUtils.$$(this, R.id.list_container); - ListView list = (ListView) ViewUtils.$$(mListContainer, R.id.search_bar_list); + mListView = (ListView) ViewUtils.$$(mListContainer, R.id.search_bar_list); mListHeader = ViewUtils.$$(mListContainer, R.id.list_header); mViewTransition = new ViewTransition(mTitleTextView, mEditText); @@ -139,22 +136,18 @@ private void init(Context context) { mBaseHeight = getMeasuredHeight(); mSuggestionList = new ArrayList<>(); - mSuggestionAdapter = new ArrayAdapter<>(getContext(), R.layout.item_simple_list, mSuggestionList); - list.setAdapter(mSuggestionAdapter); - list.setOnItemClickListener(new AdapterView.OnItemClickListener() { + mSuggestionAdapter = new SuggestionAdapter(LayoutInflater.from(getContext())); + mListView.setAdapter(mSuggestionAdapter); + mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { - String suggestion = mSuggestionList.get(MathUtils.clamp(position, 0, mSuggestionList.size() - 1)); - mEditText.setText(suggestion); - mEditText.setSelection(mEditText.getText().length()); + mSuggestionList.get(position).onClick(); } }); - list.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - String suggestion = mSuggestionList.get(MathUtils.clamp(position, 0, mSuggestionList.size() - 1)); - mSearchDatabase.deleteQuery(suggestion); - updateSuggestions(); + mSuggestionList.get(position).onLongClick(); return true; } }); @@ -169,16 +162,36 @@ private void removeListHeader() { } private void updateSuggestions() { - String prefix = mEditText.getText().toString(); - String[] suggestions = mSearchDatabase.getSuggestions(prefix); + updateSuggestions(true); + } + + private void updateSuggestions(boolean scrollToTop) { mSuggestionList.clear(); - Collections.addAll(mSuggestionList, suggestions); + + String text = mEditText.getText().toString(); + + if (mSuggestionProvider != null) { + List suggestions = mSuggestionProvider.providerSuggestions(text); + if (suggestions != null && !suggestions.isEmpty()) { + mSuggestionList.addAll(suggestions); + } + } + + String[] keywords = mSearchDatabase.getSuggestions(text, 128); + for (String keyword : keywords) { + mSuggestionList.add(new KeywordSuggestion(keyword)); + } + if (mSuggestionList.size() == 0) { removeListHeader(); } else { addListHeader(); } mSuggestionAdapter.notifyDataSetChanged(); + + if (scrollToTop) { + mListView.setSelection(0); + } } public void setAllowEmptySearch(boolean allowEmptySearch) { @@ -201,6 +214,10 @@ public void setOnStateChangeListener(OnStateChangeListener listener) { mOnStateChangeListener = listener; } + public void setSuggestionProvider(SuggestionProvider suggestionProvider) { + mSuggestionProvider = suggestionProvider; + } + public void setText(String text) { mEditText.setText(text); } @@ -490,4 +507,82 @@ public interface OnStateChangeListener { void onStateChange(SearchBar searchBar, int newState, int oldState, boolean animation); } + + public interface SuggestionProvider { + + List providerSuggestions(String text); + } + + private class SuggestionAdapter extends BaseAdapter { + + private LayoutInflater mInflater; + + private SuggestionAdapter(LayoutInflater inflater) { + mInflater = inflater; + } + + @Override + public int getCount() { + return mSuggestionList.size(); + } + + @Override + public Object getItem(int position) { + return mSuggestionList.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + TextView textView; + if (convertView == null) { + textView = (TextView) mInflater.inflate(R.layout.item_simple_list, parent, false); + } else { + textView = (TextView) convertView; + } + + textView.setText(mSuggestionList.get(position).getText(textView.getTextSize())); + + return textView; + } + } + + public abstract static class Suggestion { + + public abstract CharSequence getText(float textSize); + + public abstract void onClick(); + + public abstract void onLongClick(); + } + + public class KeywordSuggestion extends Suggestion { + + private String mKeyword; + + private KeywordSuggestion(String keyword) { + mKeyword = keyword; + } + + @Override + public CharSequence getText(float textSize) { + return mKeyword; + } + + @Override + public void onClick() { + mEditText.setText(mKeyword); + mEditText.setSelection(mEditText.getText().length()); + } + + @Override + public void onLongClick() { + mSearchDatabase.deleteQuery(mKeyword); + updateSuggestions(false); + } + } } diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SearchDatabase.java b/app/src/main/java/com/hippo/ehviewer/widget/SearchDatabase.java index 4200dfb62..408382cb6 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/SearchDatabase.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/SearchDatabase.java @@ -19,6 +19,7 @@ import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.text.TextUtils; @@ -57,10 +58,10 @@ private SearchDatabase(Context context) { mDatabase = databaseHelper.getWritableDatabase(); } - public String[] getSuggestions(String prefix) { + public String[] getSuggestions(String prefix, int limit) { List queryList = new LinkedList<>(); + limit = Math.max(0, limit); - // TODO add limit StringBuilder sb = new StringBuilder(); sb.append("SELECT * FROM ").append(TABLE_SUGGESTIONS); if (!TextUtils.isEmpty(prefix)) { @@ -68,20 +69,25 @@ public String[] getSuggestions(String prefix) { .append(SqlUtils.sqlEscapeString(prefix)).append("%'"); } sb.append(" ORDER BY ").append(COLUMN_DATE).append(" DESC") - .append(" LIMIT 5"); - Cursor cursor = mDatabase.rawQuery(sb.toString(), null); - int queryIndex = cursor.getColumnIndex(COLUMN_QUERY); - if (cursor.moveToFirst()) { - while (!cursor.isAfterLast()) { - String suggestion = cursor.getString(queryIndex); - if (!prefix.equals(suggestion)) { - queryList.add(suggestion); + .append(" LIMIT ").append(limit); + + try { + Cursor cursor = mDatabase.rawQuery(sb.toString(), null); + int queryIndex = cursor.getColumnIndex(COLUMN_QUERY); + if (cursor.moveToFirst()) { + while (!cursor.isAfterLast()) { + String suggestion = cursor.getString(queryIndex); + if (!prefix.equals(suggestion)) { + queryList.add(suggestion); + } + cursor.moveToNext(); } - cursor.moveToNext(); } + cursor.close(); + return queryList.toArray(new String[queryList.size()]); + } catch (SQLException e) { + return new String[0]; } - cursor.close(); - return queryList.toArray(new String[queryList.size()]); } public void addQuery(final String query) { diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SearchEditText.java b/app/src/main/java/com/hippo/ehviewer/widget/SearchEditText.java index 8e758ac3f..16b91c49c 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/SearchEditText.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/SearchEditText.java @@ -17,11 +17,12 @@ package com.hippo.ehviewer.widget; import android.content.Context; -import android.support.annotation.NonNull; -import android.support.v7.widget.AppCompatEditText; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.MotionEvent; +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatEditText; +import com.hippo.util.ExceptionUtils; public class SearchEditText extends AppCompatEditText { @@ -76,7 +77,14 @@ public boolean onTouchEvent(@NonNull MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP && mListener != null) { mListener.onClick(); } - return super.onTouchEvent(event); + try { + return super.onTouchEvent(event); + } catch (Throwable t) { + // Some devices crash here. + // I don't why. + ExceptionUtils.throwIfFatal(t); + return false; + } } public interface SearchEditTextListener { diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.java b/app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.java index 493ac94f1..aed164b40 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.java @@ -22,12 +22,6 @@ import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.SwitchCompat; import android.util.AttributeSet; import android.util.SparseArray; import android.view.LayoutInflater; @@ -37,7 +31,13 @@ import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; - +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.SwitchCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.hippo.android.resource.AttrResources; import com.hippo.easyrecyclerview.EasyRecyclerView; import com.hippo.easyrecyclerview.MarginItemDecoration; import com.hippo.ehviewer.R; @@ -46,7 +46,6 @@ import com.hippo.ripple.Ripple; import com.hippo.widget.RadioGridGroup; import com.hippo.yorozuya.ViewUtils; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -139,7 +138,7 @@ private void init(Context context) { mNormalSearchModeHelp = (ImageView) normalView.findViewById(R.id.normal_search_mode_help); mEnableAdvanceSwitch = (SwitchCompat) normalView.findViewById(R.id.search_enable_advance); mNormalSearchModeHelp.setOnClickListener(this); - Ripple.addRipple(mNormalSearchModeHelp, false); + Ripple.addRipple(mNormalSearchModeHelp, !AttrResources.getAttrBoolean(context, R.attr.isLightTheme)); mEnableAdvanceSwitch.setOnCheckedChangeListener(SearchLayout.this); mEnableAdvanceSwitch.setSwitchPadding(resources.getDimensionPixelSize(R.dimen.switch_padding)); @@ -169,6 +168,10 @@ public void setImageUri(Uri imageUri) { mImageView.setImageUri(imageUri); } + public void setNormalSearchMode(int id) { + mNormalSearchMode.check(id); + } + @Override public void onSelectImage() { if (mHelper != null) { @@ -233,10 +236,6 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { } } - public boolean isSpecifyGallery() { - return R.id.search_specify_gallery == mNormalSearchMode.getCheckedRadioButtonId(); - } - public void formatListUrlBuilder(ListUrlBuilder urlBuilder, String query) throws EhException { urlBuilder.reset(); @@ -248,6 +247,9 @@ public void formatListUrlBuilder(ListUrlBuilder urlBuilder, String query) throws case R.id.search_normal_search: urlBuilder.setMode(ListUrlBuilder.MODE_NORMAL); break; + case R.id.search_subscription_search: + urlBuilder.setMode(ListUrlBuilder.MODE_SUBSCRIPTION); + break; case R.id.search_specify_uploader: urlBuilder.setMode(ListUrlBuilder.MODE_UPLOADER); break; @@ -260,6 +262,8 @@ public void formatListUrlBuilder(ListUrlBuilder urlBuilder, String query) throws if (mEnableAdvance) { urlBuilder.setAdvanceSearch(mTableAdvanceSearch.getAdvanceSearch()); urlBuilder.setMinRating(mTableAdvanceSearch.getMinRating()); + urlBuilder.setPageFrom(mTableAdvanceSearch.getPageFrom()); + urlBuilder.setPageTo(mTableAdvanceSearch.getPageTo()); } break; case SEARCH_MODE_IMAGE: diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SeekBarPanel.java b/app/src/main/java/com/hippo/ehviewer/widget/SeekBarPanel.java index d5ba675aa..40f3f8634 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/SeekBarPanel.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/SeekBarPanel.java @@ -18,18 +18,18 @@ import android.content.Context; import android.graphics.Rect; -import android.support.annotation.NonNull; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.LinearLayout; import android.widget.SeekBar; - +import androidx.annotation.NonNull; import com.hippo.ehviewer.R; import com.hippo.yorozuya.ViewUtils; public class SeekBarPanel extends LinearLayout { private SeekBar mSeekBar; + private int[] mLocation = new int[2]; public SeekBarPanel(Context context) { super(context); @@ -54,8 +54,9 @@ public boolean onTouchEvent(@NonNull MotionEvent event) { if (mSeekBar == null) { return super.onTouchEvent(event); } else { - final float offsetX = -mSeekBar.getLeft(); - final float offsetY = -mSeekBar.getTop(); + ViewUtils.getLocationInAncestor(mSeekBar, mLocation, this); + final float offsetX = -mLocation[0]; + final float offsetY = -mLocation[1]; event.offsetLocation(offsetX, offsetY); mSeekBar.onTouchEvent(event); event.offsetLocation(-offsetX, -offsetY); diff --git a/app/src/main/java/com/hippo/ehviewer/widget/SimpleRatingView.java b/app/src/main/java/com/hippo/ehviewer/widget/SimpleRatingView.java index 277047c62..01fd47eb2 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/SimpleRatingView.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/SimpleRatingView.java @@ -58,9 +58,9 @@ public SimpleRatingView(Context context, AttributeSet attrs, int defStyleAttr) { private void init(Context context) { Resources resources = context.getResources(); - mStarDrawable = DrawableManager.getDrawable(context, R.drawable.v_star_x16); - mStarHalfDrawable = DrawableManager.getDrawable(context, R.drawable.v_star_half_x16); - mStarOutlineDrawable = DrawableManager.getDrawable(context, R.drawable.v_star_outline_x16); + mStarDrawable = DrawableManager.getVectorDrawable(context, R.drawable.v_star_x16); + mStarHalfDrawable = DrawableManager.getVectorDrawable(context, R.drawable.v_star_half_x16); + mStarOutlineDrawable = DrawableManager.getVectorDrawable(context, R.drawable.v_star_outline_x16); mRatingSize = resources.getDimensionPixelOffset(R.dimen.rating_size); mRatingInterval = resources.getDimensionPixelOffset(R.dimen.rating_interval); diff --git a/app/src/main/java/com/hippo/io/FileInputStreamPipe.java b/app/src/main/java/com/hippo/io/FileInputStreamPipe.java index 62b5f4c9f..f63b668fc 100644 --- a/app/src/main/java/com/hippo/io/FileInputStreamPipe.java +++ b/app/src/main/java/com/hippo/io/FileInputStreamPipe.java @@ -16,11 +16,9 @@ package com.hippo.io; -import android.support.annotation.NonNull; - +import androidx.annotation.NonNull; import com.hippo.streampipe.InputStreamPipe; import com.hippo.yorozuya.IOUtils; - import java.io.File; import java.io.FileInputStream; import java.io.IOException; diff --git a/app/src/main/java/com/hippo/io/FileOutputStreamPipe.java b/app/src/main/java/com/hippo/io/FileOutputStreamPipe.java index 1c7830e91..62c1c7a96 100644 --- a/app/src/main/java/com/hippo/io/FileOutputStreamPipe.java +++ b/app/src/main/java/com/hippo/io/FileOutputStreamPipe.java @@ -16,11 +16,9 @@ package com.hippo.io; -import android.support.annotation.NonNull; - +import androidx.annotation.NonNull; import com.hippo.streampipe.OutputStreamPipe; import com.hippo.yorozuya.IOUtils; - import java.io.File; import java.io.FileOutputStream; import java.io.IOException; diff --git a/app/src/main/java/com/hippo/io/UniFileInputStreamPipe.java b/app/src/main/java/com/hippo/io/UniFileInputStreamPipe.java index fbb68796e..6623fbb30 100644 --- a/app/src/main/java/com/hippo/io/UniFileInputStreamPipe.java +++ b/app/src/main/java/com/hippo/io/UniFileInputStreamPipe.java @@ -16,12 +16,10 @@ package com.hippo.io; -import android.support.annotation.NonNull; - +import androidx.annotation.NonNull; import com.hippo.streampipe.InputStreamPipe; import com.hippo.unifile.UniFile; import com.hippo.yorozuya.IOUtils; - import java.io.IOException; import java.io.InputStream; diff --git a/app/src/main/java/com/hippo/io/UniFileOutputStreamPipe.java b/app/src/main/java/com/hippo/io/UniFileOutputStreamPipe.java index 36387b140..37a56c28f 100644 --- a/app/src/main/java/com/hippo/io/UniFileOutputStreamPipe.java +++ b/app/src/main/java/com/hippo/io/UniFileOutputStreamPipe.java @@ -16,12 +16,10 @@ package com.hippo.io; -import android.support.annotation.NonNull; - +import androidx.annotation.NonNull; import com.hippo.streampipe.OutputStreamPipe; import com.hippo.unifile.UniFile; import com.hippo.yorozuya.IOUtils; - import java.io.IOException; import java.io.OutputStream; @@ -44,8 +42,9 @@ public void release() { // Empty } + @NonNull @Override - public @NonNull OutputStream open() throws IOException { + public OutputStream open() throws IOException { if (mOs != null) { throw new IllegalStateException("Please close it first"); } diff --git a/app/src/main/java/com/hippo/network/CookieDatabase.java b/app/src/main/java/com/hippo/network/CookieDatabase.java index c67ca35a8..b277316ae 100644 --- a/app/src/main/java/com/hippo/network/CookieDatabase.java +++ b/app/src/main/java/com/hippo/network/CookieDatabase.java @@ -26,8 +26,8 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteStatement; -import android.support.annotation.Nullable; import android.util.Log; +import androidx.annotation.Nullable; import com.hippo.database.MSQLiteBuilder; import com.hippo.util.SqlUtils; import java.util.ArrayList; diff --git a/app/src/main/java/com/hippo/network/CookieRepository.java b/app/src/main/java/com/hippo/network/CookieRepository.java index 9fab98c15..cea756ee7 100644 --- a/app/src/main/java/com/hippo/network/CookieRepository.java +++ b/app/src/main/java/com/hippo/network/CookieRepository.java @@ -31,7 +31,6 @@ import okhttp3.Cookie; import okhttp3.CookieJar; import okhttp3.HttpUrl; -import okhttp3.Request; public class CookieRepository implements CookieJar { @@ -86,6 +85,19 @@ public synchronized void addCookie(Cookie cookie) { } } + public String getCookieHeader(HttpUrl url) { + List cookies = getCookies(url); + StringBuilder cookieHeader = new StringBuilder(); + for (int i = 0, size = cookies.size(); i < size; i++) { + if (i > 0) { + cookieHeader.append("; "); + } + Cookie cookie = cookies.get(i); + cookieHeader.append(cookie.name()).append('=').append(cookie.value()); + } + return cookieHeader.toString(); + } + public synchronized List getCookies(HttpUrl url) { List accepted = new ArrayList<>(); List expired = new ArrayList<>(); @@ -146,7 +158,7 @@ public void saveFromResponse(HttpUrl httpUrl, List list) { } @Override - public List loadForRequest(HttpUrl httpUrl, Request request) { + public List loadForRequest(HttpUrl httpUrl) { return getCookies(httpUrl); } diff --git a/app/src/main/java/com/hippo/okhttp/ChromeRequestBuilder.java b/app/src/main/java/com/hippo/okhttp/ChromeRequestBuilder.java index ab2c10086..f391c1c23 100644 --- a/app/src/main/java/com/hippo/okhttp/ChromeRequestBuilder.java +++ b/app/src/main/java/com/hippo/okhttp/ChromeRequestBuilder.java @@ -16,18 +16,24 @@ package com.hippo.okhttp; -import java.net.MalformedURLException; -import java.net.URL; - import okhttp3.Request; public class ChromeRequestBuilder extends Request.Builder { - private static final String CHROME_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.103 Safari/537.36"; + private static final String CHROME_USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"; + + private static final String CHROME_ACCEPT = + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; + + private static final String CHROME_ACCEPT_LANGUAGE = + "en-US,en;q=0.5"; - public ChromeRequestBuilder(String url) throws MalformedURLException { - url(new URL(url)); + public ChromeRequestBuilder(String url) { + url(url); addHeader("User-Agent", CHROME_USER_AGENT); + addHeader("Accept", CHROME_ACCEPT); + addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE); } } diff --git a/app/src/main/java/com/hippo/preference/ActivityPreference.java b/app/src/main/java/com/hippo/preference/ActivityPreference.java index c67f66e6c..1fef14680 100644 --- a/app/src/main/java/com/hippo/preference/ActivityPreference.java +++ b/app/src/main/java/com/hippo/preference/ActivityPreference.java @@ -22,8 +22,9 @@ import android.preference.Preference; import android.util.AttributeSet; import android.util.Log; - +import android.widget.Toast; import com.hippo.ehviewer.R; +import com.hippo.util.ExceptionUtils; public class ActivityPreference extends Preference { @@ -68,6 +69,11 @@ protected void onClick() { Context context = getContext(); Intent intent = new Intent(context, mActivityClazz); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); + try { + context.startActivity(intent); + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); + Toast.makeText(context, R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show(); + } } } diff --git a/app/src/main/java/com/hippo/preference/DialogPreference.java b/app/src/main/java/com/hippo/preference/DialogPreference.java index b3de67128..98da46141 100644 --- a/app/src/main/java/com/hippo/preference/DialogPreference.java +++ b/app/src/main/java/com/hippo/preference/DialogPreference.java @@ -27,16 +27,15 @@ import android.os.Parcelable; import android.preference.Preference; import android.preference.PreferenceManager; -import android.support.annotation.DrawableRes; -import android.support.annotation.StringRes; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.AlertDialog; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.Window; import android.view.WindowManager; - +import androidx.annotation.DrawableRes; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; import com.hippo.ehviewer.R; /** diff --git a/app/src/main/java/com/hippo/preference/ListPreference.java b/app/src/main/java/com/hippo/preference/ListPreference.java index ed1035d9d..0d8ef31b6 100644 --- a/app/src/main/java/com/hippo/preference/ListPreference.java +++ b/app/src/main/java/com/hippo/preference/ListPreference.java @@ -21,11 +21,10 @@ import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; -import android.support.annotation.ArrayRes; -import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.util.AttributeSet; - +import androidx.annotation.ArrayRes; +import androidx.appcompat.app.AlertDialog; import com.hippo.ehviewer.R; public class ListPreference extends DialogPreference { diff --git a/app/src/main/java/com/hippo/preference/MessagePreference.java b/app/src/main/java/com/hippo/preference/MessagePreference.java index 0a8b5f6d2..fcc241fde 100644 --- a/app/src/main/java/com/hippo/preference/MessagePreference.java +++ b/app/src/main/java/com/hippo/preference/MessagePreference.java @@ -18,12 +18,11 @@ import android.content.Context; import android.content.res.TypedArray; -import android.support.v7.app.AlertDialog; import android.text.method.LinkMovementMethod; import android.util.AttributeSet; import android.view.View; import android.widget.TextView; - +import androidx.appcompat.app.AlertDialog; import com.hippo.ehviewer.R; import com.hippo.text.Html; diff --git a/app/src/main/java/com/hippo/preference/SwitchPreference.java b/app/src/main/java/com/hippo/preference/SwitchPreference.java index 8efddcc4f..8d7ee8f92 100644 --- a/app/src/main/java/com/hippo/preference/SwitchPreference.java +++ b/app/src/main/java/com/hippo/preference/SwitchPreference.java @@ -19,15 +19,13 @@ import android.content.Context; import android.content.res.TypedArray; import android.preference.TwoStatePreference; -import android.support.annotation.StringRes; -import android.support.v7.widget.SwitchCompat; import android.util.AttributeSet; import android.view.View; import android.widget.Checkable; import android.widget.CompoundButton; - +import androidx.annotation.StringRes; +import androidx.appcompat.widget.SwitchCompat; import com.hippo.ehviewer.R; - import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; diff --git a/app/src/main/java/com/hippo/scene/SceneFragment.java b/app/src/main/java/com/hippo/scene/SceneFragment.java index 40ef5a5c5..27468cf72 100644 --- a/app/src/main/java/com/hippo/scene/SceneFragment.java +++ b/app/src/main/java/com/hippo/scene/SceneFragment.java @@ -17,16 +17,15 @@ package com.hippo.scene; import android.os.Bundle; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentActivity; import android.view.View; - +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import com.hippo.android.resource.AttrResources; import com.hippo.ehviewer.R; import com.hippo.yorozuya.collect.IntList; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -102,7 +101,7 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); view.setTag(R.id.fragment_tag, getTag()); - view.setBackgroundColor(getResources().getColor(R.color.background_light)); + view.setBackgroundDrawable(AttrResources.getAttrDrawable(getContext(), android.R.attr.windowBackground)); // Notify FragmentActivity activity = getActivity(); diff --git a/app/src/main/java/com/hippo/scene/StageActivity.java b/app/src/main/java/com/hippo/scene/StageActivity.java index ff4f6b889..1bd2a4b80 100644 --- a/app/src/main/java/com/hippo/scene/StageActivity.java +++ b/app/src/main/java/com/hippo/scene/StageActivity.java @@ -18,20 +18,17 @@ import android.content.Intent; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; import android.util.Log; import android.view.View; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; import com.hippo.ehviewer.R; import com.hippo.ehviewer.ui.EhActivity; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.IntIdGenerator; - -import junit.framework.Assert; - import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -164,8 +161,10 @@ protected final void onCreate(@Nullable Bundle savedInstanceState) { if (savedInstanceState != null) { mStageId = savedInstanceState.getInt(KEY_STAGE_ID, IntIdGenerator.INVALID_ID); ArrayList list = savedInstanceState.getStringArrayList(KEY_SCENE_TAG_LIST); - mSceneTagList.addAll(list); - mDelaySceneTagList.addAll(list); + if (list != null) { + mSceneTagList.addAll(list); + mDelaySceneTagList.addAll(list); + } mIdGenerator.lazySet(savedInstanceState.getInt(KEY_NEXT_ID)); } @@ -231,6 +230,9 @@ protected void onRegister(int id) { protected void onUnregister() { } + protected void onTransactScene() { + } + public int getStageId() { return mStageId; } @@ -311,6 +313,7 @@ public void startScene(Announcer announcer) { // Commit transaction.commitAllowingStateLoss(); + onTransactScene(); // New arguments if (args != null && fragment instanceof SceneFragment) { @@ -330,7 +333,7 @@ public void startScene(Announcer announcer) { String tag = mSceneTagList.get(mSceneTagList.size() - 1); Fragment fragment = fragmentManager.findFragmentByTag(tag); if (fragment != null) { - Assert.assertTrue(SceneFragment.class.isInstance(fragment)); + AssertUtils.assertTrue(SceneFragment.class.isInstance(fragment)); currentScene = (SceneFragment) fragment; } } @@ -384,6 +387,7 @@ public void startScene(Announcer announcer) { // Commit transaction.commitAllowingStateLoss(); + onTransactScene(); // Check request if (announcer.requestFrom != null) { @@ -460,6 +464,7 @@ public void startSceneFirstly(Announcer announcer) { // Commit transaction.commitAllowingStateLoss(); + onTransactScene(); if (!createNewScene && args != null) { // TODO Call onNewArguments when view created ? @@ -538,6 +543,7 @@ private void finishScene(String tag, TransitionHelper transitionHelper) { } transaction.remove(scene); transaction.commitAllowingStateLoss(); + onTransactScene(); // Remove tag mSceneTagList.remove(index); @@ -562,6 +568,7 @@ public void refreshTopScene() { transaction.detach(fragment); transaction.attach(fragment); transaction.commitAllowingStateLoss(); + onTransactScene(); } @Override diff --git a/app/src/main/java/com/hippo/scene/TransitionHelper.java b/app/src/main/java/com/hippo/scene/TransitionHelper.java index 961cb264a..e7a384e70 100644 --- a/app/src/main/java/com/hippo/scene/TransitionHelper.java +++ b/app/src/main/java/com/hippo/scene/TransitionHelper.java @@ -17,8 +17,8 @@ package com.hippo.scene; import android.content.Context; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentTransaction; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; public interface TransitionHelper { diff --git a/app/src/main/java/com/hippo/text/Html.java b/app/src/main/java/com/hippo/text/Html.java index 4e69e7342..8811b79e5 100644 --- a/app/src/main/java/com/hippo/text/Html.java +++ b/app/src/main/java/com/hippo/text/Html.java @@ -22,8 +22,6 @@ import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.Drawable; -import android.support.annotation.ColorInt; -import android.support.annotation.NonNull; import android.text.Editable; import android.text.Layout; import android.text.Spannable; @@ -46,9 +44,12 @@ import android.text.style.TypefaceSpan; import android.text.style.URLSpan; import android.text.style.UnderlineSpan; - -import com.hippo.ehviewer.R; - +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import java.io.IOException; +import java.io.StringReader; +import java.util.HashMap; +import java.util.Locale; import org.ccil.cowan.tagsoup.HTMLSchema; import org.ccil.cowan.tagsoup.Parser; import org.xml.sax.Attributes; @@ -58,11 +59,6 @@ import org.xml.sax.SAXException; import org.xml.sax.XMLReader; -import java.io.IOException; -import java.io.StringReader; -import java.util.HashMap; -import java.util.Locale; - /** * This class processes HTML strings into displayable styled text. * Not all HTML tags are supported. @@ -678,7 +674,7 @@ private static void startImg(SpannableStringBuilder text, } if (d == null) { - d = Html.sResources.getDrawable(R.drawable.image_failed); + d = Resources.getSystem().getDrawable(android.R.drawable.ic_menu_report_image); d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); } @@ -717,11 +713,13 @@ private static void endFont(SpannableStringBuilder text) { where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } else { - int c = getHtmlColor(f.mColor); - if (c != -1) { + try { + int c = getHtmlColor(f.mColor); text.setSpan(new ForegroundColorSpan(c | 0xFF000000), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } catch (IllegalArgumentException e) { + // Ignore } } } @@ -910,16 +908,19 @@ public Header(int level) { */ @ColorInt public static int getHtmlColor(@NonNull String colorString) { - if (colorString.charAt(0) == '#') { + if ((colorString.length() == 7 || colorString.length() == 9) && colorString.charAt(0) == '#') { // Use a long to avoid rollovers on #ffXXXXXX - long color = Long.parseLong(colorString.substring(1), 16); + long color; + try { + color = Long.parseLong(colorString.substring(1), 16); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Unknown color: " + colorString); + } if (colorString.length() == 7) { // Set the alpha value color |= 0x00000000ff000000; - } else if (colorString.length() != 9) { - throw new IllegalArgumentException("Unknown color: " + colorString); } - return (int)color; + return (int) color; } else if (colorString.startsWith("rgb(") && colorString.endsWith(")")) { String str = colorString.substring(4, colorString.length() - 1); String[] colors = str.split("[\\s]*,[\\s]*"); diff --git a/app/src/main/java/com/hippo/util/AppHelper.java b/app/src/main/java/com/hippo/util/AppHelper.java index 490327ca6..2ba53ceac 100644 --- a/app/src/main/java/com/hippo/util/AppHelper.java +++ b/app/src/main/java/com/hippo/util/AppHelper.java @@ -19,16 +19,14 @@ import android.app.Activity; import android.app.Dialog; import android.app.Service; -import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.hippo.ehviewer.R; public class AppHelper { @@ -47,7 +45,8 @@ public static boolean sendEmail(@NonNull Activity from, @NonNull String address, try { from.startActivity(i); return true; - } catch (ActivityNotFoundException e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); Toast.makeText(from, R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show(); return false; } @@ -58,11 +57,13 @@ public static boolean share(@NonNull Activity from, String text) { sendIntent.setAction(Intent.ACTION_SEND); sendIntent.putExtra(Intent.EXTRA_TEXT, text); sendIntent.setType("text/plain"); + Intent chooser = Intent.createChooser(sendIntent, from.getString(R.string.share)); try { - from.startActivity(sendIntent); + from.startActivity(chooser); return true; - } catch (ActivityNotFoundException e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); Toast.makeText(from, R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show(); return false; } diff --git a/app/src/main/java/com/hippo/util/BitmapUtils.java b/app/src/main/java/com/hippo/util/BitmapUtils.java index 7544e79a5..838b3e4ca 100644 --- a/app/src/main/java/com/hippo/util/BitmapUtils.java +++ b/app/src/main/java/com/hippo/util/BitmapUtils.java @@ -20,11 +20,9 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.support.annotation.NonNull; - +import androidx.annotation.NonNull; import com.hippo.streampipe.InputStreamPipe; import com.hippo.yorozuya.MathUtils; - import java.io.IOException; public final class BitmapUtils { diff --git a/app/src/main/java/com/hippo/util/DrawableManager.java b/app/src/main/java/com/hippo/util/DrawableManager.java index 06277c575..e25a97596 100644 --- a/app/src/main/java/com/hippo/util/DrawableManager.java +++ b/app/src/main/java/com/hippo/util/DrawableManager.java @@ -20,18 +20,15 @@ import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Build; -import android.support.annotation.DrawableRes; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.graphics.drawable.VectorDrawableCompat; -import android.support.v7.widget.AppCompatDrawableManager; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; public final class DrawableManager { - private static final AppCompatDrawableManager sManager = AppCompatDrawableManager.get(); - - public static Drawable getDrawable(@NonNull Context context, @DrawableRes int resId) { - return sManager.getDrawable(context, resId); + public static Drawable getVectorDrawable(@NonNull Context context, @DrawableRes int resId) { + return getVectorDrawable(context.getResources(), resId, context.getTheme()); } public static Drawable getVectorDrawable(@NonNull Resources res, diff --git a/app/src/main/java/com/hippo/util/ExceptionUtils.java b/app/src/main/java/com/hippo/util/ExceptionUtils.java index b6a7d8fad..963a344a8 100644 --- a/app/src/main/java/com/hippo/util/ExceptionUtils.java +++ b/app/src/main/java/com/hippo/util/ExceptionUtils.java @@ -16,28 +16,26 @@ package com.hippo.util; - -import android.support.annotation.NonNull; - +import androidx.annotation.NonNull; import com.hippo.ehviewer.GetText; import com.hippo.ehviewer.R; import com.hippo.ehviewer.client.exception.EhException; import com.hippo.network.StatusCodeException; - -import org.apache.http.conn.ConnectTimeoutException; - import java.net.MalformedURLException; import java.net.ProtocolException; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; +import javax.net.ssl.SSLException; +import org.apache.http.conn.ConnectTimeoutException; public final class ExceptionUtils { private static final String TAG = ExceptionUtils.class.getSimpleName(); @NonNull - public static String getReadableString(@NonNull Exception e) { + public static String getReadableString(@NonNull Throwable e) { + e.printStackTrace(); if (e instanceof MalformedURLException) { return GetText.getString(R.string.error_invalid_url); } else if (e instanceof ConnectTimeoutException || @@ -55,7 +53,7 @@ public static String getReadableString(@NonNull Exception e) { return sb.toString(); } else if (e instanceof ProtocolException && e.getMessage().startsWith("Too many follow-up requests:")) { return GetText.getString(R.string.error_redirection); - } else if (e instanceof ProtocolException || e instanceof SocketException) { + } else if (e instanceof ProtocolException || e instanceof SocketException || e instanceof SSLException) { return GetText.getString(R.string.error_socket); } else if (e instanceof EhException) { return e.getMessage(); @@ -63,4 +61,15 @@ public static String getReadableString(@NonNull Exception e) { return GetText.getString(R.string.error_unknown); } } + + public static void throwIfFatal(@NonNull Throwable t) { + // values here derived from https://github.com/ReactiveX/RxJava/issues/748#issuecomment-32471495 + if (t instanceof VirtualMachineError) { + throw (VirtualMachineError) t; + } else if (t instanceof ThreadDeath) { + throw (ThreadDeath) t; + } else if (t instanceof LinkageError) { + throw (LinkageError) t; + } + } } diff --git a/app/src/main/java/com/hippo/util/IoThreadPoolExecutor.java b/app/src/main/java/com/hippo/util/IoThreadPoolExecutor.java new file mode 100644 index 000000000..bac34cc9b --- /dev/null +++ b/app/src/main/java/com/hippo/util/IoThreadPoolExecutor.java @@ -0,0 +1,90 @@ +/* + * Copyright 2019 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.util; + +import androidx.annotation.NonNull; +import com.hippo.yorozuya.thread.PriorityThreadFactory; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class IoThreadPoolExecutor extends ThreadPoolExecutor { + + private final static ThreadPoolExecutor INSTANCE = + IoThreadPoolExecutor.newInstance(3, 32, 1L, TimeUnit.SECONDS, + new PriorityThreadFactory("IO",android.os.Process.THREAD_PRIORITY_BACKGROUND)); + + private IoThreadPoolExecutor( + int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler + ) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); + } + + public static ThreadPoolExecutor getInstance() { + return INSTANCE; + } + + private static ThreadPoolExecutor newInstance( + int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + ThreadFactory threadFactory + ) { + ThreadQueue queue = new ThreadQueue(); + PutRunnableBackHandler handler = new PutRunnableBackHandler(); + IoThreadPoolExecutor executor = new IoThreadPoolExecutor( + corePoolSize, maximumPoolSize, keepAliveTime, unit, queue, threadFactory, handler); + queue.setThreadPoolExecutor(executor); + return executor; + } + + private static class ThreadQueue extends LinkedBlockingQueue { + + private ThreadPoolExecutor executor; + + void setThreadPoolExecutor(ThreadPoolExecutor executor) { + this.executor = executor; + } + + @Override + public boolean offer(@NonNull Runnable o) { + int allWorkingThreads = executor.getActiveCount() + super.size(); + return allWorkingThreads < executor.getPoolSize() && super.offer(o); + } + } + + public static class PutRunnableBackHandler implements RejectedExecutionHandler { + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + try { + executor.getQueue().put(r); + } catch (InterruptedException e) { + throw new RejectedExecutionException(e); + } + } + } +} diff --git a/app/src/main/java/com/hippo/util/JsoupUtils.java b/app/src/main/java/com/hippo/util/JsoupUtils.java index d5371096b..e2ed8c5b8 100644 --- a/app/src/main/java/com/hippo/util/JsoupUtils.java +++ b/app/src/main/java/com/hippo/util/JsoupUtils.java @@ -16,8 +16,7 @@ package com.hippo.util; -import android.support.annotation.Nullable; - +import androidx.annotation.Nullable; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; diff --git a/app/src/main/java/com/hippo/util/ApiHelper.java b/app/src/main/java/com/hippo/util/MutableBoolean.java similarity index 75% rename from app/src/main/java/com/hippo/util/ApiHelper.java rename to app/src/main/java/com/hippo/util/MutableBoolean.java index 4d9d42eae..e76150eb3 100644 --- a/app/src/main/java/com/hippo/util/ApiHelper.java +++ b/app/src/main/java/com/hippo/util/MutableBoolean.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 Hippo Seven + * Copyright 2019 Hippo Seven * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,11 @@ package com.hippo.util; -import android.os.Build; +public class MutableBoolean { -public final class ApiHelper { + public boolean value; - public static final boolean SUPPORT_TRANSITION = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + public MutableBoolean(boolean value) { + this.value = value; + } } diff --git a/app/src/main/java/com/hippo/util/NaturalComparator.java b/app/src/main/java/com/hippo/util/NaturalComparator.java new file mode 100644 index 000000000..faad7c353 --- /dev/null +++ b/app/src/main/java/com/hippo/util/NaturalComparator.java @@ -0,0 +1,152 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.util; + +import java.util.Comparator; + +/** + * Implements natural sort order. + */ +public class NaturalComparator implements Comparator { + + @Override + public int compare(String o1, String o2) { + if (o1 == null && o2 == null) { + return 0; + } + if (o1 == null) { + return -1; + } + if (o2 == null) { + return 1; + } + + int index1 = 0; + int index2 = 0; + while (true) { + String data1 = nextSlice(o1, index1); + String data2 = nextSlice(o2, index2); + + if (data1 == null && data2 == null) { + return 0; + } + if (data1 == null) { + return -1; + } + if (data2 == null) { + return 1; + } + + index1 += data1.length(); + index2 += data2.length(); + + int result; + if (isDigit(data1) && isDigit(data2)) { + result = compareNumberString(data1, data2); + } else { + result = data1.compareToIgnoreCase(data2); + } + + if (result != 0) { + return result; + } + } + } + + private static boolean isDigit(String str) { + // Just check the first char + char ch = str.charAt(0); + return ch >= '0' && ch <= '9'; + } + + private static String nextSlice(String str, int index) { + int length = str.length(); + if (index == length) { + return null; + } + + char ch = str.charAt(index); + if (ch == '.' || ch == ' ') { + return str.substring(index, index + 1); + } else if (ch >= '0' && ch <= '9') { + return str.substring(index, nextNumberBound(str, index + 1)); + } else { + return str.substring(index, nextOtherBound(str, index + 1)); + } + } + + private static int nextNumberBound(String str, int index) { + for (int length = str.length(); index < length; index++) { + char ch = str.charAt(index); + if (ch < '0' || ch > '9') { + break; + } + } + return index; + } + + private static int nextOtherBound(String str, int index) { + for (int length = str.length(); index < length; index++) { + char ch = str.charAt(index); + if (ch == '.' || ch == ' ' || (ch >= '0' && ch <= '9')) { + break; + } + } + return index; + } + + private static String removeLeadingZero(String s) { + if (s.length() < 1) { + return s; + } + + // At least keep the last number + for (int i = 0, n = s.length() - 1; i < n; i++) { + if (s.charAt(i) != '0') { + return s.substring(i); + } + } + + return s.substring(s.length() - 1); + } + + private static int compareNumberString(String s1, String s2) { + String p1 = removeLeadingZero(s1); + String p2 = removeLeadingZero(s2); + + int l1 = p1.length(); + int l2 = p2.length(); + + if (l1 > l2) { + return 1; + } else if (l1 < l2) { + return -1; + } else { + for (int i = 0; i < l1; i++) { + char c1 = p1.charAt(i); + char c2 = p2.charAt(i); + if (c1 > c2) { + return 1; + } else if (c1 < c2) { + return -1; + } + } + } + + return -Integer.compare(s1.length(), s2.length()); + } +} diff --git a/app/src/main/java/com/hippo/util/PackageUtils.java b/app/src/main/java/com/hippo/util/PackageUtils.java index 296a136dc..364b6849d 100644 --- a/app/src/main/java/com/hippo/util/PackageUtils.java +++ b/app/src/main/java/com/hippo/util/PackageUtils.java @@ -40,7 +40,8 @@ public static String getSignature(Context context, String packageName) { } else { Log.e(TAG, "Can't find signature in package " + packageName); } - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); Log.e(TAG, "Can't find package " + packageName, e); } return null; diff --git a/app/src/main/java/com/hippo/util/PathNaturalComparator.java b/app/src/main/java/com/hippo/util/PathNaturalComparator.java new file mode 100644 index 000000000..62d15a39f --- /dev/null +++ b/app/src/main/java/com/hippo/util/PathNaturalComparator.java @@ -0,0 +1,104 @@ +package com.hippo.util; + +import java.util.Comparator; + +/** + * Compares each path segment in natural sort order. + * {@code \} and {@code /} are the same. Duplicate {@code \} and {@code /} are ignored. + * Leading {@code \} and {@code /} are ignored, so there is no difference between + * relative path and absolute path. + * {@code /./} and {@code /../} are treated as normal path. + */ +public class PathNaturalComparator implements Comparator { + + private static final boolean TYPE_SEPARATOR = false; + private static final boolean TYPE_NORMAL = true; + + private Comparator naturalComparator = new NaturalComparator(); + + @Override + public int compare(String o1, String o2) { + if (o1 == null && o2 == null) { + return 0; + } + if (o1 == null) { + return -1; + } + if (o2 == null) { + return 1; + } + + int index1 = 0; + int index2 = 0; + + while (true) { + String data1; + String data2; + + for (;;) { + int newIndex1 = nextSegmentStart(o1, index1); + if (newIndex1 == index1) { + data1 = null; + break; + } + if (getType(o1.charAt(newIndex1 - 1)) == TYPE_NORMAL) { + data1 = o1.substring(index1, newIndex1); + index1 = newIndex1; + break; + } + index1 = newIndex1; + } + + for (;;) { + int newIndex2 = nextSegmentStart(o2, index2); + if (newIndex2 == index2) { + data2 = null; + break; + } + if (getType(o2.charAt(newIndex2 - 1)) == TYPE_NORMAL) { + data2 = o2.substring(index2, newIndex2); + index2 = newIndex2; + break; + } + index2 = newIndex2; + } + + if (data1 == null && data2 == null) { + return 0; + } + if (data1 == null) { + return -1; + } + if (data2 == null) { + return 1; + } + + int result = naturalComparator.compare(data1, data2); + if (result != 0) { + return result; + } + } + } + + private static int nextSegmentStart(String str, int index) { + if (index >= str.length()) { + return index; + } + + boolean type = getType(str.charAt(index)); + int i = index + 1; + while (i < str.length() && type == getType(str.charAt(i))) { + i += 1; + } + + return i; + } + + private static boolean getType(char c) { + if (c == '/' || c == '\\') { + return TYPE_SEPARATOR; + } else { + return TYPE_NORMAL; + } + } +} diff --git a/app/src/main/java/com/hippo/util/PermissionRequester.java b/app/src/main/java/com/hippo/util/PermissionRequester.java index 2863e0c48..45c20a627 100644 --- a/app/src/main/java/com/hippo/util/PermissionRequester.java +++ b/app/src/main/java/com/hippo/util/PermissionRequester.java @@ -19,14 +19,14 @@ import android.app.Activity; import android.content.DialogInterface; import android.content.pm.PackageManager; -import android.support.v4.app.ActivityCompat; -import android.support.v7.app.AlertDialog; +import androidx.appcompat.app.AlertDialog; +import androidx.core.app.ActivityCompat; public class PermissionRequester { /** * @return true for there no need to request, do your work it now. - * false for do in {@link android.support.v4.app.ActivityCompat.OnRequestPermissionsResultCallback#onRequestPermissionsResult(int, String[], int[])} + * false for do in {@link androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback#onRequestPermissionsResult(int, String[], int[])} */ public static boolean request(final Activity activity, final String permission, String rationale, final int requestCode) { if (!(activity instanceof ActivityCompat.OnRequestPermissionsResultCallback)) { @@ -44,18 +44,28 @@ public static boolean request(final Activity activity, final String permission, .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - ActivityCompat.requestPermissions(activity, - new String[]{permission}, - requestCode); + requestPermissions(activity, new String[]{permission}, requestCode); } }).setNegativeButton(android.R.string.cancel, null) .show(); } else { - ActivityCompat.requestPermissions(activity, - new String[]{permission}, - requestCode); + return requestPermissions(activity, new String[]{permission}, requestCode); } return false; } + + private static boolean requestPermissions( + final Activity activity, + final String[] permissions, + int requestCode + ) { + try { + ActivityCompat.requestPermissions(activity, permissions, requestCode); + return true; + } catch (Throwable t) { + ExceptionUtils.throwIfFatal(t); + return false; + } + } } diff --git a/app/src/main/java/com/hippo/util/ReadableTime.java b/app/src/main/java/com/hippo/util/ReadableTime.java index 8b2dca541..10ab35d00 100644 --- a/app/src/main/java/com/hippo/util/ReadableTime.java +++ b/app/src/main/java/com/hippo/util/ReadableTime.java @@ -16,17 +16,16 @@ package com.hippo.util; +import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; - import com.hippo.ehviewer.R; - import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.Locale; -import java.util.TimeZone; +@SuppressLint("SimpleDateFormat") public final class ReadableTime { private static Resources sResources; @@ -56,30 +55,21 @@ public final class ReadableTime { R.plurals.second }; - private static final Calendar sCalendar = Calendar.getInstance(TimeZone.getTimeZone("GMT+08:00")); + private static final Calendar sCalendar = Calendar.getInstance(); private static final Object sCalendarLock = new Object(); - private static final SimpleDateFormat DATE_FORMAT_WITHOUT_YEAR = - new SimpleDateFormat("MMM d", Locale.getDefault()); + private static final SimpleDateFormat DATE_FORMAT_WITHOUT_YEAR = new SimpleDateFormat("MMM d"); + private static final SimpleDateFormat DATE_FORMAT_WITH_YEAR = new SimpleDateFormat("MMM d, yyyy"); - private static final SimpleDateFormat DATE_FORMAT_WIT_YEAR = - new SimpleDateFormat("yyy MMM d", Locale.getDefault()); + private static final SimpleDateFormat DATE_FORMAT_WITHOUT_YEAR_ZH = new SimpleDateFormat("M月d日"); + private static final SimpleDateFormat DATE_FORMAT_WITH_YEAR_ZH = new SimpleDateFormat("yyyy年M月d日"); - private static final SimpleDateFormat DATE_FORMAT = - new SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault()); + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yy-MM-dd HH:mm"); private static final Object sDateFormatLock1 = new Object(); - private static final SimpleDateFormat FILENAMABLE_DATE_FORMAT = - new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.getDefault()); + private static final SimpleDateFormat FILENAMABLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS"); private static final Object sDateFormatLock2 = new Object(); - static { - // The website use GMT+08:00, so tell user the same - DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT+08:00")); - - DATE_FORMAT_WITHOUT_YEAR.setTimeZone(TimeZone.getTimeZone("GMT+08:00")); - } - public static void initialize(Context context) { sResources = context.getApplicationContext().getResources(); } @@ -134,11 +124,12 @@ public static String getTimeAgo(long time) { int nowYear = sCalendar.get(Calendar.YEAR); sCalendar.setTime(timeDate); int timeYear = sCalendar.get(Calendar.YEAR); + boolean isZh = Locale.getDefault().getLanguage().equals("zh"); if (nowYear == timeYear) { - return DATE_FORMAT_WITHOUT_YEAR.format(timeDate); + return (isZh ? DATE_FORMAT_WITHOUT_YEAR_ZH : DATE_FORMAT_WITHOUT_YEAR).format(timeDate); } else { - return DATE_FORMAT_WIT_YEAR.format(timeDate); + return (isZh ? DATE_FORMAT_WITH_YEAR_ZH : DATE_FORMAT_WITH_YEAR).format(timeDate); } } } diff --git a/app/src/main/java/com/hippo/util/TextUrl.java b/app/src/main/java/com/hippo/util/TextUrl.java index f9461dae9..a7cd35d3d 100644 --- a/app/src/main/java/com/hippo/util/TextUrl.java +++ b/app/src/main/java/com/hippo/util/TextUrl.java @@ -20,11 +20,14 @@ import android.text.SpannableString; import android.text.style.URLSpan; +import java.nio.charset.Charset; import java.util.regex.Matcher; import java.util.regex.Pattern; public final class TextUrl { + public static final Charset UTF_8 = Charset.forName("UTF-8"); + private static final Pattern URL_PATTERN = Pattern.compile("(http|https)://[a-z0-9A-Z%-]+(\\.[a-z0-9A-Z%-]+)+(:\\d{1,5})?(/[a-zA-Z0-9-_~:#@!&',;=%/\\*\\.\\?\\+\\$\\[\\]\\(\\)]+)?/?"); public static CharSequence handleTextUrl(CharSequence content) { diff --git a/app/src/main/java/com/hippo/widget/BatteryView.java b/app/src/main/java/com/hippo/widget/BatteryView.java index c8a4b7fcc..d3e04a19d 100644 --- a/app/src/main/java/com/hippo/widget/BatteryView.java +++ b/app/src/main/java/com/hippo/widget/BatteryView.java @@ -25,12 +25,11 @@ import android.graphics.Color; import android.os.BatteryManager; import android.util.AttributeSet; -import android.widget.TextView; - +import androidx.appcompat.widget.AppCompatTextView; import com.hippo.drawable.BatteryDrawable; import com.hippo.ehviewer.R; -public class BatteryView extends TextView { +public class BatteryView extends AppCompatTextView { private int mColor; private int mWarningColor; diff --git a/app/src/main/java/com/hippo/widget/CheckTextView.java b/app/src/main/java/com/hippo/widget/CheckTextView.java index 3ec309f94..92d653597 100644 --- a/app/src/main/java/com/hippo/widget/CheckTextView.java +++ b/app/src/main/java/com/hippo/widget/CheckTextView.java @@ -26,13 +26,12 @@ import android.os.Build; import android.os.Bundle; import android.os.Parcelable; -import android.support.annotation.NonNull; import android.util.AttributeSet; import android.view.View; import android.view.View.OnClickListener; import android.view.animation.Interpolator; -import android.widget.TextView; - +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatTextView; import com.hippo.ehviewer.R; import com.hippo.hotspot.Hotspot; import com.hippo.hotspot.Hotspotable; @@ -40,7 +39,7 @@ import com.hippo.yorozuya.MathUtils; import com.hippo.yorozuya.SimpleAnimatorListener; -public class CheckTextView extends TextView implements OnClickListener, Hotspotable { +public class CheckTextView extends AppCompatTextView implements OnClickListener, Hotspotable { private static final String STATE_KEY_SUPER = "super"; private static final String STATE_KEY_CHECKED = "checked"; diff --git a/app/src/main/java/com/hippo/widget/ContentLayout.java b/app/src/main/java/com/hippo/widget/ContentLayout.java index 80dbeee91..49e0a5db3 100644 --- a/app/src/main/java/com/hippo/widget/ContentLayout.java +++ b/app/src/main/java/com/hippo/widget/ContentLayout.java @@ -20,8 +20,6 @@ import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Parcelable; -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; @@ -30,7 +28,10 @@ import android.widget.FrameLayout; import android.widget.TextView; import android.widget.Toast; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; +import com.hippo.android.resource.AttrResources; import com.hippo.easyrecyclerview.EasyRecyclerView; import com.hippo.easyrecyclerview.FastScroller; import com.hippo.easyrecyclerview.HandlerDrawable; @@ -43,10 +44,10 @@ import com.hippo.view.ViewTransition; import com.hippo.yorozuya.IntIdGenerator; import com.hippo.yorozuya.LayoutUtils; -import com.hippo.yorozuya.ResourcesUtils; import com.hippo.yorozuya.collect.IntList; - import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; import java.util.List; public class ContentLayout extends FrameLayout { @@ -92,7 +93,7 @@ private void init(Context context) { mFastScroller.attachToRecyclerView(mRecyclerView); HandlerDrawable drawable = new HandlerDrawable(); - drawable.setColor(ResourcesUtils.getAttrColor(context, R.attr.colorAccent)); + drawable.setColor(AttrResources.getAttrColor(context, R.attr.widgetColorThemeAccent)); mFastScroller.setHandlerDrawable(drawable); mRefreshLayout.setHeaderColorSchemeResources( @@ -168,6 +169,8 @@ public abstract static class ContentHelper implements View private static final String TAG = ContentHelper.class.getSimpleName(); + private static final int CHECK_DUPLICATE_RANGE = 50; + private static final String KEY_SUPER = "super"; private static final String KEY_SHOWN_VIEW = "shown_view"; private static final String KEY_TIP = "tip"; @@ -233,6 +236,8 @@ public abstract static class ContentHelper implements View */ private int mPages; + private int mNextPage; + private int mCurrentTaskId; private int mCurrentTaskType; private int mCurrentTaskPage; @@ -269,6 +274,13 @@ public void onHeaderRefresh() { public void onFooterRefresh() { if (mEndPage < mPages) { // Get next page + // Fill pages before NextPage with empty list + while (mNextPage > mEndPage && mEndPage < mPages) { + mCurrentTaskId = mIdGenerator.nextId(); + mCurrentTaskType = TYPE_NEXT_PAGE_KEEP_POS; + mCurrentTaskPage = mEndPage; + onGetPageData(mCurrentTaskId, mPages, mNextPage, Collections.emptyList()); + } mCurrentTaskId = mIdGenerator.nextId(); mCurrentTaskType = TYPE_NEXT_PAGE_KEEP_POS; mCurrentTaskPage = mEndPage; @@ -281,6 +293,7 @@ public void onFooterRefresh() { getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage); } else { Log.e(TAG, "Try to footer refresh, but mEndPage = " + mEndPage + ", mPages = " + mPages); + mRefreshLayout.setFooterRefreshing(false); } } }; @@ -303,7 +316,7 @@ private void init(ContentLayout contentLayout) { mRefreshLayout = contentLayout.mRefreshLayout; mRecyclerView = contentLayout.mRecyclerView; - Drawable drawable = DrawableManager.getDrawable(getContext(), R.drawable.big_weird_face); + Drawable drawable = DrawableManager.getVectorDrawable(getContext(), R.drawable.big_sad_pandroid); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); mTipView.setCompoundDrawables(null, drawable, null, null); @@ -322,7 +335,7 @@ public void onClick(View v) { } /** - * Call {@link #onGetPageData(int, List)} when get data + * Call {@link #onGetPageData(int, int, int, List)} when get data * * @param taskId task id * @param page the page to get @@ -370,6 +383,15 @@ public E getDataAt(int location) { return mData.get(location); } + @Nullable + public E getDataAtEx(int location) { + if (location >= 0 && location < mData.size()) { + return mData.get(location); + } else { + return null; + } + } + public int size() { return mData.size(); } @@ -378,19 +400,13 @@ public boolean isCurrentTask(int taskId) { return mCurrentTaskId == taskId; } - public void setPages(int taskId, int pages) { - // TODO what it pages > mEndPage - if (mCurrentTaskId == taskId) { - mPages = pages; - } - } - public int getPages() { return mPages; } public void addAt(int index, E data) { mData.add(index, data); + onAddData(data); for (int i = 0, n = mPageDivider.size(); i < n; i++) { int divider = mPageDivider.get(i); @@ -403,7 +419,8 @@ public void addAt(int index, E data) { } public void removeAt(int index) { - mData.remove(index); + E data = mData.remove(index); + onRemoveData(data); for (int i = 0, n = mPageDivider.size(); i < n; i++) { int divider = mPageDivider.get(i); @@ -415,7 +432,33 @@ public void removeAt(int index) { notifyItemRangeRemoved(index, 1); } - public void onGetPageData(int taskId, List data) { + protected abstract boolean isDuplicate(E d1, E d2); + + private void removeDuplicateData(List data, int start, int end) { + start = Math.max(0, start); + end = Math.min(mData.size(), end); + for (Iterator iterator = data.iterator(); iterator.hasNext();) { + E d = iterator.next(); + for (int i = start; i < end; i++) { + if (isDuplicate(d, mData.get(i))) { + iterator.remove(); + break; + } + } + } + } + + protected void onAddData(E data) { } + + protected void onAddData(List data) { } + + protected void onRemoveData(E data) { } + + protected void onRemoveData(List data) { } + + protected void onClearData() { } + + public void onGetPageData(int taskId, int pages, int nextPage, List data) { if (mCurrentTaskId == taskId) { int dataSize; @@ -423,11 +466,14 @@ public void onGetPageData(int taskId, List data) { case TYPE_REFRESH: mStartPage = 0; mEndPage = 1; + mPages = pages; + mNextPage = nextPage; mPageDivider.clear(); mPageDivider.add(data.size()); if (data.isEmpty()) { mData.clear(); + onClearData(); notifyDataSetChanged(); if (true || mEndPage >= mPages) { // Not found @@ -449,7 +495,9 @@ public void onGetPageData(int taskId, List data) { } } else { mData.clear(); + onClearData(); mData.addAll(data); + onAddData(data); notifyDataSetChanged(); // Ui change, show content @@ -467,12 +515,14 @@ public void onGetPageData(int taskId, List data) { break; case TYPE_PRE_PAGE: case TYPE_PRE_PAGE_KEEP_POS: + removeDuplicateData(data, 0, CHECK_DUPLICATE_RANGE); dataSize = data.size(); for (int i = 0, n = mPageDivider.size(); i < n; i++) { mPageDivider.set(i, mPageDivider.get(i) + dataSize); } mPageDivider.add(0, dataSize); mStartPage--; + mPages = Math.max(mEndPage, pages); // assert mStartPage >= 0 if (data.isEmpty()) { @@ -506,6 +556,7 @@ public void onGetPageData(int taskId, List data) { } } else { mData.addAll(0, data); + onAddData(data); notifyItemRangeInserted(0, data.size()); // Ui change, show content @@ -529,10 +580,13 @@ public void onGetPageData(int taskId, List data) { break; case TYPE_NEXT_PAGE: case TYPE_NEXT_PAGE_KEEP_POS: + removeDuplicateData(data, mData.size() - CHECK_DUPLICATE_RANGE, mData.size()); dataSize = data.size(); int oldDataSize = mData.size(); mPageDivider.add(oldDataSize + dataSize); mEndPage++; + mNextPage = nextPage; + mPages = Math.max(mEndPage, pages); if (data.isEmpty()) { if (true || mEndPage >= mPages) { // OK, that's all @@ -565,6 +619,7 @@ public void onGetPageData(int taskId, List data) { } } else { mData.addAll(data); + onAddData(data); notifyItemRangeInserted(oldDataSize, dataSize); // Ui change, show content @@ -587,11 +642,14 @@ public void onGetPageData(int taskId, List data) { case TYPE_SOMEWHERE: mStartPage = mCurrentTaskPage; mEndPage = mCurrentTaskPage + 1; + mNextPage = nextPage; + mPages = pages; mPageDivider.clear(); mPageDivider.add(data.size()); if (data.isEmpty()) { mData.clear(); + onClearData(); notifyDataSetChanged(); if (true || mEndPage >= mPages) { // Not found @@ -613,7 +671,9 @@ public void onGetPageData(int taskId, List data) { } } else { mData.clear(); + onClearData(); mData.addAll(data); + onAddData(data); notifyDataSetChanged(); // Ui change, show content @@ -636,12 +696,22 @@ public void onGetPageData(int taskId, List data) { break; } + if (mCurrentTaskPage == mEndPage - 1) { + mNextPage = nextPage; + } + + mPages = Math.max(mEndPage, pages); + int oldIndexStart = mCurrentTaskPage == mStartPage ? 0 : mPageDivider.get(mCurrentTaskPage - mStartPage - 1); int oldIndexEnd = mPageDivider.get(mCurrentTaskPage - mStartPage); - mData.subList(oldIndexStart, oldIndexEnd).clear(); + List toRemove = mData.subList(oldIndexStart, oldIndexEnd); + onRemoveData(toRemove); + toRemove.clear(); + removeDuplicateData(data, oldIndexStart - CHECK_DUPLICATE_RANGE, oldIndexStart + CHECK_DUPLICATE_RANGE); int newIndexStart = oldIndexStart; int newIndexEnd = newIndexStart + data.size(); mData.addAll(oldIndexStart, data); + onAddData(data); notifyDataSetChanged(); for (int i = mCurrentTaskPage - mStartPage, n = mPageDivider.size(); i < n; i++) { @@ -859,7 +929,7 @@ public void goTo(int page) throws IndexOutOfBoundsException { private int mSavedDataId = IntIdGenerator.INVALID_ID; - private Parcelable saveInstanceState(Parcelable superState) { + protected Parcelable saveInstanceState(Parcelable superState) { Bundle bundle = new Bundle(); bundle.putParcelable(KEY_SUPER, superState); int shownView = mViewTransition.getShownViewIndex(); @@ -883,19 +953,20 @@ private Parcelable saveInstanceState(Parcelable superState) { return bundle; } - private Parcelable restoreInstanceState(Parcelable state) { + protected Parcelable restoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; mViewTransition.showView(bundle.getInt(KEY_SHOWN_VIEW), false); mTipView.setText(bundle.getString(KEY_TIP)); mSavedDataId = bundle.getInt(KEY_DATA); + ArrayList newData = null; EhApplication app = (EhApplication) getContext().getApplicationContext(); if (mSavedDataId != IntIdGenerator.INVALID_ID) { - ArrayList data = (ArrayList) app.removeGlobalStuff(mSavedDataId); + newData = (ArrayList) app.removeGlobalStuff(mSavedDataId); mSavedDataId = IntIdGenerator.INVALID_ID; - if (data != null) { - mData = data; + if (newData != null) { + mData = newData; } } @@ -904,6 +975,17 @@ private Parcelable restoreInstanceState(Parcelable state) { mStartPage = bundle.getInt(KEY_START_PAGE); mEndPage = bundle.getInt(KEY_END_PAGE); mPages = bundle.getInt(KEY_PAGES); + + notifyDataSetChanged(); + + if (newData == null) { + mPageDivider.clear(); + mStartPage = 0; + mEndPage = 0; + mPages = 0; + firstRefresh(); + } + return bundle.getParcelable(KEY_SUPER); } else { return state; diff --git a/app/src/main/java/com/hippo/widget/CuteSpinner.java b/app/src/main/java/com/hippo/widget/CuteSpinner.java index b8b014eae..ce8119f46 100644 --- a/app/src/main/java/com/hippo/widget/CuteSpinner.java +++ b/app/src/main/java/com/hippo/widget/CuteSpinner.java @@ -18,28 +18,27 @@ import android.content.Context; import android.content.res.Resources; -import android.support.v7.widget.AppCompatSpinner; -import android.support.v7.widget.TintTypedArray; +import android.content.res.TypedArray; import android.util.AttributeSet; import android.widget.ArrayAdapter; - +import androidx.appcompat.widget.AppCompatSpinner; import com.hippo.ehviewer.R; public class CuteSpinner extends AppCompatSpinner { public CuteSpinner(Context context) { super(context); - init(context, null, android.support.v7.appcompat.R.attr.spinnerStyle); + init(context, null, androidx.appcompat.R.attr.spinnerStyle); } public CuteSpinner(Context context, int mode) { super(context, mode); - init(context, null, android.support.v7.appcompat.R.attr.spinnerStyle); + init(context, null, androidx.appcompat.R.attr.spinnerStyle); } public CuteSpinner(Context context, AttributeSet attrs) { super(context, attrs); - init(context, attrs, android.support.v7.appcompat.R.attr.spinnerStyle); + init(context, attrs, androidx.appcompat.R.attr.spinnerStyle); } public CuteSpinner(Context context, AttributeSet attrs, int defStyleAttr) { @@ -58,8 +57,8 @@ public CuteSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mo } private void init(Context context, AttributeSet attrs, int defStyleAttr) { - TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs, - android.support.v7.appcompat.R.styleable.Spinner, defStyleAttr, 0); + TypedArray a = context.obtainStyledAttributes(attrs, + androidx.appcompat.R.styleable.Spinner, defStyleAttr, 0); final CharSequence[] entries = a.getTextArray(R.styleable.Spinner_android_entries); if (entries != null) { final ArrayAdapter adapter = new ArrayAdapter<>(context, diff --git a/app/src/main/java/com/hippo/widget/DirExplorer.java b/app/src/main/java/com/hippo/widget/DirExplorer.java index b2ade3db9..5cad27e29 100644 --- a/app/src/main/java/com/hippo/widget/DirExplorer.java +++ b/app/src/main/java/com/hippo/widget/DirExplorer.java @@ -17,20 +17,21 @@ package com.hippo.widget; import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; import android.os.Environment; -import android.support.v7.widget.LinearLayoutManager; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - +import androidx.recyclerview.widget.LinearLayoutManager; +import com.hippo.android.resource.AttrResources; import com.hippo.easyrecyclerview.EasyRecyclerView; import com.hippo.easyrecyclerview.LinearDividerItemDecoration; import com.hippo.ehviewer.R; import com.hippo.ripple.Ripple; import com.hippo.yorozuya.LayoutUtils; - import java.io.File; import java.io.FileFilter; import java.util.ArrayList; @@ -73,11 +74,11 @@ private void init(Context context) { setAdapter(mAdapter); setLayoutManager(new LinearLayoutManager(context)); LinearDividerItemDecoration decoration = new LinearDividerItemDecoration( - LinearDividerItemDecoration.VERTICAL, getResources().getColor(R.color.divider), + LinearDividerItemDecoration.VERTICAL, AttrResources.getAttrColor(context, R.attr.dividerColor), LayoutUtils.dp2pix(context, 1)); decoration.setShowLastDivider(true); addItemDecoration(decoration); - setSelector(Ripple.generateRippleDrawable(context, false)); + setSelector(Ripple.generateRippleDrawable(context, !AttrResources.getAttrBoolean(context, R.attr.isLightTheme), new ColorDrawable(Color.TRANSPARENT))); setOnItemClickListener(this); mCurrentFile = Environment.getExternalStorageDirectory(); diff --git a/app/src/main/java/com/hippo/widget/FabLayout.java b/app/src/main/java/com/hippo/widget/FabLayout.java index a1e4e7116..cc8b7f672 100644 --- a/app/src/main/java/com/hippo/widget/FabLayout.java +++ b/app/src/main/java/com/hippo/widget/FabLayout.java @@ -20,19 +20,17 @@ import android.content.Context; import android.os.Bundle; import android.os.Parcelable; -import android.support.annotation.NonNull; -import android.support.design.widget.FloatingActionButton; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.animation.Interpolator; - +import androidx.annotation.NonNull; +import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.hippo.ehviewer.R; import com.hippo.yorozuya.AnimationUtils; +import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.SimpleAnimatorListener; -import junit.framework.Assert; - public class FabLayout extends ViewGroup implements View.OnClickListener { private static final long ANIMATE_TIME = 300L; @@ -107,11 +105,24 @@ public FloatingActionButton getSecondaryFabAt(int index) { return (FloatingActionButton) getChildAt(index); } + public void setSecondaryFabVisibilityAt(int index, boolean visible) { + View fab = getSecondaryFabAt(index); + if (fab != null) { + if (visible && fab.getVisibility() == View.GONE) { + fab.animate().cancel(); + fab.setVisibility(mExpanded ? View.VISIBLE : View.INVISIBLE); + } else if (!visible && fab.getVisibility() != View.GONE) { + fab.animate().cancel(); + fab.setVisibility(View.GONE); + } + } + } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - Assert.assertEquals("Measure mode must be MeasureSpec.EXACTLY", + AssertUtils.assertEquals("Measure mode must be MeasureSpec.EXACTLY", MeasureSpec.getMode(widthMeasureSpec), MeasureSpec.EXACTLY); - Assert.assertEquals("Measure mode must be MeasureSpec.EXACTLY", + AssertUtils.assertEquals("Measure mode must be MeasureSpec.EXACTLY", MeasureSpec.getMode(heightMeasureSpec), MeasureSpec.EXACTLY); int width = MeasureSpec.getSize(widthMeasureSpec); @@ -232,6 +243,9 @@ public void setExpanded(boolean expanded, boolean animation) { int checkCount = mHidePrimaryFab ? count : count - 1; for (int i = 0; i < checkCount; i++) { View child = getChildAt(i); + if (child.getVisibility() == GONE) { + continue; + } child.setVisibility(expanded ? View.VISIBLE : View.INVISIBLE); if (expanded) { child.setAlpha(1f); @@ -244,6 +258,9 @@ public void setExpanded(boolean expanded, boolean animation) { for (int i = 0; i < count - 1; i++) { View child = getChildAt(i); + if (child.getVisibility() == GONE) { + continue; + } setSecondaryFabAnimation(child, expanded, expanded); } } diff --git a/app/src/main/java/com/hippo/widget/FixedAspectImageView.java b/app/src/main/java/com/hippo/widget/FixedAspectImageView.java index 2da8a7fdd..471da0a6f 100644 --- a/app/src/main/java/com/hippo/widget/FixedAspectImageView.java +++ b/app/src/main/java/com/hippo/widget/FixedAspectImageView.java @@ -19,10 +19,9 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; -import android.support.v7.widget.AppCompatImageView; import android.util.AttributeSet; import android.view.View; - +import androidx.appcompat.widget.AppCompatImageView; import com.hippo.ehviewer.R; import com.hippo.yorozuya.MathUtils; diff --git a/app/src/main/java/com/hippo/widget/HackyTextInputEditText.java b/app/src/main/java/com/hippo/widget/HackyTextInputEditText.java new file mode 100644 index 000000000..b8dd51bb9 --- /dev/null +++ b/app/src/main/java/com/hippo/widget/HackyTextInputEditText.java @@ -0,0 +1,84 @@ +/* + * Copyright 2018 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.TextView; +import com.google.android.material.textfield.TextInputEditText; +import com.hippo.util.ExceptionUtils; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +// Avoid crash on some Meizu devices +// https://github.com/android-in-china/Compatibility/issues/11 +// https://stackoverflow.com/questions/51891415/nullpointerexception-on-meizu-devices-in-editor-updatecursorpositionmz/52001305 +public class HackyTextInputEditText extends TextInputEditText { + + private static final boolean HAS_METHOD_UPDATE_CURSOR_POSITION_MZ; + private static final Field FIELD_M_HINT; + + static { + boolean hasMethodUpdateCursorPositionMz = false; + try { + Class clazz = ClassLoader.getSystemClassLoader().loadClass("android.widget.Editor"); + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + if ("updateCursorPositionMz".equals(method.getName())) { + hasMethodUpdateCursorPositionMz = true; + } + } + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); + } + + Field fieldMHint = null; + if (hasMethodUpdateCursorPositionMz) { + try { + fieldMHint = TextView.class.getDeclaredField("mHint"); + fieldMHint.setAccessible(true); + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); + } + } + + HAS_METHOD_UPDATE_CURSOR_POSITION_MZ = hasMethodUpdateCursorPositionMz; + FIELD_M_HINT = fieldMHint; + } + + public HackyTextInputEditText(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public CharSequence getHint() { + if (HAS_METHOD_UPDATE_CURSOR_POSITION_MZ && FIELD_M_HINT != null) { + try { + return getSuperHint(); + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); + return super.getHint(); + } + } else { + return super.getHint(); + } + } + + private CharSequence getSuperHint() throws IllegalAccessException { + return (CharSequence) FIELD_M_HINT.get(this); + } +} diff --git a/app/src/main/java/com/hippo/widget/IndicatingListView.java b/app/src/main/java/com/hippo/widget/IndicatingListView.java index 70ddff806..1ce79e20e 100644 --- a/app/src/main/java/com/hippo/widget/IndicatingListView.java +++ b/app/src/main/java/com/hippo/widget/IndicatingListView.java @@ -22,10 +22,9 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; -import android.support.annotation.NonNull; import android.util.AttributeSet; import android.widget.ListView; - +import androidx.annotation.NonNull; import com.hippo.ehviewer.R; public class IndicatingListView extends ListView { diff --git a/app/src/main/java/com/hippo/widget/IndicatingScrollView.java b/app/src/main/java/com/hippo/widget/IndicatingScrollView.java index a2dcabcc5..a503a24b9 100644 --- a/app/src/main/java/com/hippo/widget/IndicatingScrollView.java +++ b/app/src/main/java/com/hippo/widget/IndicatingScrollView.java @@ -22,10 +22,9 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; -import android.support.annotation.NonNull; import android.util.AttributeSet; import android.widget.ScrollView; - +import androidx.annotation.NonNull; import com.hippo.ehviewer.R; public class IndicatingScrollView extends ScrollView { diff --git a/app/src/main/java/com/hippo/widget/LinkifyTextView.java b/app/src/main/java/com/hippo/widget/LinkifyTextView.java index deab26aa2..45eb356e5 100644 --- a/app/src/main/java/com/hippo/widget/LinkifyTextView.java +++ b/app/src/main/java/com/hippo/widget/LinkifyTextView.java @@ -17,12 +17,12 @@ package com.hippo.widget; import android.content.Context; -import android.support.annotation.NonNull; import android.text.Layout; import android.text.Spanned; import android.text.style.ClickableSpan; import android.util.AttributeSet; import android.view.MotionEvent; +import androidx.annotation.NonNull; public class LinkifyTextView extends ObservedTextView { diff --git a/app/src/main/java/com/hippo/widget/LoadImageView.java b/app/src/main/java/com/hippo/widget/LoadImageView.java index 26fb2ab4d..41a3b98b4 100644 --- a/app/src/main/java/com/hippo/widget/LoadImageView.java +++ b/app/src/main/java/com/hippo/widget/LoadImageView.java @@ -23,13 +23,12 @@ import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; -import android.support.annotation.DrawableRes; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; import android.util.AttributeSet; import android.util.Log; import android.view.View; - +import androidx.annotation.DrawableRes; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; import com.hippo.conaco.Conaco; import com.hippo.conaco.ConacoTask; import com.hippo.conaco.DataContainer; @@ -40,7 +39,7 @@ import com.hippo.image.ImageBitmap; import com.hippo.image.ImageDrawable; import com.hippo.image.RecycledException; - +import com.hippo.util.DrawableManager; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -240,11 +239,13 @@ public void load(String key, String url, DataContainer container, boolean useNet public void load(Drawable drawable) { unload(); + onPreSetImageDrawable(drawable, true); setImageDrawable(drawable); } public void load(@DrawableRes int id) { unload(); + onPreSetImageResource(id, true); setImageResource(id); } @@ -291,6 +292,7 @@ public boolean onGetValue(@NonNull ImageBitmap value, int source) { drawable = new PreciselyClipDrawable(drawable, mOffsetX, mOffsetY, mClipWidth, mClipHeight); } + onPreSetImageDrawable(drawable, true); if ((source == Conaco.SOURCE_DISK || source == Conaco.SOURCE_NETWORK) && isShown()) { Drawable[] layers = new Drawable[2]; layers[0] = new ColorDrawable(Color.TRANSPARENT); @@ -309,7 +311,9 @@ public boolean onGetValue(@NonNull ImageBitmap value, int source) { public void onFailure() { mFailed = true; clearDrawable(); - setImageDrawable(getContext().getResources().getDrawable(R.drawable.image_failed)); + Drawable drawable = DrawableManager.getVectorDrawable(getContext(), R.drawable.image_failed); + onPreSetImageDrawable(drawable, true); + setImageDrawable(drawable); if (mRetryType == RETRY_TYPE_CLICK) { setOnClickListener(this); } else if (mRetryType == RETRY_TYPE_LONG_CLICK) { @@ -363,4 +367,8 @@ public boolean onLongClick(@NonNull View v) { load(mKey, mUrl, mContainer, true); return true; } + + public void onPreSetImageDrawable(Drawable drawable, boolean isTarget) { } + + public void onPreSetImageResource(int resId, boolean isTarget) { } } diff --git a/app/src/main/java/com/hippo/widget/MaxSizeContainer.java b/app/src/main/java/com/hippo/widget/MaxSizeContainer.java index e3dcd0eca..8cdf7feaf 100644 --- a/app/src/main/java/com/hippo/widget/MaxSizeContainer.java +++ b/app/src/main/java/com/hippo/widget/MaxSizeContainer.java @@ -21,10 +21,8 @@ import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; - import com.hippo.ehviewer.R; - -import junit.framework.Assert; +import com.hippo.yorozuya.AssertUtils; public class MaxSizeContainer extends ViewGroup { @@ -70,7 +68,7 @@ private int getMeasureSpec(int measureSpec, int max) { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - Assert.assertEquals("getChildCount() must be 1", 1, getChildCount()); + AssertUtils.assertEquals("getChildCount() must be 1", 1, getChildCount()); View child = getChildAt(0); if (child.getVisibility() != GONE) { @@ -84,7 +82,7 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { - Assert.assertEquals("getChildCount() must be 1", 1, getChildCount()); + AssertUtils.assertEquals("getChildCount() must be 1", 1, getChildCount()); View child = getChildAt(0); if (child.getVisibility() != GONE) { diff --git a/app/src/main/java/com/hippo/widget/ObservedTextView.java b/app/src/main/java/com/hippo/widget/ObservedTextView.java index 730f53363..e8f488e6e 100644 --- a/app/src/main/java/com/hippo/widget/ObservedTextView.java +++ b/app/src/main/java/com/hippo/widget/ObservedTextView.java @@ -18,9 +18,9 @@ import android.content.Context; import android.util.AttributeSet; -import android.widget.TextView; +import androidx.appcompat.widget.AppCompatTextView; -public class ObservedTextView extends TextView { +public class ObservedTextView extends AppCompatTextView { private OnWindowAttachListener mOnWindowAttachListener; diff --git a/app/src/main/java/com/hippo/widget/ProgressView.java b/app/src/main/java/com/hippo/widget/ProgressView.java index d25fe710f..3a0189bae 100644 --- a/app/src/main/java/com/hippo/widget/ProgressView.java +++ b/app/src/main/java/com/hippo/widget/ProgressView.java @@ -25,18 +25,16 @@ import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; -import android.support.annotation.NonNull; -import android.support.v4.view.ViewCompat; -import android.support.v4.view.animation.PathInterpolatorCompat; import android.util.AttributeSet; import android.view.View; import android.view.animation.Animation; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; - +import androidx.annotation.NonNull; +import androidx.core.view.ViewCompat; +import androidx.core.view.animation.PathInterpolatorCompat; import com.hippo.ehviewer.R; import com.hippo.yorozuya.ViewUtils; - import java.util.ArrayList; // Base on android.graphics.drawable.MaterialProgressDrawable in L preview diff --git a/app/src/main/java/com/hippo/widget/RadioGridGroup.java b/app/src/main/java/com/hippo/widget/RadioGridGroup.java index 7ca2d05a3..c0161dcc4 100644 --- a/app/src/main/java/com/hippo/widget/RadioGridGroup.java +++ b/app/src/main/java/com/hippo/widget/RadioGridGroup.java @@ -18,13 +18,12 @@ import android.content.Context; import android.content.res.TypedArray; -import android.support.annotation.IdRes; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.widget.CompoundButton; import android.widget.RadioButton; - +import androidx.annotation.IdRes; import com.hippo.yorozuya.ViewUtils; public class RadioGridGroup extends SimpleGridLayout { diff --git a/app/src/main/java/com/hippo/widget/SafeCoordinatorLayout.java b/app/src/main/java/com/hippo/widget/SafeCoordinatorLayout.java index 37feb9cb2..339014964 100644 --- a/app/src/main/java/com/hippo/widget/SafeCoordinatorLayout.java +++ b/app/src/main/java/com/hippo/widget/SafeCoordinatorLayout.java @@ -17,9 +17,10 @@ package com.hippo.widget; import android.content.Context; -import android.support.design.widget.CoordinatorLayout; import android.util.AttributeSet; import android.view.MotionEvent; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import com.hippo.util.ExceptionUtils; public class SafeCoordinatorLayout extends CoordinatorLayout { @@ -39,7 +40,8 @@ public SafeCoordinatorLayout(Context context, AttributeSet attrs, int defStyleAt public boolean onInterceptTouchEvent(MotionEvent ev) { try { return super.onInterceptTouchEvent(ev); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); return false; } } @@ -48,7 +50,8 @@ public boolean onInterceptTouchEvent(MotionEvent ev) { public boolean onTouchEvent(MotionEvent ev) { try { return super.onTouchEvent(ev); - } catch (Exception e) { + } catch (Throwable e) { + ExceptionUtils.throwIfFatal(e); return false; } } diff --git a/app/src/main/java/com/hippo/widget/SearchBarMover.java b/app/src/main/java/com/hippo/widget/SearchBarMover.java index 7a8192a41..928726fde 100644 --- a/app/src/main/java/com/hippo/widget/SearchBarMover.java +++ b/app/src/main/java/com/hippo/widget/SearchBarMover.java @@ -18,10 +18,9 @@ import android.animation.Animator; import android.animation.ValueAnimator; -import android.support.annotation.Nullable; -import android.support.v7.widget.RecyclerView; import android.view.View; - +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; import com.hippo.yorozuya.MathUtils; import com.hippo.yorozuya.SimpleAnimatorListener; import com.hippo.yorozuya.ViewUtils; diff --git a/app/src/main/java/com/hippo/widget/SensitiveCheckBox.java b/app/src/main/java/com/hippo/widget/SensitiveCheckBox.java index 9adb97e46..ab33d1af3 100644 --- a/app/src/main/java/com/hippo/widget/SensitiveCheckBox.java +++ b/app/src/main/java/com/hippo/widget/SensitiveCheckBox.java @@ -17,8 +17,8 @@ package com.hippo.widget; import android.content.Context; -import android.support.v7.widget.AppCompatCheckBox; import android.util.AttributeSet; +import androidx.appcompat.widget.AppCompatCheckBox; public class SensitiveCheckBox extends AppCompatCheckBox { diff --git a/app/src/main/java/com/hippo/widget/ShadowLinearLayout.java b/app/src/main/java/com/hippo/widget/ShadowLinearLayout.java index 8db7a9f85..79d798f4d 100644 --- a/app/src/main/java/com/hippo/widget/ShadowLinearLayout.java +++ b/app/src/main/java/com/hippo/widget/ShadowLinearLayout.java @@ -21,11 +21,10 @@ import android.graphics.Rect; import android.graphics.drawable.NinePatchDrawable; import android.os.Build; -import android.support.annotation.NonNull; import android.util.AttributeSet; import android.view.ViewOutlineProvider; import android.widget.LinearLayout; - +import androidx.annotation.NonNull; import com.hippo.ehviewer.R; import com.hippo.yorozuya.LayoutUtils; diff --git a/app/src/main/java/com/hippo/widget/Slider.java b/app/src/main/java/com/hippo/widget/Slider.java index 288957639..df27b2210 100644 --- a/app/src/main/java/com/hippo/widget/Slider.java +++ b/app/src/main/java/com/hippo/widget/Slider.java @@ -27,9 +27,6 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; -import android.support.annotation.NonNull; -import android.support.v4.graphics.drawable.DrawableCompat; -import android.support.v7.widget.AppCompatImageView; import android.util.AttributeSet; import android.view.Gravity; import android.view.MotionEvent; @@ -37,7 +34,9 @@ import android.view.ViewConfiguration; import android.widget.AbsoluteLayout; import android.widget.PopupWindow; - +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.core.graphics.drawable.DrawableCompat; import com.hippo.ehviewer.R; import com.hippo.yorozuya.AnimationUtils; import com.hippo.yorozuya.LayoutUtils; diff --git a/app/src/main/java/com/hippo/widget/TextClock.java b/app/src/main/java/com/hippo/widget/TextClock.java index 2cf52369c..52bf6bf16 100644 --- a/app/src/main/java/com/hippo/widget/TextClock.java +++ b/app/src/main/java/com/hippo/widget/TextClock.java @@ -30,12 +30,11 @@ import android.text.format.DateFormat; import android.util.AttributeSet; import android.view.ViewDebug.ExportedProperty; -import android.widget.TextView; - +import androidx.appcompat.widget.AppCompatTextView; import java.util.Calendar; import java.util.TimeZone; -public class TextClock extends TextView { +public class TextClock extends AppCompatTextView { public static final CharSequence DEFAULT_FORMAT_12_HOUR = "hh:mm a"; public static final CharSequence DEFAULT_FORMAT_24_HOUR; diff --git a/app/src/main/java/com/hippo/widget/lockpattern/LockPatternView.java b/app/src/main/java/com/hippo/widget/lockpattern/LockPatternView.java index d2167c0b0..8839af89d 100644 --- a/app/src/main/java/com/hippo/widget/lockpattern/LockPatternView.java +++ b/app/src/main/java/com/hippo/widget/lockpattern/LockPatternView.java @@ -29,18 +29,17 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; -import android.support.annotation.NonNull; -import android.support.v4.view.animation.FastOutSlowInInterpolator; -import android.support.v4.view.animation.LinearOutSlowInInterpolator; import android.util.AttributeSet; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityManager; import android.view.animation.Interpolator; - +import androidx.annotation.NonNull; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; +import androidx.interpolator.view.animation.LinearOutSlowInInterpolator; +import com.hippo.android.resource.AttrResources; import com.hippo.ehviewer.R; - import java.util.ArrayList; import java.util.List; @@ -270,7 +269,8 @@ public LockPatternView(Context context, AttributeSet attrs) { mPathPaint.setAntiAlias(true); mPathPaint.setDither(true); - mRegularColor = getResources().getColor(R.color.lock_pattern_view_regular_color); + mRegularColor = getResources().getColor(AttrResources.getAttrBoolean(context, R.attr.isLightTheme) ? + R.color.lock_pattern_view_regular_color_light : R.color.lock_pattern_view_regular_color_dark); mErrorColor = getResources().getColor(R.color.lock_pattern_view_error_color); mSuccessColor = getResources().getColor(R.color.lock_pattern_view_success_color); diff --git a/app/src/main/java/com/hippo/widget/recyclerview/AutoGridLayoutManager.java b/app/src/main/java/com/hippo/widget/recyclerview/AutoGridLayoutManager.java index 2a3db9810..9a1669603 100644 --- a/app/src/main/java/com/hippo/widget/recyclerview/AutoGridLayoutManager.java +++ b/app/src/main/java/com/hippo/widget/recyclerview/AutoGridLayoutManager.java @@ -17,9 +17,8 @@ package com.hippo.widget.recyclerview; import android.content.Context; -import android.support.v7.widget.GridLayoutManager; -import android.support.v7.widget.RecyclerView; - +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; @@ -79,7 +78,7 @@ public static int getSpanCountForMinSize(int total, int single) { public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { if (mColumnSizeChanged && mColumnSize > 0) { int totalSpace; - if (getOrientation() == VERTICAL) { + if (getOrientation() == RecyclerView.VERTICAL) { totalSpace = getWidth() - getPaddingRight() - getPaddingLeft(); } else { totalSpace = getHeight() - getPaddingTop() - getPaddingBottom(); diff --git a/app/src/main/java/com/hippo/widget/recyclerview/AutoStaggeredGridLayoutManager.java b/app/src/main/java/com/hippo/widget/recyclerview/AutoStaggeredGridLayoutManager.java index df5b36ba1..5f30e494a 100644 --- a/app/src/main/java/com/hippo/widget/recyclerview/AutoStaggeredGridLayoutManager.java +++ b/app/src/main/java/com/hippo/widget/recyclerview/AutoStaggeredGridLayoutManager.java @@ -16,10 +16,9 @@ package com.hippo.widget.recyclerview; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.StaggeredGridLayoutManager; import android.view.View; - +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; import java.util.ArrayList; import java.util.List; diff --git a/app/src/main/res/color/preference_header_title_light.xml b/app/src/main/res/color/content_reactive_black.xml similarity index 79% rename from app/src/main/res/color/preference_header_title_light.xml rename to app/src/main/res/color/content_reactive_black.xml index 4f7807db5..433e7e6af 100644 --- a/app/src/main/res/color/preference_header_title_light.xml +++ b/app/src/main/res/color/content_reactive_black.xml @@ -1,6 +1,6 @@ - - + + + + diff --git a/app/src/main/res/color/secondary_text_dark.xml b/app/src/main/res/color/content_reactive_light.xml similarity index 78% rename from app/src/main/res/color/secondary_text_dark.xml rename to app/src/main/res/color/content_reactive_light.xml index fd8b3f636..37b385e37 100644 --- a/app/src/main/res/color/secondary_text_dark.xml +++ b/app/src/main/res/color/content_reactive_light.xml @@ -1,6 +1,6 @@ - - + + + + diff --git a/app/src/main/res/color/primary_text_light.xml b/app/src/main/res/color/primary_text_material_black.xml similarity index 84% rename from app/src/main/res/color/primary_text_light.xml rename to app/src/main/res/color/primary_text_material_black.xml index 6dc9eb3c1..456b910e3 100644 --- a/app/src/main/res/color/primary_text_light.xml +++ b/app/src/main/res/color/primary_text_material_black.xml @@ -1,6 +1,6 @@ - - + + diff --git a/app/src/main/res/drawable-hdpi/sadpanda_low_poly.webp b/app/src/main/res/drawable-hdpi-v15/sadpanda_low_poly.webp similarity index 100% rename from app/src/main/res/drawable-hdpi/sadpanda_low_poly.webp rename to app/src/main/res/drawable-hdpi-v15/sadpanda_low_poly.webp diff --git a/app/src/main/res/drawable-hdpi/card_grey_no_padding_2dp.9.png b/app/src/main/res/drawable-hdpi/card_grey_no_padding_2dp.9.png deleted file mode 100644 index 42e65c7bd..000000000 Binary files a/app/src/main/res/drawable-hdpi/card_grey_no_padding_2dp.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/card_white_no_padding_2dp.9.png b/app/src/main/res/drawable-hdpi/card_white_no_padding_2dp.9.png deleted file mode 100644 index 4066b40b9..000000000 Binary files a/app/src/main/res/drawable-hdpi/card_white_no_padding_2dp.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/image_failed.png b/app/src/main/res/drawable-hdpi/image_failed.png deleted file mode 100644 index 7855526d6..000000000 Binary files a/app/src/main/res/drawable-hdpi/image_failed.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/sadpanda_low_poly.webp b/app/src/main/res/drawable-mdpi-v15/sadpanda_low_poly.webp similarity index 100% rename from app/src/main/res/drawable-mdpi/sadpanda_low_poly.webp rename to app/src/main/res/drawable-mdpi-v15/sadpanda_low_poly.webp diff --git a/app/src/main/res/drawable-mdpi/card_grey_no_padding_2dp.9.png b/app/src/main/res/drawable-mdpi/card_grey_no_padding_2dp.9.png deleted file mode 100644 index 7353a334b..000000000 Binary files a/app/src/main/res/drawable-mdpi/card_grey_no_padding_2dp.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/card_white_no_padding_2dp.9.png b/app/src/main/res/drawable-mdpi/card_white_no_padding_2dp.9.png deleted file mode 100644 index 4b41cb22f..000000000 Binary files a/app/src/main/res/drawable-mdpi/card_white_no_padding_2dp.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/image_failed.png b/app/src/main/res/drawable-mdpi/image_failed.png deleted file mode 100644 index 90244a276..000000000 Binary files a/app/src/main/res/drawable-mdpi/image_failed.png and /dev/null differ diff --git a/app/src/main/res/drawable-v25/ic_shortcut_start.xml b/app/src/main/res/drawable-v25/ic_shortcut_start.xml new file mode 100644 index 000000000..364be6f25 --- /dev/null +++ b/app/src/main/res/drawable-v25/ic_shortcut_start.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable-v25/ic_shortcut_stop.xml b/app/src/main/res/drawable-v25/ic_shortcut_stop.xml new file mode 100644 index 000000000..8920f7368 --- /dev/null +++ b/app/src/main/res/drawable-v25/ic_shortcut_stop.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable-v26/ic_shortcut_start.xml b/app/src/main/res/drawable-v26/ic_shortcut_start.xml new file mode 100644 index 000000000..7ee07242d --- /dev/null +++ b/app/src/main/res/drawable-v26/ic_shortcut_start.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v26/ic_shortcut_stop.xml b/app/src/main/res/drawable-v26/ic_shortcut_stop.xml new file mode 100644 index 000000000..fe1717b42 --- /dev/null +++ b/app/src/main/res/drawable-v26/ic_shortcut_stop.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/sadpanda_low_poly.webp b/app/src/main/res/drawable-xhdpi-v15/sadpanda_low_poly.webp similarity index 100% rename from app/src/main/res/drawable-xhdpi/sadpanda_low_poly.webp rename to app/src/main/res/drawable-xhdpi-v15/sadpanda_low_poly.webp diff --git a/app/src/main/res/drawable-xhdpi/card_grey_no_padding_2dp.9.png b/app/src/main/res/drawable-xhdpi/card_grey_no_padding_2dp.9.png deleted file mode 100644 index 51888f0db..000000000 Binary files a/app/src/main/res/drawable-xhdpi/card_grey_no_padding_2dp.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/card_white_no_padding_2dp.9.png b/app/src/main/res/drawable-xhdpi/card_white_no_padding_2dp.9.png deleted file mode 100644 index 07838d0c2..000000000 Binary files a/app/src/main/res/drawable-xhdpi/card_white_no_padding_2dp.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/image_failed.png b/app/src/main/res/drawable-xhdpi/image_failed.png deleted file mode 100644 index 76cfd7eb4..000000000 Binary files a/app/src/main/res/drawable-xhdpi/image_failed.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/sadpanda_low_poly.webp b/app/src/main/res/drawable-xxhdpi-v15/sadpanda_low_poly.webp similarity index 100% rename from app/src/main/res/drawable-xxhdpi/sadpanda_low_poly.webp rename to app/src/main/res/drawable-xxhdpi-v15/sadpanda_low_poly.webp diff --git a/app/src/main/res/drawable-xxhdpi/card_grey_no_padding_2dp.9.png b/app/src/main/res/drawable-xxhdpi/card_grey_no_padding_2dp.9.png deleted file mode 100644 index 0ebb0ddc7..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/card_grey_no_padding_2dp.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/card_white_no_padding_2dp.9.png b/app/src/main/res/drawable-xxhdpi/card_white_no_padding_2dp.9.png deleted file mode 100644 index 2c05ddeb3..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/card_white_no_padding_2dp.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_fp_40px.png b/app/src/main/res/drawable-xxhdpi/ic_fp_40px.png index 0fb854524..e9869f6bb 100755 Binary files a/app/src/main/res/drawable-xxhdpi/ic_fp_40px.png and b/app/src/main/res/drawable-xxhdpi/ic_fp_40px.png differ diff --git a/app/src/main/res/drawable-xxhdpi/image_failed.png b/app/src/main/res/drawable-xxhdpi/image_failed.png deleted file mode 100644 index 4caa2805e..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/image_failed.png and /dev/null differ diff --git a/app/src/main/res/drawable/big_download.xml b/app/src/main/res/drawable/big_download.xml index 7addd999a..7f04b6e88 100644 --- a/app/src/main/res/drawable/big_download.xml +++ b/app/src/main/res/drawable/big_download.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/big_filter.xml b/app/src/main/res/drawable/big_filter.xml index 87dd72398..b61bf432a 100644 --- a/app/src/main/res/drawable/big_filter.xml +++ b/app/src/main/res/drawable/big_filter.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/big_history.xml b/app/src/main/res/drawable/big_history.xml index 7f3b7b7ac..f3350d390 100644 --- a/app/src/main/res/drawable/big_history.xml +++ b/app/src/main/res/drawable/big_history.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/big_label.xml b/app/src/main/res/drawable/big_label.xml index 5b3a77f6b..77d2ead88 100644 --- a/app/src/main/res/drawable/big_label.xml +++ b/app/src/main/res/drawable/big_label.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/big_sad_pandroid.xml b/app/src/main/res/drawable/big_sad_pandroid.xml new file mode 100644 index 000000000..4079eede5 --- /dev/null +++ b/app/src/main/res/drawable/big_sad_pandroid.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/big_search.xml b/app/src/main/res/drawable/big_search.xml index 20e758ad4..1c0f547ec 100644 --- a/app/src/main/res/drawable/big_search.xml +++ b/app/src/main/res/drawable/big_search.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/card_background_no_padding.xml b/app/src/main/res/drawable/divider_gallery_detail_dark.xml similarity index 75% rename from app/src/main/res/drawable/card_background_no_padding.xml rename to app/src/main/res/drawable/divider_gallery_detail_dark.xml index c12e115e8..0924cb242 100644 --- a/app/src/main/res/drawable/card_background_no_padding.xml +++ b/app/src/main/res/drawable/divider_gallery_detail_dark.xml @@ -1,6 +1,6 @@ - - - + - + + + diff --git a/app/src/main/res/drawable/divider_gallery_detail.xml b/app/src/main/res/drawable/divider_gallery_detail_light.xml similarity index 94% rename from app/src/main/res/drawable/divider_gallery_detail.xml rename to app/src/main/res/drawable/divider_gallery_detail_light.xml index 557dfc2a8..d28ede080 100644 --- a/app/src/main/res/drawable/divider_gallery_detail.xml +++ b/app/src/main/res/drawable/divider_gallery_detail_light.xml @@ -19,7 +19,7 @@ xmlns:android="http://schemas.android.com/apk/res/android"> + android:color="@color/divider_light"/> diff --git a/app/src/main/res/drawable-mdpi/ic_fingerprint_error.xml b/app/src/main/res/drawable/fingerprint_error.xml old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/res/drawable-mdpi/ic_fingerprint_error.xml rename to app/src/main/res/drawable/fingerprint_error.xml diff --git a/app/src/main/res/drawable-mdpi/ic_fingerprint_success.xml b/app/src/main/res/drawable/fingerprint_success.xml old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/res/drawable-mdpi/ic_fingerprint_success.xml rename to app/src/main/res/drawable/fingerprint_success.xml diff --git a/app/src/main/res/drawable/ic_pause_108dp.xml b/app/src/main/res/drawable/ic_pause_108dp.xml new file mode 100644 index 000000000..aff1b7b4c --- /dev/null +++ b/app/src/main/res/drawable/ic_pause_108dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_play_arrow_108dp.xml b/app/src/main/res/drawable/ic_play_arrow_108dp.xml new file mode 100644 index 000000000..2a04d42f5 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_arrow_108dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/big_weird_face.xml b/app/src/main/res/drawable/image_failed.xml similarity index 83% rename from app/src/main/res/drawable/big_weird_face.xml rename to app/src/main/res/drawable/image_failed.xml index a53f280dd..8a968a063 100644 --- a/app/src/main/res/drawable/big_weird_face.xml +++ b/app/src/main/res/drawable/image_failed.xml @@ -17,13 +17,13 @@ + android:fillColor="?attr/drawableColorPrimary" + android:pathData="@string/pd_file_image"/> diff --git a/app/src/main/res/color/secondary_text_light.xml b/app/src/main/res/drawable/preference_header_background_black.xml similarity index 78% rename from app/src/main/res/color/secondary_text_light.xml rename to app/src/main/res/drawable/preference_header_background_black.xml index 67b0b4e3e..604e688b6 100644 --- a/app/src/main/res/color/secondary_text_light.xml +++ b/app/src/main/res/drawable/preference_header_background_black.xml @@ -1,6 +1,6 @@ - - + + + + diff --git a/app/src/main/res/drawable/preference_header_background_dark.xml b/app/src/main/res/drawable/preference_header_background_dark.xml new file mode 100644 index 000000000..b230b22ab --- /dev/null +++ b/app/src/main/res/drawable/preference_header_background_dark.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/preference_header_background.xml b/app/src/main/res/drawable/preference_header_background_light.xml similarity index 100% rename from app/src/main/res/drawable/preference_header_background.xml rename to app/src/main/res/drawable/preference_header_background_light.xml diff --git a/app/src/main/res/drawable/spacer_x6.xml b/app/src/main/res/drawable/spacer_x6.xml new file mode 100644 index 000000000..9beea5d49 --- /dev/null +++ b/app/src/main/res/drawable/spacer_x6.xml @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/v_adb_primary_x24.xml b/app/src/main/res/drawable/v_adb_primary_x24.xml index b155c431c..e3ae9b090 100644 --- a/app/src/main/res/drawable/v_adb_primary_x24.xml +++ b/app/src/main/res/drawable/v_adb_primary_x24.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/v_archive_primary_x48.xml b/app/src/main/res/drawable/v_archive_primary_x48.xml index 60342ec30..6801b4cfe 100644 --- a/app/src/main/res/drawable/v_archive_primary_x48.xml +++ b/app/src/main/res/drawable/v_archive_primary_x48.xml @@ -23,7 +23,7 @@ android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/v_book_open_primary_x24.xml b/app/src/main/res/drawable/v_book_open_primary_x24.xml index 070ae7992..c0d0069d1 100644 --- a/app/src/main/res/drawable/v_book_open_primary_x24.xml +++ b/app/src/main/res/drawable/v_book_open_primary_x24.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/v_book_open_x24.xml b/app/src/main/res/drawable/v_book_open_x24.xml new file mode 100644 index 000000000..9fa1fdaed --- /dev/null +++ b/app/src/main/res/drawable/v_book_open_x24.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/v_check_dark_x24.xml b/app/src/main/res/drawable/v_check_dark_x24.xml new file mode 100644 index 000000000..695620ecf --- /dev/null +++ b/app/src/main/res/drawable/v_check_dark_x24.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/v_copy_x24.xml b/app/src/main/res/drawable/v_copy_x24.xml new file mode 100644 index 000000000..284e254ce --- /dev/null +++ b/app/src/main/res/drawable/v_copy_x24.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/v_delete_x24.xml b/app/src/main/res/drawable/v_delete_x24.xml index e8544990b..d59a54efa 100644 --- a/app/src/main/res/drawable/v_delete_x24.xml +++ b/app/src/main/res/drawable/v_delete_x24.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/v_download_dark_x24.xml b/app/src/main/res/drawable/v_download_dark_x24.xml new file mode 100644 index 000000000..d56e0c549 --- /dev/null +++ b/app/src/main/res/drawable/v_download_dark_x24.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/v_download_primary_x24.xml b/app/src/main/res/drawable/v_download_primary_x24.xml index 5d37e9fda..792e20b28 100644 --- a/app/src/main/res/drawable/v_download_primary_x24.xml +++ b/app/src/main/res/drawable/v_download_primary_x24.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/v_download_x16.xml b/app/src/main/res/drawable/v_download_x16.xml new file mode 100644 index 000000000..e7976b267 --- /dev/null +++ b/app/src/main/res/drawable/v_download_x16.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/v_download_x24.xml b/app/src/main/res/drawable/v_download_x24.xml new file mode 100644 index 000000000..94353a77e --- /dev/null +++ b/app/src/main/res/drawable/v_download_x24.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/v_eh_subscription_black_x24.xml b/app/src/main/res/drawable/v_eh_subscription_black_x24.xml new file mode 100644 index 000000000..020119277 --- /dev/null +++ b/app/src/main/res/drawable/v_eh_subscription_black_x24.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/v_file_find_primary_x48.xml b/app/src/main/res/drawable/v_file_find_primary_x48.xml index efba8f12b..4c75cfc39 100644 --- a/app/src/main/res/drawable/v_file_find_primary_x48.xml +++ b/app/src/main/res/drawable/v_file_find_primary_x48.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/v_heart_broken_x24.xml b/app/src/main/res/drawable/v_heart_broken_x24.xml new file mode 100644 index 000000000..d11f24f42 --- /dev/null +++ b/app/src/main/res/drawable/v_heart_broken_x24.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/v_heart_outline_primary_x48.xml b/app/src/main/res/drawable/v_heart_outline_primary_x48.xml index 9a27594ca..42d85326f 100644 --- a/app/src/main/res/drawable/v_heart_outline_primary_x48.xml +++ b/app/src/main/res/drawable/v_heart_outline_primary_x48.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/v_heart_primary_x48.xml b/app/src/main/res/drawable/v_heart_primary_x48.xml index 5d20e6f9f..c3e473ebe 100644 --- a/app/src/main/res/drawable/v_heart_primary_x48.xml +++ b/app/src/main/res/drawable/v_heart_primary_x48.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/v_heart_x16.xml b/app/src/main/res/drawable/v_heart_x16.xml new file mode 100644 index 000000000..cb2dfc70e --- /dev/null +++ b/app/src/main/res/drawable/v_heart_x16.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/v_heart_x24.xml b/app/src/main/res/drawable/v_heart_x24.xml new file mode 100644 index 000000000..bc4c9cddc --- /dev/null +++ b/app/src/main/res/drawable/v_heart_x24.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/v_help_circle_x24.xml b/app/src/main/res/drawable/v_help_circle_x24.xml index e4d08478a..586dbb848 100644 --- a/app/src/main/res/drawable/v_help_circle_x24.xml +++ b/app/src/main/res/drawable/v_help_circle_x24.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/v_info_primary_x24.xml b/app/src/main/res/drawable/v_info_primary_x24.xml index 1b8122ab1..360564fb5 100644 --- a/app/src/main/res/drawable/v_info_primary_x24.xml +++ b/app/src/main/res/drawable/v_info_primary_x24.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/v_magnify_x24.xml b/app/src/main/res/drawable/v_magnify_x24.xml index 6d0b6bf34..552b8e9c6 100644 --- a/app/src/main/res/drawable/v_magnify_x24.xml +++ b/app/src/main/res/drawable/v_magnify_x24.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/v_pause_x24.xml b/app/src/main/res/drawable/v_pause_x24.xml index 052084756..097e64bac 100644 --- a/app/src/main/res/drawable/v_pause_x24.xml +++ b/app/src/main/res/drawable/v_pause_x24.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/v_pencil_dark_x24.xml b/app/src/main/res/drawable/v_pencil_dark_x24.xml new file mode 100644 index 000000000..0818a4e59 --- /dev/null +++ b/app/src/main/res/drawable/v_pencil_dark_x24.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/v_play_x24.xml b/app/src/main/res/drawable/v_play_x24.xml index 922a22ceb..17a315c0a 100644 --- a/app/src/main/res/drawable/v_play_x24.xml +++ b/app/src/main/res/drawable/v_play_x24.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/v_reorder_x24.xml b/app/src/main/res/drawable/v_reorder_x24.xml new file mode 100644 index 000000000..0505d1b72 --- /dev/null +++ b/app/src/main/res/drawable/v_reorder_x24.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/v_sad_panda_primary_x24.xml b/app/src/main/res/drawable/v_sad_panda_primary_x24.xml index 2c6b974a9..6508d0591 100644 --- a/app/src/main/res/drawable/v_sad_panda_primary_x24.xml +++ b/app/src/main/res/drawable/v_sad_panda_primary_x24.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/v_sec_primary_x24.xml b/app/src/main/res/drawable/v_sec_primary_x24.xml new file mode 100644 index 000000000..0614fccf5 --- /dev/null +++ b/app/src/main/res/drawable/v_sec_primary_x24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/v_share_primary_x48.xml b/app/src/main/res/drawable/v_share_primary_x48.xml index c9137acac..f781df8dc 100644 --- a/app/src/main/res/drawable/v_share_primary_x48.xml +++ b/app/src/main/res/drawable/v_share_primary_x48.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/v_similar_primary_x48.xml b/app/src/main/res/drawable/v_similar_primary_x48.xml index c20289ee6..111737cfd 100644 --- a/app/src/main/res/drawable/v_similar_primary_x48.xml +++ b/app/src/main/res/drawable/v_similar_primary_x48.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/v_thumb_up_primary_x48.xml b/app/src/main/res/drawable/v_thumb_up_primary_x48.xml index bb3ca1987..ec5694230 100644 --- a/app/src/main/res/drawable/v_thumb_up_primary_x48.xml +++ b/app/src/main/res/drawable/v_thumb_up_primary_x48.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/v_utorrent_primary_x48.xml b/app/src/main/res/drawable/v_utorrent_primary_x48.xml index b403246d6..1a9fb3719 100644 --- a/app/src/main/res/drawable/v_utorrent_primary_x48.xml +++ b/app/src/main/res/drawable/v_utorrent_primary_x48.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/layout-v21/drawer_list.xml b/app/src/main/res/layout-v21/drawer_list.xml index 47236053d..1a461448a 100644 --- a/app/src/main/res/layout-v21/drawer_list.xml +++ b/app/src/main/res/layout-v21/drawer_list.xml @@ -21,14 +21,16 @@ android:layout_height="match_parent" android:orientation="vertical"> - + android:saveEnabled="false" + android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" + app:popupTheme="?attr/toolbarPopupTheme"/> + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-v21/scene_toolbar.xml b/app/src/main/res/layout-v21/scene_toolbar.xml index 6423b2fe3..02cb0ebdd 100644 --- a/app/src/main/res/layout-v21/scene_toolbar.xml +++ b/app/src/main/res/layout-v21/scene_toolbar.xml @@ -21,13 +21,15 @@ android:layout_height="match_parent" android:orientation="vertical"> - + app:popupTheme="?attr/toolbarPopupTheme"/> - + + + + + + diff --git a/app/src/main/res/layout/activity_gallery.xml b/app/src/main/res/layout/activity_gallery.xml index 2abe6e831..bd179bc96 100644 --- a/app/src/main/res/layout/activity_gallery.xml +++ b/app/src/main/res/layout/activity_gallery.xml @@ -26,34 +26,42 @@ android:layout_width="match_parent" android:layout_height="match_parent"/> - - - - - + android:layout_gravity="top"> + + + + + + + + diff --git a/app/src/main/res/layout/activity_hosts.xml b/app/src/main/res/layout/activity_hosts.xml new file mode 100644 index 000000000..ab6e0ec62 --- /dev/null +++ b/app/src/main/res/layout/activity_hosts.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index f457299a4..7b0dc08bd 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -34,13 +34,30 @@ + android:orientation="vertical" + android:background="?android:attr/windowBackground"> + + + +